package sliderule import ( "context" "path" "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 containing a 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. It // may also optionally have text before the colon, in which case the pattern // will not match unless the request path segment contains that prefix. // - 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 } // Handle implements Handler // // If no route matches, Handle 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) Handle(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) } // Handler builds a Handler // // It is only here for compatibility because Router implements Handler directly. func (r Router) Handler() Handler { return r } // 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. // // The root pattern ("/") in the sub-router will become a route which may or may not // end with a forward slash. func (r *Router) Mount(prefix string, subrouter *Router) { prefix = strings.TrimSuffix(prefix, "/") for _, subroute := range subrouter.tree.Routes() { fullroute := path.Join(prefix, subroute.Pattern) if strings.HasSuffix(subroute.Pattern, "/") { fullroute = fullroute + "/" } r.Route(fullroute, subroute.Value) if subroute.Pattern == "/" || subroute.Pattern == "" { r.Route(prefix, subroute.Value) r.Route(prefix+"/", 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{}