trout

Paddy 2015-12-13 Parent:bf38b050b6c4 Child:3df515f0cec5

2:b966f38379dd Go to Latest

trout/route.go

Publish under MIT license. Add a license so this package can officially be used by any project, basically.

History
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 }