1
// Copyright 2013 Julien Schmidt. All rights reserved.
2
// Use of this source code is governed by a BSD-style license that can be found
3
// in the LICENSE file.
12
func min(a, b int) int {
19
func countParams(path string) uint8 {
21
for i := 0; i < len(path); i++ {
22
if path[i] != ':' && path[i] != '*' {
36
static nodeType = iota // default
53
// increments priority of the given child and reorders if necessary
54
func (n *node) incrementChildPrio(pos int) int {
55
n.children[pos].priority++
56
prio := n.children[pos].priority
58
// adjust position (move to front)
60
for newPos > 0 && n.children[newPos-1].priority < prio {
61
// swap node positions
62
tmpN := n.children[newPos-1]
63
n.children[newPos-1] = n.children[newPos]
64
n.children[newPos] = tmpN
69
// build new index char string
71
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
72
n.indices[pos:pos+1] + // the index char we move
73
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
79
// addRoute adds a node with the given handle to the path.
80
// Not concurrency-safe!
81
func (n *node) addRoute(path string, handle Handle) {
84
numParams := countParams(path)
87
if len(n.path) > 0 || len(n.children) > 0 {
90
// Update maxParams of the current node
91
if numParams > n.maxParams {
92
n.maxParams = numParams
95
// Find the longest common prefix.
96
// This also implies that the common prefix contains no ':' or '*'
97
// since the existing key can't contain those chars.
99
max := min(len(path), len(n.path))
100
for i < max && path[i] == n.path[i] {
108
wildChild: n.wildChild,
110
children: n.children,
112
priority: n.priority - 1,
115
// Update maxParams (max of all children)
116
for i := range child.children {
117
if child.children[i].maxParams > child.maxParams {
118
child.maxParams = child.children[i].maxParams
122
n.children = []*node{&child}
123
// []byte for proper unicode char conversion, see #65
124
n.indices = string([]byte{n.path[i]})
130
// Make new node a child of this node
138
// Update maxParams of the child node
139
if numParams > n.maxParams {
140
n.maxParams = numParams
144
// Check if the wildcard matches
145
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
146
// check for longer wildcard, e.g. :name and :names
147
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
152
panic("path segment '" + path +
153
"' conflicts with existing wildcard '" + n.path +
154
"' in path '" + fullPath + "'")
160
if n.nType == param && c == '/' && len(n.children) == 1 {
166
// Check if a child with the next path byte exists
167
for i := 0; i < len(n.indices); i++ {
168
if c == n.indices[i] {
169
i = n.incrementChildPrio(i)
175
// Otherwise insert it
176
if c != ':' && c != '*' {
177
// []byte for proper unicode char conversion, see #65
178
n.indices += string([]byte{c})
180
maxParams: numParams,
182
n.children = append(n.children, child)
183
n.incrementChildPrio(len(n.indices) - 1)
186
n.insertChild(numParams, path, fullPath, handle)
189
} else if i == len(path) { // Make node a (in-path) leaf
191
panic("a handle is already registered for path '" + fullPath + "'")
197
} else { // Empty tree
198
n.insertChild(numParams, path, fullPath, handle)
203
func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
204
var offset int // already handled bytes of the path
206
// find prefix until first wildcard (beginning with ':'' or '*'')
207
for i, max := 0, len(path); numParams > 0; i++ {
209
if c != ':' && c != '*' {
213
// find wildcard end (either '/' or path end)
215
for end < max && path[end] != '/' {
217
// the wildcard name must not contain ':' and '*'
219
panic("only one wildcard per path segment is allowed, has: '" +
220
path[i:] + "' in path '" + fullPath + "'")
226
// check if this Node existing children which would be
227
// unreachable if we insert the wildcard here
228
if len(n.children) > 0 {
229
panic("wildcard route '" + path[i:end] +
230
"' conflicts with existing children in path '" + fullPath + "'")
233
// check if the wildcard has a name
235
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
238
if c == ':' { // param
239
// split path at the beginning of the wildcard
241
n.path = path[offset:i]
247
maxParams: numParams,
249
n.children = []*node{child}
255
// if the path doesn't end with the wildcard, then there
256
// will be another non-wildcard subpath starting with '/'
258
n.path = path[offset:end]
262
maxParams: numParams,
265
n.children = []*node{child}
270
if end != max || numParams > 1 {
271
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
274
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
275
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
278
// currently fixed width 1 for '/'
281
panic("no / before catch-all in path '" + fullPath + "'")
284
n.path = path[offset:i]
286
// first node: catchAll node with empty path
292
n.children = []*node{child}
293
n.indices = string(path[i])
297
// second node: node holding the variable
305
n.children = []*node{child}
311
// insert remaining path part and handle to the leaf
312
n.path = path[offset:]
316
// Returns the handle registered with the given path (key). The values of
317
// wildcards are saved to a map.
318
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
319
// made if a handle exists with an extra (without the) trailing slash for the
321
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
322
walk: // Outer loop for walking the tree
324
if len(path) > len(n.path) {
325
if path[:len(n.path)] == n.path {
326
path = path[len(n.path):]
327
// If this node does not have a wildcard (param or catchAll)
328
// child, we can just look up the next child node and continue
329
// to walk down the tree
332
for i := 0; i < len(n.indices); i++ {
333
if c == n.indices[i] {
340
// We can recommend to redirect to the same URL without a
341
// trailing slash if a leaf exists for that path.
342
tsr = (path == "/" && n.handle != nil)
347
// handle wildcard child
351
// find param end (either '/' or path end)
353
for end < len(path) && path[end] != '/' {
360
p = make(Params, 0, n.maxParams)
363
p = p[:i+1] // expand slice within preallocated capacity
364
p[i].Key = n.path[1:]
365
p[i].Value = path[:end]
367
// we need to go deeper!
369
if len(n.children) > 0 {
376
tsr = (len(path) == end+1)
380
if handle = n.handle; handle != nil {
382
} else if len(n.children) == 1 {
383
// No handle found. Check if a handle for this path + a
384
// trailing slash exists for TSR recommendation
386
tsr = (n.path == "/" && n.handle != nil)
395
p = make(Params, 0, n.maxParams)
398
p = p[:i+1] // expand slice within preallocated capacity
399
p[i].Key = n.path[2:]
406
panic("invalid node type")
409
} else if path == n.path {
410
// We should have reached the node containing the handle.
411
// Check if this node has a handle registered.
412
if handle = n.handle; handle != nil {
416
if path == "/" && n.wildChild && n.nType != root {
421
// No handle found. Check if a handle for this path + a
422
// trailing slash exists for trailing slash recommendation
423
for i := 0; i < len(n.indices); i++ {
424
if n.indices[i] == '/' {
426
tsr = (len(n.path) == 1 && n.handle != nil) ||
427
(n.nType == catchAll && n.children[0].handle != nil)
435
// Nothing found. We can recommend to redirect to the same URL with an
436
// extra trailing slash if a leaf exists for that path
437
tsr = (path == "/") ||
438
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
439
path == n.path[:len(n.path)-1] && n.handle != nil)
444
// Makes a case-insensitive lookup of the given path and tries to find a handler.
445
// It can optionally also fix trailing slashes.
446
// It returns the case-corrected path and a bool indicating whether the lookup
448
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
449
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
451
// Outer loop for walking the tree
452
for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) {
453
path = path[len(n.path):]
454
ciPath = append(ciPath, n.path...)
457
// If this node does not have a wildcard (param or catchAll) child,
458
// we can just look up the next child node and continue to walk down
461
r := unicode.ToLower(rune(path[0]))
462
for i, index := range n.indices {
463
// must use recursive approach since both index and
464
// ToLower(index) could exist. We must check both.
465
if r == unicode.ToLower(index) {
466
out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash)
468
return append(ciPath, out...), true
473
// Nothing found. We can recommend to redirect to the same URL
474
// without a trailing slash if a leaf exists for that path
475
found = (fixTrailingSlash && path == "/" && n.handle != nil)
482
// find param end (either '/' or path end)
484
for k < len(path) && path[k] != '/' {
488
// add param value to case insensitive path
489
ciPath = append(ciPath, path[:k]...)
491
// we need to go deeper!
493
if len(n.children) > 0 {
500
if fixTrailingSlash && len(path) == k+1 {
508
} else if fixTrailingSlash && len(n.children) == 1 {
509
// No handle found. Check if a handle for this path + a
510
// trailing slash exists
512
if n.path == "/" && n.handle != nil {
513
return append(ciPath, '/'), true
519
return append(ciPath, path...), true
522
panic("invalid node type")
525
// We should have reached the node containing the handle.
526
// Check if this node has a handle registered.
532
// Try to fix the path by adding a trailing slash
533
if fixTrailingSlash {
534
for i := 0; i < len(n.indices); i++ {
535
if n.indices[i] == '/' {
537
if (len(n.path) == 1 && n.handle != nil) ||
538
(n.nType == catchAll && n.children[0].handle != nil) {
539
return append(ciPath, '/'), true
550
// Try to fix the path by adding / removing a trailing slash
551
if fixTrailingSlash {
555
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
556
strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) &&
558
return append(ciPath, n.path...), true