package gus import ( "context" "crypto/tls" "net/url" "strings" "tildegit.org/tjp/gus/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 requst path. type Router struct { tree internal.PathTree[Handler] } // Route adds a handler to the router under a path pattern. func (r Router) Route(pattern string, handler Handler) { r.tree.Add(pattern, handler) } // Handler matches against the request path and dipatches to a route handler. // // If no route matches, it 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(ctx context.Context, request *Request) *Response { handler, params := r.Match(request) if handler == nil { return nil } // as we may be a sub-router, check for existing stashed params // and combine with that map if found. priorParams := RouteParams(ctx) for k, v := range priorParams { if k == subrouterPathKey { continue } params[k] = v } return handler(context.WithValue(ctx, routeParamsKey, params), request) } // Match returns the matched handler and captured path parameters, or nils. 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, "/") r.Route(prefix+"/*"+subrouterPathKey, func(ctx context.Context, request *Request) *Response { r := cloneRequest(request) r.Path = "/" + RouteParams(ctx)[subrouterPathKey] return subrouter.Handler(ctx, r) }) // TODO: better approach. the above works but it's a little hacky // - add a method to PathTree that returns all the registered patterns // and their associated handlers // - have Mount pull those out of the subrouter, prepend the prefix to // all its patterns, and re-add them to the parent router. } // 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 } const subrouterPathKey = "subrouter_path" type routeParamsKeyType struct{} var routeParamsKey = routeParamsKeyType{} func cloneRequest(start *Request) *Request { end := &Request{} *end = *start end.URL = &url.URL{} *end.URL = *start.URL if start.TLSState != nil { end.TLSState = &tls.ConnectionState{} *end.TLSState = *start.TLSState } return end }