package sliderule import ( "context" "strings" "tildegit.org/tjp/sliderule/internal" ) // Router stores a mapping of request path patterns to handlers. // // Pattern may begin with "/" and then contain slash-delimited segments. // - Segments beginning with colon (:) are wildcards and will match any path // segment at that location. It may optionally have a word after the colon, // which will be the parameter name the path segment is captured into. // - Segments beginning with asterisk (*) are remainder wildcards. This must // come last and will capture any remainder of the path. It may have a name // after the asterisk which will be the parameter name. // - Any other segment in the pattern must match a path segment exactly. // // These patterns do not match any path which shares a prefix, rather then // full path must match a pattern. If you want to only match a prefix of the // path you can end the pattern with a *remainder segment. // // The zero value is a usable Router which will fail to match any request path. type Router struct { tree internal.PathTree[Handler] middleware []Middleware routeAdded bool } // Route adds a handler to the router under a path pattern. func (r *Router) Route(pattern string, handler Handler) { for i := len(r.middleware) - 1; i >= 0; i-- { handler = r.middleware[i](handler) } r.tree.Add(pattern, handler) r.routeAdded = true } // Handler builds a Handler which matches the request path and dispatches to a route. // // If no route matches, the handler returns a nil response. // Captured path parameters will be stored in the context passed into the handler // and can be retrieved with RouteParams(). func (r Router) Handler() Handler { return HandlerFunc(func(ctx context.Context, request *Request) *Response { handler, params := r.Match(request) if handler == nil { return nil } return handler.Handle(context.WithValue(ctx, routeParamsKey, params), request) }) } // Match returns the matched handler and captured path parameters, or (nil, nil). // // The returned handlers will be wrapped with any middleware attached to the router. func (r Router) Match(request *Request) (Handler, map[string]string) { handler, params := r.tree.Match(request.Path) if handler == nil { return nil, nil } return *handler, params } // Mount attaches a sub-router to handle path suffixes after an initial prefix pattern. // // The prefix pattern may include segment :wildcards, but no *remainder segment. The // mounted sub-router should have patterns which only include the portion of the path // after whatever was matched by the prefix pattern. func (r *Router) Mount(prefix string, subrouter *Router) { prefix = strings.TrimSuffix(prefix, "/") for _, subroute := range subrouter.tree.Routes() { r.Route(prefix+"/"+subroute.Pattern, subroute.Value) } } // Use attaches a middleware to the router. // // Any routes set on the router will have their handlers decorated by the attached // middlewares in reverse order (the first middleware attached will be the outer-most: // first to see requests and the last to see responses). // // Use will panic if Route or Mount have already been called on the router - // middlewares must be set before any routes. func (r *Router) Use(mw Middleware) { if r.routeAdded { panic("all middlewares must be added prior to adding routes") } r.middleware = append(r.middleware, mw) } // RouteParams gathers captured path parameters from the request context. // // If the context doesn't contain a parameter map, it returns nil. // If Router was used but no parameters were captured in the pattern, it // returns a non-nil empty map. func RouteParams(ctx context.Context) map[string]string { if m, ok := ctx.Value(routeParamsKey).(map[string]string); ok { return m } return nil } type routeParamsKeyType struct{} var routeParamsKey = routeParamsKeyType{}