You've already forked openaccounting-server
mirror of
https://github.com/openaccounting/oa-server.git
synced 2025-12-09 00:50:59 +13:00
195 lines
4.7 KiB
Go
195 lines
4.7 KiB
Go
package rest
|
|
|
|
import (
|
|
"errors"
|
|
"github.com/ant0ine/go-json-rest/rest/trie"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
type router struct {
|
|
Routes []*Route
|
|
|
|
disableTrieCompression bool
|
|
index map[*Route]int
|
|
trie *trie.Trie
|
|
}
|
|
|
|
// MakeRouter returns the router app. Given a set of Routes, it dispatches the request to the
|
|
// HandlerFunc of the first route that matches. The order of the Routes matters.
|
|
func MakeRouter(routes ...*Route) (App, error) {
|
|
r := &router{
|
|
Routes: routes,
|
|
}
|
|
err := r.start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// Handle the REST routing and run the user code.
|
|
func (rt *router) AppFunc() HandlerFunc {
|
|
return func(writer ResponseWriter, request *Request) {
|
|
|
|
// find the route
|
|
route, params, pathMatched := rt.findRouteFromURL(request.Method, request.URL)
|
|
if route == nil {
|
|
|
|
if pathMatched {
|
|
// no route found, but path was matched: 405 Method Not Allowed
|
|
Error(writer, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// no route found, the path was not matched: 404 Not Found
|
|
NotFound(writer, request)
|
|
return
|
|
}
|
|
|
|
// a route was found, set the PathParams
|
|
request.PathParams = params
|
|
|
|
// run the user code
|
|
handler := route.Func
|
|
handler(writer, request)
|
|
}
|
|
}
|
|
|
|
// This is run for each new request, perf is important.
|
|
func escapedPath(urlObj *url.URL) string {
|
|
// the escape method of url.URL should be public
|
|
// that would avoid this split.
|
|
parts := strings.SplitN(urlObj.RequestURI(), "?", 2)
|
|
return parts[0]
|
|
}
|
|
|
|
var preEscape = strings.NewReplacer("*", "__SPLAT_PLACEHOLDER__", "#", "__RELAXED_PLACEHOLDER__")
|
|
|
|
var postEscape = strings.NewReplacer("__SPLAT_PLACEHOLDER__", "*", "__RELAXED_PLACEHOLDER__", "#")
|
|
|
|
// This is run at init time only.
|
|
func escapedPathExp(pathExp string) (string, error) {
|
|
|
|
// PathExp validation
|
|
if pathExp == "" {
|
|
return "", errors.New("empty PathExp")
|
|
}
|
|
if pathExp[0] != '/' {
|
|
return "", errors.New("PathExp must start with /")
|
|
}
|
|
if strings.Contains(pathExp, "?") {
|
|
return "", errors.New("PathExp must not contain the query string")
|
|
}
|
|
|
|
// Get the right escaping
|
|
// XXX a bit hacky
|
|
|
|
pathExp = preEscape.Replace(pathExp)
|
|
|
|
urlObj, err := url.Parse(pathExp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// get the same escaping as find requests
|
|
pathExp = urlObj.RequestURI()
|
|
|
|
pathExp = postEscape.Replace(pathExp)
|
|
|
|
return pathExp, nil
|
|
}
|
|
|
|
// This validates the Routes and prepares the Trie data structure.
|
|
// It must be called once the Routes are defined and before trying to find Routes.
|
|
// The order matters, if multiple Routes match, the first defined will be used.
|
|
func (rt *router) start() error {
|
|
|
|
rt.trie = trie.New()
|
|
rt.index = map[*Route]int{}
|
|
|
|
for i, route := range rt.Routes {
|
|
|
|
// work with the PathExp urlencoded.
|
|
pathExp, err := escapedPathExp(route.PathExp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// insert in the Trie
|
|
err = rt.trie.AddRoute(
|
|
strings.ToUpper(route.HttpMethod), // work with the HttpMethod in uppercase
|
|
pathExp,
|
|
route,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// index
|
|
rt.index[route] = i
|
|
}
|
|
|
|
if rt.disableTrieCompression == false {
|
|
rt.trie.Compress()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// return the result that has the route defined the earliest
|
|
func (rt *router) ofFirstDefinedRoute(matches []*trie.Match) *trie.Match {
|
|
minIndex := -1
|
|
var bestMatch *trie.Match
|
|
|
|
for _, result := range matches {
|
|
route := result.Route.(*Route)
|
|
routeIndex := rt.index[route]
|
|
if minIndex == -1 || routeIndex < minIndex {
|
|
minIndex = routeIndex
|
|
bestMatch = result
|
|
}
|
|
}
|
|
|
|
return bestMatch
|
|
}
|
|
|
|
// Return the first matching Route and the corresponding parameters for a given URL object.
|
|
func (rt *router) findRouteFromURL(httpMethod string, urlObj *url.URL) (*Route, map[string]string, bool) {
|
|
|
|
// lookup the routes in the Trie
|
|
matches, pathMatched := rt.trie.FindRoutesAndPathMatched(
|
|
strings.ToUpper(httpMethod), // work with the httpMethod in uppercase
|
|
escapedPath(urlObj), // work with the path urlencoded
|
|
)
|
|
|
|
// short cuts
|
|
if len(matches) == 0 {
|
|
// no route found
|
|
return nil, nil, pathMatched
|
|
}
|
|
|
|
if len(matches) == 1 {
|
|
// one route found
|
|
return matches[0].Route.(*Route), matches[0].Params, pathMatched
|
|
}
|
|
|
|
// multiple routes found, pick the first defined
|
|
result := rt.ofFirstDefinedRoute(matches)
|
|
return result.Route.(*Route), result.Params, pathMatched
|
|
}
|
|
|
|
// Parse the url string (complete or just the path) and return the first matching Route and the corresponding parameters.
|
|
func (rt *router) findRoute(httpMethod, urlStr string) (*Route, map[string]string, bool, error) {
|
|
|
|
// parse the url
|
|
urlObj, err := url.Parse(urlStr)
|
|
if err != nil {
|
|
return nil, nil, false, err
|
|
}
|
|
|
|
route, params, pathMatched := rt.findRouteFromURL(httpMethod, urlObj)
|
|
return route, params, pathMatched, nil
|
|
}
|