├── LICENSE ├── tree_test.go └── tree.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Rob Figueiredo 2 | All Rights Reserved. 3 | 4 | MIT LICENSE 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package pathtree 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestColon(t *testing.T) { 10 | n := New() 11 | 12 | n.Add("/:first/:second/", 1) 13 | n.Add("/:first", 2) 14 | n.Add("/", 3) 15 | 16 | found(t, n, "/", nil, 3) 17 | found(t, n, "/a", []string{"a"}, 2) 18 | found(t, n, "/a/", []string{"a"}, 2) 19 | found(t, n, "/a/b", []string{"a", "b"}, 1) 20 | found(t, n, "/a/b/", []string{"a", "b"}, 1) 21 | 22 | notfound(t, n, "/a/b/c") 23 | } 24 | 25 | func TestStar(t *testing.T) { 26 | n := New() 27 | 28 | n.Add("/first/second/*star", 1) 29 | n.Add("/:first/*star/", 2) 30 | n.Add("/*star", 3) 31 | n.Add("/", 4) 32 | 33 | found(t, n, "/", nil, 4) 34 | found(t, n, "/a", []string{"a"}, 3) 35 | found(t, n, "/a/", []string{"a"}, 3) 36 | found(t, n, "/a/b", []string{"a", "b"}, 2) 37 | found(t, n, "/a/b/", []string{"a", "b"}, 2) 38 | found(t, n, "/a/b/c", []string{"a", "b/c"}, 2) 39 | found(t, n, "/a/b/c/", []string{"a", "b/c"}, 2) 40 | found(t, n, "/a/b/c/d", []string{"a", "b/c/d"}, 2) 41 | found(t, n, "/first/second", []string{"first", "second"}, 2) 42 | found(t, n, "/first/second/", []string{"first", "second"}, 2) 43 | found(t, n, "/first/second/third", []string{"third"}, 1) 44 | } 45 | 46 | func TestMixedTree(t *testing.T) { 47 | n := New() 48 | 49 | n.Add("/", 0) 50 | n.Add("/path/to/nowhere", 1) 51 | n.Add("/path/:i/nowhere", 2) 52 | n.Add("/:id/to/nowhere", 3) 53 | n.Add("/:a/:b", 4) 54 | n.Add("/not/found", 5) 55 | 56 | found(t, n, "/", nil, 0) 57 | found(t, n, "/path/to/nowhere", nil, 1) 58 | found(t, n, "/path/to/nowhere/", nil, 1) 59 | found(t, n, "/path/from/nowhere", []string{"from"}, 2) 60 | found(t, n, "/walk/to/nowhere", []string{"walk"}, 3) 61 | found(t, n, "/path/to/", []string{"path", "to"}, 4) 62 | found(t, n, "/path/to", []string{"path", "to"}, 4) 63 | found(t, n, "/not/found", []string{"not", "found"}, 4) 64 | notfound(t, n, "/path/to/somewhere") 65 | notfound(t, n, "/path/to/nowhere/else") 66 | notfound(t, n, "/path") 67 | notfound(t, n, "/path/") 68 | 69 | notfound(t, n, "") 70 | notfound(t, n, "xyz") 71 | notfound(t, n, "/path//to/nowhere") 72 | } 73 | 74 | func TestExtensions(t *testing.T) { 75 | n := New() 76 | 77 | n.Add("/:first/:second.json", 1) 78 | n.Add("/a/:second.xml", 2) 79 | n.Add("/:first/:second", 3) 80 | 81 | found(t, n, "/a/b", []string{"a", "b"}, 3) 82 | found(t, n, "/a/b.json", []string{"a", "b"}, 1) 83 | found(t, n, "/a/b.xml", []string{"b"}, 2) 84 | found(t, n, "/a/b.c.xml", []string{"b.c"}, 2) 85 | found(t, n, "/other/b.xml", []string{"other", "b.xml"}, 3) 86 | } 87 | 88 | func TestErrors(t *testing.T) { 89 | n := New() 90 | fails(t, n.Add("//", 1), "empty path elements not allowed") 91 | } 92 | 93 | func BenchmarkTree100(b *testing.B) { 94 | n := New() 95 | n.Add("/", "root") 96 | 97 | // Exact matches 98 | for i := 0; i < 100; i++ { 99 | depth := i%5 + 1 100 | key := "" 101 | for j := 0; j < depth-1; j++ { 102 | key += fmt.Sprintf("/dir%d", j) 103 | } 104 | key += fmt.Sprintf("/resource%d", i) 105 | n.Add(key, "literal") 106 | // b.Logf("Adding %s", key) 107 | } 108 | 109 | // Wildcards at each level if no exact matches work. 110 | for i := 0; i < 5; i++ { 111 | var key string 112 | for j := 0; j < i; j++ { 113 | key += fmt.Sprintf("/dir%d", j) 114 | } 115 | key += "/:var" 116 | n.Add(key, "var") 117 | // b.Logf("Adding %s", key) 118 | } 119 | 120 | n.Add("/public/*filepath", "static") 121 | // b.Logf("Adding /public/*filepath") 122 | 123 | queries := map[string]string{ 124 | "/": "root", 125 | "/dir0/dir1/dir2/dir3/resource4": "literal", 126 | "/dir0/dir1/resource97": "literal", 127 | "/dir0/variable": "var", 128 | "/dir0/dir1/dir2/dir3/variable": "var", 129 | "/public/stylesheets/main.css": "static", 130 | "/public/images/icons/an-image.png": "static", 131 | } 132 | 133 | for query, answer := range queries { 134 | leaf, _ := n.Find(query) 135 | if leaf == nil { 136 | b.Errorf("Failed to find leaf for querY %s", query) 137 | return 138 | } 139 | if leaf.Value.(string) != answer { 140 | b.Errorf("Incorrect answer for querY %s: expected: %s, actual: %s", 141 | query, answer, leaf.Value.(string)) 142 | return 143 | } 144 | } 145 | 146 | b.ResetTimer() 147 | 148 | for i := 0; i < b.N/len(queries); i++ { 149 | for k, _ := range queries { 150 | n.Find(k) 151 | } 152 | } 153 | } 154 | 155 | func notfound(t *testing.T, n *Node, p string) { 156 | if leaf, _ := n.Find(p); leaf != nil { 157 | t.Errorf("Should not have found: %s", p) 158 | } 159 | } 160 | 161 | func found(t *testing.T, n *Node, p string, expectedExpansions []string, val interface{}) { 162 | leaf, expansions := n.Find(p) 163 | if leaf == nil { 164 | t.Errorf("Didn't find: %s", p) 165 | return 166 | } 167 | if !reflect.DeepEqual(expansions, expectedExpansions) { 168 | t.Errorf("%s: Wildcard expansions (actual) %v != %v (expected)", p, expansions, expectedExpansions) 169 | } 170 | if leaf.Value != val { 171 | t.Errorf("%s: Value (actual) %v != %v (expected)", p, leaf.Value, val) 172 | } 173 | } 174 | 175 | func fails(t *testing.T, err error, msg string) { 176 | if err == nil { 177 | t.Errorf("expected an error. %s", msg) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | // pathtree implements a tree for fast path lookup. 2 | // 3 | // Restrictions 4 | // 5 | // - Paths must be a '/'-separated list of strings, like a URL or Unix filesystem. 6 | // - All paths must begin with a '/'. 7 | // - Path elements may not contain a '/'. 8 | // - Path elements beginning with a ':' or '*' will be interpreted as wildcards. 9 | // - Trailing slashes are inconsequential. 10 | // 11 | // Wildcards 12 | // 13 | // Wildcards are named path elements that may match any strings in that 14 | // location. Two different kinds of wildcards are permitted: 15 | // - :var - names beginning with ':' will match any single path element. 16 | // - *var - names beginning with '*' will match one or more path elements. 17 | // (however, no path elements may come after a star wildcard) 18 | // 19 | // Extensions 20 | // 21 | // Single element wildcards in the last path element can optionally end with an 22 | // extension. This allows for routes like '/users/:id.json', which will not 23 | // conflict with '/users/:id'. 24 | // 25 | // Algorithm 26 | // 27 | // Paths are mapped to the tree in the following way: 28 | // - Each '/' is a Node in the tree. The root node is the leading '/'. 29 | // - Each Node has edges to other nodes. The edges are named according to the 30 | // possible path elements at that depth in the path. 31 | // - Any Node may have an associated Leaf. Leafs are terminals containing the 32 | // data associated with the path as traversed from the root to that Node. 33 | // 34 | // Edges are implemented as a map from the path element name to the next node in 35 | // the path. 36 | package pathtree 37 | 38 | import ( 39 | "errors" 40 | "strings" 41 | ) 42 | 43 | type Node struct { 44 | edges map[string]*Node // the various path elements leading out of this node. 45 | wildcard *Node // if set, this node had a wildcard as its path element. 46 | leaf *Leaf // if set, this is a terminal node for this leaf. 47 | extensions map[string]*Leaf // if set, this is a terminal node with a leaf that ends in a specific extension. 48 | star *Leaf // if set, this path ends in a star. 49 | leafs int // counter for # leafs in the tree 50 | } 51 | 52 | type Leaf struct { 53 | Value interface{} // the value associated with this node 54 | Wildcards []string // the wildcard names, in order they appear in the path 55 | order int // the order this leaf was added 56 | } 57 | 58 | // New returns a new path tree. 59 | func New() *Node { 60 | return &Node{edges: make(map[string]*Node)} 61 | } 62 | 63 | // Add a path and its associated value to the tree. 64 | // - key must begin with "/" 65 | // - key must not duplicate any existing key. 66 | // Returns an error if those conditions do not hold. 67 | func (n *Node) Add(key string, val interface{}) error { 68 | if key == "" || key[0] != '/' { 69 | return errors.New("Path must begin with /") 70 | } 71 | n.leafs++ 72 | return n.add(n.leafs, splitPath(key), nil, val) 73 | } 74 | 75 | // Adds a leaf to a terminal node. 76 | // If the last wildcard contains an extension, add it to the 'extensions' map. 77 | func (n *Node) addLeaf(leaf *Leaf) error { 78 | extension := stripExtensionFromLastSegment(leaf.Wildcards) 79 | if extension != "" { 80 | if n.extensions == nil { 81 | n.extensions = make(map[string]*Leaf) 82 | } 83 | if n.extensions[extension] != nil { 84 | return errors.New("duplicate path") 85 | } 86 | n.extensions[extension] = leaf 87 | return nil 88 | } 89 | 90 | if n.leaf != nil { 91 | return errors.New("duplicate path") 92 | } 93 | n.leaf = leaf 94 | return nil 95 | } 96 | 97 | func (n *Node) add(order int, elements, wildcards []string, val interface{}) error { 98 | if len(elements) == 0 { 99 | leaf := &Leaf{ 100 | order: order, 101 | Value: val, 102 | Wildcards: wildcards, 103 | } 104 | return n.addLeaf(leaf) 105 | } 106 | 107 | var el string 108 | el, elements = elements[0], elements[1:] 109 | if el == "" { 110 | return errors.New("empty path elements are not allowed") 111 | } 112 | 113 | // Handle wildcards. 114 | switch el[0] { 115 | case ':': 116 | if n.wildcard == nil { 117 | n.wildcard = New() 118 | } 119 | return n.wildcard.add(order, elements, append(wildcards, el[1:]), val) 120 | case '*': 121 | if n.star != nil { 122 | return errors.New("duplicate path") 123 | } 124 | n.star = &Leaf{ 125 | order: order, 126 | Value: val, 127 | Wildcards: append(wildcards, el[1:]), 128 | } 129 | return nil 130 | } 131 | 132 | // It's a normal path element. 133 | e, ok := n.edges[el] 134 | if !ok { 135 | e = New() 136 | n.edges[el] = e 137 | } 138 | 139 | return e.add(order, elements, wildcards, val) 140 | } 141 | 142 | // Find a given path. Any wildcards traversed along the way are expanded and 143 | // returned, along with the value. 144 | func (n *Node) Find(key string) (leaf *Leaf, expansions []string) { 145 | if len(key) == 0 || key[0] != '/' { 146 | return nil, nil 147 | } 148 | 149 | return n.find(splitPath(key), nil) 150 | } 151 | 152 | func (n *Node) find(elements, exp []string) (leaf *Leaf, expansions []string) { 153 | if len(elements) == 0 { 154 | // If this node has explicit extensions, check if the path matches one. 155 | if len(exp) > 0 && n.extensions != nil { 156 | lastExp := exp[len(exp)-1] 157 | prefix, extension := extensionForPath(lastExp) 158 | if leaf := n.extensions[extension]; leaf != nil { 159 | exp[len(exp)-1] = prefix 160 | return leaf, exp 161 | } 162 | } 163 | return n.leaf, exp 164 | } 165 | 166 | // If this node has a star, calculate the star expansions in advance. 167 | var starExpansion string 168 | if n.star != nil { 169 | starExpansion = strings.Join(elements, "/") 170 | } 171 | 172 | // Peel off the next element and look up the associated edge. 173 | var el string 174 | el, elements = elements[0], elements[1:] 175 | if nextNode, ok := n.edges[el]; ok { 176 | leaf, expansions = nextNode.find(elements, exp) 177 | } 178 | 179 | // Handle colon 180 | if n.wildcard != nil { 181 | wildcardLeaf, wildcardExpansions := n.wildcard.find(elements, append(exp, el)) 182 | if wildcardLeaf != nil && (leaf == nil || leaf.order > wildcardLeaf.order) { 183 | leaf = wildcardLeaf 184 | expansions = wildcardExpansions 185 | } 186 | } 187 | 188 | // Handle star 189 | if n.star != nil && (leaf == nil || leaf.order > n.star.order) { 190 | leaf = n.star 191 | expansions = append(exp, starExpansion) 192 | } 193 | 194 | return 195 | } 196 | 197 | func extensionForPath(path string) (string, string) { 198 | dotPosition := strings.LastIndex(path, ".") 199 | if dotPosition != -1 { 200 | return path[:dotPosition], path[dotPosition:] 201 | } 202 | return "", "" 203 | } 204 | 205 | func splitPath(key string) []string { 206 | elements := strings.Split(key, "/") 207 | if elements[0] == "" { 208 | elements = elements[1:] 209 | } 210 | if elements[len(elements)-1] == "" { 211 | elements = elements[:len(elements)-1] 212 | } 213 | return elements 214 | } 215 | 216 | // stripExtensionFromLastSegment determines if a string slice representing a path 217 | // ends with a file extension, removes the extension from the input, and returns it. 218 | func stripExtensionFromLastSegment(segments []string) string { 219 | if len(segments) == 0 { 220 | return "" 221 | } 222 | lastSegment := segments[len(segments)-1] 223 | prefix, extension := extensionForPath(lastSegment) 224 | if extension != "" { 225 | segments[len(segments)-1] = prefix 226 | } 227 | return extension 228 | } 229 | --------------------------------------------------------------------------------