summaryrefslogtreecommitdiff
path: root/router.go
blob: dfa936e779d719b2cde33aaaed495f450332becf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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{}