trout
trout/route.go
Publish under MIT license. Add a license so this package can officially be used by any project, basically.
| paddy@0 | 1 package trout |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "net/http" |
| paddy@0 | 5 "strconv" |
| paddy@0 | 6 "strings" |
| paddy@0 | 7 "time" |
| paddy@0 | 8 ) |
| paddy@0 | 9 |
| paddy@0 | 10 var ( |
| paddy@0 | 11 default404Handler = http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| paddy@0 | 12 w.WriteHeader(http.StatusNotFound) |
| paddy@0 | 13 w.Write([]byte("404 Not Found")) |
| paddy@0 | 14 return |
| paddy@0 | 15 })) |
| paddy@0 | 16 default405Handler = http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| paddy@0 | 17 w.WriteHeader(http.StatusMethodNotAllowed) |
| paddy@0 | 18 w.Write([]byte("405 Method Not Allowed")) |
| paddy@0 | 19 return |
| paddy@0 | 20 })) |
| paddy@0 | 21 ) |
| paddy@0 | 22 |
| paddy@0 | 23 // RequestVars returns easy-to-access mappings of parameters to values for URL templates. Any {parameter} in |
| paddy@0 | 24 // your URL template will be available in the returned Header as a slice of strings, one for each instance of |
| paddy@0 | 25 // the {parameter}. In the case of a parameter name being used more than once in the same URL template, the |
| paddy@0 | 26 // values will be in the slice in the order they appeared in the template. |
| paddy@0 | 27 // |
| paddy@0 | 28 // Values can easily be accessed by using the .Get() method of the returned Header, though to access multiple |
| paddy@0 | 29 // values, they must be accessed through the map. All parameters use http.CanonicalHeaderKey for their formatting. |
| paddy@0 | 30 // When using .Get(), the parameter name will be transformed automatically. When utilising the Header as a map, |
| paddy@0 | 31 // the parameter name needs to have http.CanonicalHeaderKey applied manually. |
| paddy@0 | 32 func RequestVars(r *http.Request) http.Header { |
| paddy@0 | 33 res := http.Header{} |
| paddy@0 | 34 for h, v := range r.Header { |
| paddy@0 | 35 stripped := strings.TrimPrefix(h, http.CanonicalHeaderKey("Trout-Param-")) |
| paddy@0 | 36 if stripped != h { |
| paddy@0 | 37 res[stripped] = v |
| paddy@0 | 38 } |
| paddy@0 | 39 } |
| paddy@0 | 40 return res |
| paddy@0 | 41 } |
| paddy@0 | 42 |
| paddy@0 | 43 // Router defines a set of Endpoints that map requests to the http.Handlers. The http.Handler assigned to |
| paddy@0 | 44 // Handle404, if set, will be called when no Endpoint matches the current request. The http.Handler assigned |
| paddy@0 | 45 // to Handle405, if set, will be called when an Endpoint matches the current request, but has no http.Handler |
| paddy@0 | 46 // set for the HTTP method that the request used. Should either of these properties be unset, a default |
| paddy@0 | 47 // http.Handler will be used. |
| paddy@0 | 48 // |
| paddy@0 | 49 // The Router type is safe for use with empty values, but makes no attempt at concurrency-safety in adding |
| paddy@0 | 50 // Endpoints or in setting properties. It should also be noted that the adding Endpoints while simultaneously |
| paddy@0 | 51 // routing requests will lead to undefined and (almost certainly) undesirable behaviour. Routers are intended |
| paddy@0 | 52 // to be initialised with a set of Endpoints, and then start serving requests. Using them outside of this use |
| paddy@0 | 53 // case is unsupported. |
| paddy@0 | 54 type Router struct { |
| paddy@0 | 55 t *trie |
| paddy@0 | 56 Handle404 http.Handler |
| paddy@0 | 57 Handle405 http.Handler |
| paddy@0 | 58 } |
| paddy@0 | 59 |
| paddy@1 | 60 func (router *Router) serve404(w http.ResponseWriter, r *http.Request, t time.Time) { |
| paddy@0 | 61 h := default404Handler |
| paddy@0 | 62 if router.Handle404 != nil { |
| paddy@0 | 63 h = router.Handle404 |
| paddy@0 | 64 } |
| paddy@0 | 65 r.Header.Set("Trout-Timer", strconv.FormatInt(time.Now().Sub(t).Nanoseconds(), 10)) |
| paddy@0 | 66 h.ServeHTTP(w, r) |
| paddy@0 | 67 } |
| paddy@0 | 68 |
| paddy@1 | 69 func (router *Router) serve405(w http.ResponseWriter, r *http.Request, t time.Time) { |
| paddy@0 | 70 h := default405Handler |
| paddy@0 | 71 if router.Handle405 != nil { |
| paddy@0 | 72 h = router.Handle405 |
| paddy@0 | 73 } |
| paddy@0 | 74 r.Header.Set("Trout-Timer", strconv.FormatInt(time.Now().Sub(t).Nanoseconds(), 10)) |
| paddy@0 | 75 h.ServeHTTP(w, r) |
| paddy@0 | 76 } |
| paddy@0 | 77 |
| paddy@0 | 78 func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| paddy@0 | 79 start := time.Now() |
| paddy@0 | 80 if router.t == nil { |
| paddy@0 | 81 router.serve404(w, r, start) |
| paddy@1 | 82 return |
| paddy@0 | 83 } |
| paddy@0 | 84 pieces := strings.Split(strings.ToLower(strings.Trim(r.URL.Path, "/")), "/") |
| paddy@0 | 85 router.t.RLock() |
| paddy@0 | 86 defer router.t.RUnlock() |
| paddy@0 | 87 branches := make([]*branch, len(pieces)) |
| paddy@0 | 88 path, ok := router.t.match(pieces) |
| paddy@0 | 89 if !ok { |
| paddy@0 | 90 router.serve404(w, r, start) |
| paddy@1 | 91 return |
| paddy@0 | 92 } |
| paddy@0 | 93 b := router.t.branch |
| paddy@0 | 94 for i, pos := range path { |
| paddy@0 | 95 b = b.children[pos] |
| paddy@0 | 96 branches[i] = b |
| paddy@0 | 97 } |
| paddy@0 | 98 v := vars(branches, pieces) |
| paddy@0 | 99 for key, vals := range v { |
| paddy@0 | 100 r.Header[http.CanonicalHeaderKey("Trout-Param-"+key)] = vals |
| paddy@0 | 101 } |
| paddy@0 | 102 ms := make([]string, len(b.methods)) |
| paddy@0 | 103 i := 0 |
| paddy@0 | 104 for m := range b.methods { |
| paddy@0 | 105 ms[i] = m |
| paddy@0 | 106 i = i + 1 |
| paddy@0 | 107 } |
| paddy@0 | 108 r.Header[http.CanonicalHeaderKey("Trout-Methods")] = ms |
| paddy@0 | 109 h := b.methods[r.Method] |
| paddy@0 | 110 if h == nil { |
| paddy@0 | 111 router.serve405(w, r, start) |
| paddy@1 | 112 return |
| paddy@0 | 113 } |
| paddy@0 | 114 r.Header.Set("Trout-Timer", strconv.FormatInt(time.Now().Sub(start).Nanoseconds(), 10)) |
| paddy@0 | 115 h.ServeHTTP(w, r) |
| paddy@0 | 116 } |
| paddy@0 | 117 |
| paddy@0 | 118 // Endpoint defines a new Endpoint on the Router. The Endpoint should be a URL template, using curly braces |
| paddy@0 | 119 // to denote parameters that should be filled at runtime. For example, `{id}` denotes a parameter named `id` |
| paddy@0 | 120 // that should be filled with whatever the request has in that space. |
| paddy@0 | 121 // |
| paddy@0 | 122 // Parameters are always `/`-separated strings. There is no support for regular expressions or other limitations |
| paddy@0 | 123 // on what may be in those strings. A parameter is simply defined as "whatever is between these two / characters". |
| paddy@1 | 124 func (router *Router) Endpoint(e string) *Endpoint { |
| paddy@0 | 125 e = strings.Trim(e, "/") |
| paddy@0 | 126 e = strings.ToLower(e) |
| paddy@0 | 127 pieces := strings.Split(e, "/") |
| paddy@1 | 128 if router.t == nil { |
| paddy@1 | 129 router.t = &trie{} |
| paddy@1 | 130 } |
| paddy@0 | 131 router.t.Lock() |
| paddy@0 | 132 defer router.t.Unlock() |
| paddy@0 | 133 if router.t.branch == nil { |
| paddy@0 | 134 router.t.branch = &branch{ |
| paddy@0 | 135 parent: nil, |
| paddy@0 | 136 children: []*branch{}, |
| paddy@0 | 137 key: "", |
| paddy@0 | 138 isParam: false, |
| paddy@0 | 139 methods: map[string]http.Handler{}, |
| paddy@0 | 140 } |
| paddy@0 | 141 } |
| paddy@0 | 142 closest := findClosestLeaf(pieces, router.t.branch) |
| paddy@0 | 143 b := router.t.branch |
| paddy@0 | 144 for _, pos := range closest { |
| paddy@0 | 145 b = b.children[pos] |
| paddy@0 | 146 } |
| paddy@0 | 147 if len(closest) == len(pieces) { |
| paddy@0 | 148 return (*Endpoint)(b) |
| paddy@0 | 149 } |
| paddy@0 | 150 offset := len(closest) |
| paddy@0 | 151 for i := offset; i < len(pieces); i++ { |
| paddy@0 | 152 piece := pieces[i] |
| paddy@0 | 153 var isParam bool |
| paddy@1 | 154 if len(piece) > 0 && piece[0:1] == "{" && piece[len(piece)-1:] == "}" { |
| paddy@0 | 155 isParam = true |
| paddy@0 | 156 piece = piece[1 : len(piece)-1] |
| paddy@0 | 157 } |
| paddy@0 | 158 b = b.addChild(piece, isParam) |
| paddy@0 | 159 } |
| paddy@0 | 160 return (*Endpoint)(b) |
| paddy@0 | 161 } |
| paddy@0 | 162 |
| paddy@0 | 163 func vars(path []*branch, pieces []string) map[string][]string { |
| paddy@0 | 164 v := map[string][]string{} |
| paddy@0 | 165 for pos, p := range path { |
| paddy@0 | 166 if !p.isParam { |
| paddy@0 | 167 continue |
| paddy@0 | 168 } |
| paddy@0 | 169 _, ok := v[p.key] |
| paddy@0 | 170 if !ok { |
| paddy@0 | 171 v[p.key] = []string{pieces[pos]} |
| paddy@0 | 172 continue |
| paddy@0 | 173 } |
| paddy@0 | 174 v[p.key] = append(v[p.key], pieces[pos]) |
| paddy@0 | 175 } |
| paddy@0 | 176 return v |
| paddy@0 | 177 } |
| paddy@0 | 178 |
| paddy@0 | 179 func findClosestLeaf(pieces []string, b *branch) []int { |
| paddy@0 | 180 offset := 0 |
| paddy@0 | 181 path := []int{} |
| paddy@0 | 182 longest := []int{} |
| paddy@0 | 183 num := len(pieces) |
| paddy@0 | 184 for i := 0; i < num; i++ { |
| paddy@0 | 185 piece := pieces[i] |
| paddy@0 | 186 var isParam bool |
| paddy@1 | 187 if len(piece) > 0 && piece[0:1] == "{" && piece[len(piece)-1:] == "}" { |
| paddy@0 | 188 isParam = true |
| paddy@0 | 189 piece = piece[1 : len(piece)-1] |
| paddy@0 | 190 } |
| paddy@0 | 191 offset = pickNextRoute(b, offset, piece, isParam) |
| paddy@0 | 192 if offset == -1 { |
| paddy@0 | 193 if len(path) == 0 { |
| paddy@0 | 194 // exhausted our options, bail |
| paddy@0 | 195 break |
| paddy@0 | 196 } |
| paddy@0 | 197 // no match, maybe save this and backup |
| paddy@0 | 198 if len(path) > len(longest) { |
| paddy@0 | 199 longest = append([]int{}, path...) // copy them over so they don't get modified |
| paddy@0 | 200 } |
| paddy@0 | 201 path, offset = backup(path) |
| paddy@0 | 202 offset = offset + 1 |
| paddy@0 | 203 b = b.parent |
| paddy@0 | 204 i = i - 2 |
| paddy@0 | 205 } else { |
| paddy@0 | 206 path = append(path, offset) |
| paddy@0 | 207 b = b.children[offset] |
| paddy@0 | 208 offset = 0 |
| paddy@0 | 209 } |
| paddy@0 | 210 } |
| paddy@0 | 211 if len(longest) < len(path) { |
| paddy@0 | 212 longest = append([]int{}, path...) |
| paddy@0 | 213 } |
| paddy@0 | 214 return longest |
| paddy@0 | 215 } |
| paddy@0 | 216 |
| paddy@0 | 217 func pickNextRoute(b *branch, offset int, input string, variable bool) int { |
| paddy@0 | 218 count := len(b.children) |
| paddy@0 | 219 for i := offset; i < count; i++ { |
| paddy@0 | 220 if b.children[i].key == input && b.isParam == variable { |
| paddy@0 | 221 return i |
| paddy@0 | 222 } |
| paddy@0 | 223 } |
| paddy@0 | 224 return -1 |
| paddy@0 | 225 } |
| paddy@0 | 226 |
| paddy@0 | 227 // Endpoint defines a single URL template that requests can be matched against. It uses |
| paddy@0 | 228 // URL parameters to accept variables in the URL structure and make them available to |
| paddy@0 | 229 // the Handlers associated with the Endpoint. |
| paddy@0 | 230 type Endpoint branch |
| paddy@0 | 231 |
| paddy@0 | 232 // Handler associates the passed http.Handler with the Endpoint. This http.Handler will be |
| paddy@0 | 233 // used for all requests, regardless of the HTTP method they are using, unless overridden by |
| paddy@0 | 234 // the Methods method. Endpoints without a http.Handler associated with them will not be |
| paddy@0 | 235 // considered matches for requests, unless the request was made using an HTTP method that the |
| paddy@0 | 236 // Endpoint has an http.Handler mapped to. |
| paddy@0 | 237 func (e *Endpoint) Handler(h http.Handler) { |
| paddy@0 | 238 (*branch)(e).setHandler("", h) |
| paddy@0 | 239 } |
| paddy@0 | 240 |
| paddy@0 | 241 // Methods simple returns a Methods object that will enable the mapping of the passed HTTP |
| paddy@0 | 242 // request methods to a Methods object. On its own, this function does not modify anything. It |
| paddy@0 | 243 // should, instead, be used as a friendly shorthand to get to the Methods.Handler method. |
| paddy@0 | 244 func (e *Endpoint) Methods(m ...string) Methods { |
| paddy@0 | 245 return Methods{ |
| paddy@0 | 246 e: e, |
| paddy@0 | 247 m: m, |
| paddy@0 | 248 } |
| paddy@0 | 249 } |
| paddy@0 | 250 |
| paddy@0 | 251 // Methods defines a pairing of an Endpoint to the HTTP request methods that should be mapped to |
| paddy@0 | 252 // specific http.Handlers. Its sole purpose is to enable the Methods.Handler method. |
| paddy@0 | 253 type Methods struct { |
| paddy@0 | 254 e *Endpoint |
| paddy@0 | 255 m []string |
| paddy@0 | 256 } |
| paddy@0 | 257 |
| paddy@0 | 258 // Handler maps a Methods object to a specific http.Handler. This overrides the http.Handler |
| paddy@0 | 259 // associated with the Endpoint to only handle specific HTTP method(s). |
| paddy@0 | 260 func (m Methods) Handler(h http.Handler) { |
| paddy@0 | 261 b := (*branch)(m.e) |
| paddy@0 | 262 for _, method := range m.m { |
| paddy@0 | 263 b.setHandler(method, h) |
| paddy@0 | 264 } |
| paddy@0 | 265 } |