Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
466 lines
15 KiB
466 lines
15 KiB
package chi |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"net/http" |
|
"strings" |
|
"sync" |
|
) |
|
|
|
var _ Router = &Mux{} |
|
|
|
// Mux is a simple HTTP route multiplexer that parses a request path, |
|
// records any URL params, and executes an end handler. It implements |
|
// the http.Handler interface and is friendly with the standard library. |
|
// |
|
// Mux is designed to be fast, minimal and offer a powerful API for building |
|
// modular and composable HTTP services with a large set of handlers. It's |
|
// particularly useful for writing large REST API services that break a handler |
|
// into many smaller parts composed of middlewares and end handlers. |
|
type Mux struct { |
|
// The radix trie router |
|
tree *node |
|
|
|
// The middleware stack |
|
middlewares []func(http.Handler) http.Handler |
|
|
|
// Controls the behaviour of middleware chain generation when a mux |
|
// is registered as an inline group inside another mux. |
|
inline bool |
|
parent *Mux |
|
|
|
// The computed mux handler made of the chained middleware stack and |
|
// the tree router |
|
handler http.Handler |
|
|
|
// Routing context pool |
|
pool *sync.Pool |
|
|
|
// Custom route not found handler |
|
notFoundHandler http.HandlerFunc |
|
|
|
// Custom method not allowed handler |
|
methodNotAllowedHandler http.HandlerFunc |
|
} |
|
|
|
// NewMux returns a newly initialized Mux object that implements the Router |
|
// interface. |
|
func NewMux() *Mux { |
|
mux := &Mux{tree: &node{}, pool: &sync.Pool{}} |
|
mux.pool.New = func() interface{} { |
|
return NewRouteContext() |
|
} |
|
return mux |
|
} |
|
|
|
// ServeHTTP is the single method of the http.Handler interface that makes |
|
// Mux interoperable with the standard library. It uses a sync.Pool to get and |
|
// reuse routing contexts for each request. |
|
func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
// Ensure the mux has some routes defined on the mux |
|
if mx.handler == nil { |
|
mx.NotFoundHandler().ServeHTTP(w, r) |
|
return |
|
} |
|
|
|
// Check if a routing context already exists from a parent router. |
|
rctx, _ := r.Context().Value(RouteCtxKey).(*Context) |
|
if rctx != nil { |
|
mx.handler.ServeHTTP(w, r) |
|
return |
|
} |
|
|
|
// Fetch a RouteContext object from the sync pool, and call the computed |
|
// mx.handler that is comprised of mx.middlewares + mx.routeHTTP. |
|
// Once the request is finished, reset the routing context and put it back |
|
// into the pool for reuse from another request. |
|
rctx = mx.pool.Get().(*Context) |
|
rctx.Reset() |
|
rctx.Routes = mx |
|
|
|
// NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation |
|
r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) |
|
|
|
// Serve the request and once its done, put the request context back in the sync pool |
|
mx.handler.ServeHTTP(w, r) |
|
mx.pool.Put(rctx) |
|
} |
|
|
|
// Use appends a middleware handler to the Mux middleware stack. |
|
// |
|
// The middleware stack for any Mux will execute before searching for a matching |
|
// route to a specific handler, which provides opportunity to respond early, |
|
// change the course of the request execution, or set request-scoped values for |
|
// the next http.Handler. |
|
func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { |
|
if mx.handler != nil { |
|
panic("chi: all middlewares must be defined before routes on a mux") |
|
} |
|
mx.middlewares = append(mx.middlewares, middlewares...) |
|
} |
|
|
|
// Handle adds the route `pattern` that matches any http method to |
|
// execute the `handler` http.Handler. |
|
func (mx *Mux) Handle(pattern string, handler http.Handler) { |
|
mx.handle(mALL, pattern, handler) |
|
} |
|
|
|
// HandleFunc adds the route `pattern` that matches any http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mALL, pattern, handlerFn) |
|
} |
|
|
|
// Method adds the route `pattern` that matches `method` http method to |
|
// execute the `handler` http.Handler. |
|
func (mx *Mux) Method(method, pattern string, handler http.Handler) { |
|
m, ok := methodMap[strings.ToUpper(method)] |
|
if !ok { |
|
panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) |
|
} |
|
mx.handle(m, pattern, handler) |
|
} |
|
|
|
// MethodFunc adds the route `pattern` that matches `method` http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { |
|
mx.Method(method, pattern, handlerFn) |
|
} |
|
|
|
// Connect adds the route `pattern` that matches a CONNECT http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mCONNECT, pattern, handlerFn) |
|
} |
|
|
|
// Delete adds the route `pattern` that matches a DELETE http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mDELETE, pattern, handlerFn) |
|
} |
|
|
|
// Get adds the route `pattern` that matches a GET http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mGET, pattern, handlerFn) |
|
} |
|
|
|
// Head adds the route `pattern` that matches a HEAD http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mHEAD, pattern, handlerFn) |
|
} |
|
|
|
// Options adds the route `pattern` that matches a OPTIONS http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mOPTIONS, pattern, handlerFn) |
|
} |
|
|
|
// Patch adds the route `pattern` that matches a PATCH http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mPATCH, pattern, handlerFn) |
|
} |
|
|
|
// Post adds the route `pattern` that matches a POST http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mPOST, pattern, handlerFn) |
|
} |
|
|
|
// Put adds the route `pattern` that matches a PUT http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mPUT, pattern, handlerFn) |
|
} |
|
|
|
// Trace adds the route `pattern` that matches a TRACE http method to |
|
// execute the `handlerFn` http.HandlerFunc. |
|
func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { |
|
mx.handle(mTRACE, pattern, handlerFn) |
|
} |
|
|
|
// NotFound sets a custom http.HandlerFunc for routing paths that could |
|
// not be found. The default 404 handler is `http.NotFound`. |
|
func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { |
|
// Build NotFound handler chain |
|
m := mx |
|
hFn := handlerFn |
|
if mx.inline && mx.parent != nil { |
|
m = mx.parent |
|
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP |
|
} |
|
|
|
// Update the notFoundHandler from this point forward |
|
m.notFoundHandler = hFn |
|
m.updateSubRoutes(func(subMux *Mux) { |
|
if subMux.notFoundHandler == nil { |
|
subMux.NotFound(hFn) |
|
} |
|
}) |
|
} |
|
|
|
// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the |
|
// method is unresolved. The default handler returns a 405 with an empty body. |
|
func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { |
|
// Build MethodNotAllowed handler chain |
|
m := mx |
|
hFn := handlerFn |
|
if mx.inline && mx.parent != nil { |
|
m = mx.parent |
|
hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP |
|
} |
|
|
|
// Update the methodNotAllowedHandler from this point forward |
|
m.methodNotAllowedHandler = hFn |
|
m.updateSubRoutes(func(subMux *Mux) { |
|
if subMux.methodNotAllowedHandler == nil { |
|
subMux.MethodNotAllowed(hFn) |
|
} |
|
}) |
|
} |
|
|
|
// With adds inline middlewares for an endpoint handler. |
|
func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { |
|
// Similarly as in handle(), we must build the mux handler once additional |
|
// middleware registration isn't allowed for this stack, like now. |
|
if !mx.inline && mx.handler == nil { |
|
mx.buildRouteHandler() |
|
} |
|
|
|
// Copy middlewares from parent inline muxs |
|
var mws Middlewares |
|
if mx.inline { |
|
mws = make(Middlewares, len(mx.middlewares)) |
|
copy(mws, mx.middlewares) |
|
} |
|
mws = append(mws, middlewares...) |
|
|
|
im := &Mux{ |
|
pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, |
|
notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, |
|
} |
|
|
|
return im |
|
} |
|
|
|
// Group creates a new inline-Mux with a fresh middleware stack. It's useful |
|
// for a group of handlers along the same routing path that use an additional |
|
// set of middlewares. See _examples/. |
|
func (mx *Mux) Group(fn func(r Router)) Router { |
|
im := mx.With().(*Mux) |
|
if fn != nil { |
|
fn(im) |
|
} |
|
return im |
|
} |
|
|
|
// Route creates a new Mux with a fresh middleware stack and mounts it |
|
// along the `pattern` as a subrouter. Effectively, this is a short-hand |
|
// call to Mount. See _examples/. |
|
func (mx *Mux) Route(pattern string, fn func(r Router)) Router { |
|
subRouter := NewRouter() |
|
if fn != nil { |
|
fn(subRouter) |
|
} |
|
mx.Mount(pattern, subRouter) |
|
return subRouter |
|
} |
|
|
|
// Mount attaches another http.Handler or chi Router as a subrouter along a routing |
|
// path. It's very useful to split up a large API as many independent routers and |
|
// compose them as a single service using Mount. See _examples/. |
|
// |
|
// Note that Mount() simply sets a wildcard along the `pattern` that will continue |
|
// routing at the `handler`, which in most cases is another chi.Router. As a result, |
|
// if you define two Mount() routes on the exact same pattern the mount will panic. |
|
func (mx *Mux) Mount(pattern string, handler http.Handler) { |
|
// Provide runtime safety for ensuring a pattern isn't mounted on an existing |
|
// routing pattern. |
|
if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { |
|
panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) |
|
} |
|
|
|
// Assign sub-Router's with the parent not found & method not allowed handler if not specified. |
|
subr, ok := handler.(*Mux) |
|
if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { |
|
subr.NotFound(mx.notFoundHandler) |
|
} |
|
if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { |
|
subr.MethodNotAllowed(mx.methodNotAllowedHandler) |
|
} |
|
|
|
mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
rctx := RouteContext(r.Context()) |
|
rctx.RoutePath = mx.nextRoutePath(rctx) |
|
handler.ServeHTTP(w, r) |
|
}) |
|
|
|
if pattern == "" || pattern[len(pattern)-1] != '/' { |
|
mx.handle(mALL|mSTUB, pattern, mountHandler) |
|
mx.handle(mALL|mSTUB, pattern+"/", mountHandler) |
|
pattern += "/" |
|
} |
|
|
|
method := mALL |
|
subroutes, _ := handler.(Routes) |
|
if subroutes != nil { |
|
method |= mSTUB |
|
} |
|
n := mx.handle(method, pattern+"*", mountHandler) |
|
|
|
if subroutes != nil { |
|
n.subroutes = subroutes |
|
} |
|
} |
|
|
|
// Routes returns a slice of routing information from the tree, |
|
// useful for traversing available routes of a router. |
|
func (mx *Mux) Routes() []Route { |
|
return mx.tree.routes() |
|
} |
|
|
|
// Middlewares returns a slice of middleware handler functions. |
|
func (mx *Mux) Middlewares() Middlewares { |
|
return mx.middlewares |
|
} |
|
|
|
// Match searches the routing tree for a handler that matches the method/path. |
|
// It's similar to routing a http request, but without executing the handler |
|
// thereafter. |
|
// |
|
// Note: the *Context state is updated during execution, so manage |
|
// the state carefully or make a NewRouteContext(). |
|
func (mx *Mux) Match(rctx *Context, method, path string) bool { |
|
m, ok := methodMap[method] |
|
if !ok { |
|
return false |
|
} |
|
|
|
node, _, h := mx.tree.FindRoute(rctx, m, path) |
|
|
|
if node != nil && node.subroutes != nil { |
|
rctx.RoutePath = mx.nextRoutePath(rctx) |
|
return node.subroutes.Match(rctx, method, rctx.RoutePath) |
|
} |
|
|
|
return h != nil |
|
} |
|
|
|
// NotFoundHandler returns the default Mux 404 responder whenever a route |
|
// cannot be found. |
|
func (mx *Mux) NotFoundHandler() http.HandlerFunc { |
|
if mx.notFoundHandler != nil { |
|
return mx.notFoundHandler |
|
} |
|
return http.NotFound |
|
} |
|
|
|
// MethodNotAllowedHandler returns the default Mux 405 responder whenever |
|
// a method cannot be resolved for a route. |
|
func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { |
|
if mx.methodNotAllowedHandler != nil { |
|
return mx.methodNotAllowedHandler |
|
} |
|
return methodNotAllowedHandler |
|
} |
|
|
|
// buildRouteHandler builds the single mux handler that is a chain of the middleware |
|
// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this |
|
// point, no other middlewares can be registered on this Mux's stack. But you can still |
|
// compose additional middlewares via Group()'s or using a chained middleware handler. |
|
func (mx *Mux) buildRouteHandler() { |
|
mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) |
|
} |
|
|
|
// handle registers a http.Handler in the routing tree for a particular http method |
|
// and routing pattern. |
|
func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { |
|
if len(pattern) == 0 || pattern[0] != '/' { |
|
panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) |
|
} |
|
|
|
// Build the computed routing handler for this routing pattern. |
|
if !mx.inline && mx.handler == nil { |
|
mx.buildRouteHandler() |
|
} |
|
|
|
// Build endpoint handler with inline middlewares for the route |
|
var h http.Handler |
|
if mx.inline { |
|
mx.handler = http.HandlerFunc(mx.routeHTTP) |
|
h = Chain(mx.middlewares...).Handler(handler) |
|
} else { |
|
h = handler |
|
} |
|
|
|
// Add the endpoint to the tree and return the node |
|
return mx.tree.InsertRoute(method, pattern, h) |
|
} |
|
|
|
// routeHTTP routes a http.Request through the Mux routing tree to serve |
|
// the matching handler for a particular http method. |
|
func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { |
|
// Grab the route context object |
|
rctx := r.Context().Value(RouteCtxKey).(*Context) |
|
|
|
// The request routing path |
|
routePath := rctx.RoutePath |
|
if routePath == "" { |
|
if r.URL.RawPath != "" { |
|
routePath = r.URL.RawPath |
|
} else { |
|
routePath = r.URL.Path |
|
} |
|
} |
|
|
|
// Check if method is supported by chi |
|
if rctx.RouteMethod == "" { |
|
rctx.RouteMethod = r.Method |
|
} |
|
method, ok := methodMap[rctx.RouteMethod] |
|
if !ok { |
|
mx.MethodNotAllowedHandler().ServeHTTP(w, r) |
|
return |
|
} |
|
|
|
// Find the route |
|
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { |
|
h.ServeHTTP(w, r) |
|
return |
|
} |
|
if rctx.methodNotAllowed { |
|
mx.MethodNotAllowedHandler().ServeHTTP(w, r) |
|
} else { |
|
mx.NotFoundHandler().ServeHTTP(w, r) |
|
} |
|
} |
|
|
|
func (mx *Mux) nextRoutePath(rctx *Context) string { |
|
routePath := "/" |
|
nx := len(rctx.routeParams.Keys) - 1 // index of last param in list |
|
if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { |
|
routePath = "/" + rctx.routeParams.Values[nx] |
|
} |
|
return routePath |
|
} |
|
|
|
// Recursively update data on child routers. |
|
func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { |
|
for _, r := range mx.tree.routes() { |
|
subMux, ok := r.SubRoutes.(*Mux) |
|
if !ok { |
|
continue |
|
} |
|
fn(subMux) |
|
} |
|
} |
|
|
|
// methodNotAllowedHandler is a helper function to respond with a 405, |
|
// method not allowed. |
|
func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { |
|
w.WriteHeader(405) |
|
w.Write(nil) |
|
}
|
|
|