├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── go.mod ├── urlpath.go └── urlpath_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | build: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v2 7 | - uses: actions/setup-go@v1 8 | with: 9 | go-version: "1.12" 10 | - run: go vet ./... 11 | - run: go test ./... 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ulysse Carion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # urlpath [![GoDoc Badge][badge]][godoc] [![CI Badge][ci-badge]][ci-url] 2 | 3 | `urlpath` is a Golang library for matching paths against a template, or 4 | constructing paths using a template. It's meant for applications that take in 5 | REST-like URL paths, and need to validate and extract data from those paths. 6 | 7 | [badge]: https://godoc.org/github.com/ucarion/urlpath?status.svg 8 | [godoc]: https://godoc.org/github.com/ucarion/urlpath 9 | [ci-badge]: https://github.com/ucarion/urlpath/workflows/.github/workflows/test.yml/badge.svg 10 | [ci-url]: https://github.com/ucarion/urlpath/actions 11 | 12 | This is easiest explained with an example: 13 | 14 | ```go 15 | import "github.com/ucarion/urlpath" 16 | 17 | var getBookPath = urlpath.New("/shelves/:shelf/books/:book") 18 | 19 | func main() { 20 | inputPath := "/shelves/foo/books/bar" 21 | match, ok := getBookPath.Match(inputPath) 22 | if !ok { 23 | // handle the input not being valid 24 | return 25 | } 26 | 27 | // Output: 28 | // 29 | // foo 30 | // bar 31 | fmt.Println(match.Params["shelf"]) 32 | fmt.Println(match.Params["book"]) 33 | } 34 | ``` 35 | 36 | One slightly fancier feature is support for trailing segments, like if you have 37 | a path that ends with a filename. For example, a GitHub-like API might need to 38 | deal with paths like: 39 | 40 | ```text 41 | /ucarion/urlpath/blob/master/src/foo/bar/baz.go 42 | ``` 43 | 44 | You can do this with a path that ends with "*". This works like: 45 | 46 | ```go 47 | path := urlpath.New("/:user/:repo/blob/:branch/*") 48 | 49 | match, ok := path.Match("/ucarion/urlpath/blob/master/src/foo/bar/baz.go") 50 | fmt.Println(match.Params["user"]) // ucarion 51 | fmt.Println(match.Params["repo"]) // urlpath 52 | fmt.Println(match.Params["branch"]) // master 53 | fmt.Println(match.Trailing) // src/foo/bar/baz.go 54 | ``` 55 | 56 | Additionally, you can call `Build` to construct a path from a template: 57 | 58 | ```go 59 | path := urlpath.New("/:user/:repo/blob/:branch/*") 60 | 61 | res, ok := path.Build(urlpath.Match{ 62 | Params: map[string]string{ 63 | "user": "ucarion", 64 | "repo": "urlpath", 65 | "branch": "master", 66 | }, 67 | Trailing: "src/foo/bar/baz.go", 68 | }) 69 | 70 | fmt.Println(res) // /ucarion/urlpath/blob/master/src/foo/bar/baz.go 71 | ``` 72 | 73 | ## How it works 74 | 75 | `urlpath` operates on the basis of "segments", which is basically the result of 76 | splitting a path by slashes. When you call `urlpath.New`, each of the segments 77 | in the input is treated as either: 78 | 79 | * A parameterized segment, like `:user`. All segments starting with `:` are 80 | considered parameterized. Any corresponding segment in the input (even the 81 | empty string!) will be satisfactory, and will be sent to `Params` in the 82 | outputted `Match`. For example, data corresponding to `:user` would go in 83 | `Params["user"]`. 84 | * An exact-match segment, like `users`. Only segments exactly equal to `users` 85 | will be satisfactory. 86 | * A "trailing" segment, `*`. This is only treated specially when it's the last 87 | segment -- otherwise, it's just a usual exact-match segment. Any leftover data 88 | in the input, after all previous segments were satisfied, goes into `Trailing` 89 | in the outputted `Match`. 90 | 91 | ## Performance 92 | 93 | Although performance wasn't the top priority for this library, `urlpath` does 94 | typically perform better than an equivalent regular expression. In other words, 95 | this: 96 | 97 | ```go 98 | path := urlpath.New("/test/:foo/bar/:baz") 99 | matches := path.Match(...) 100 | ``` 101 | 102 | Will usually perform better than this: 103 | 104 | ```go 105 | r := regexp.MustCompile("/test/(?P[^/]+)/bar/(?P[^/]+)") 106 | matches := r.FindStringSubmatch(...) 107 | ``` 108 | 109 | The results of `go test -benchmem -bench .`: 110 | 111 | ```text 112 | goos: darwin 113 | goarch: amd64 114 | pkg: github.com/ucarion/urlpath 115 | BenchmarkMatch/without_trailing_segments/urlpath-8 1436247 819 ns/op 784 B/op 10 allocs/op 116 | BenchmarkMatch/without_trailing_segments/regex-8 693924 1816 ns/op 338 B/op 10 allocs/op 117 | BenchmarkMatch/with_trailing_segments/urlpath-8 1454750 818 ns/op 784 B/op 10 allocs/op 118 | BenchmarkMatch/with_trailing_segments/regex-8 592644 2365 ns/op 225 B/op 8 allocs/op 119 | ``` 120 | 121 | Do your own benchmarking if performance matters a lot to you. See 122 | `BenchmarkMatch` in `urlpath_test.go` for the code that gives these results. 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ucarion/urlpath 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /urlpath.go: -------------------------------------------------------------------------------- 1 | // Package urlpath matches paths against a template. It's meant for applications 2 | // that take in REST-like URL paths, and need to validate and extract data from 3 | // those paths. 4 | // 5 | // See New for documentation of the syntax for creating paths. See Match for how 6 | // to validate and parse an inputted path. 7 | package urlpath 8 | 9 | import "strings" 10 | 11 | // Path is a representation of a sequence of segments. 12 | // 13 | // To construct instances of Path, see New. 14 | type Path struct { 15 | // A sequence of constraints on what valid segments must look like. 16 | Segments []Segment 17 | 18 | // Whether additional, trailing segments after Segments are acceptable. 19 | Trailing bool 20 | } 21 | 22 | // Segment is a constraint on a single segment in a path. 23 | type Segment struct { 24 | // Whether this segment is parameterized. 25 | IsParam bool 26 | 27 | // The name of the parameter this segment will be mapped to. 28 | Param string 29 | 30 | // The constant value the segment is expected to take. 31 | Const string 32 | } 33 | 34 | // Match represents the data extracted by matching an input against a Path. 35 | // 36 | // To construct instances of Match, see the Match method on Path. 37 | type Match struct { 38 | // The segments in the input corresponding to parameterized segments in Path. 39 | Params map[string]string 40 | 41 | // The trailing segments from the input. Note that the leading slash from the 42 | // trailing segments is not included, since it's implied. 43 | // 44 | // An exception to this leading slash rule is made if the Path was constructed 45 | // as New("*"), in which case Trailing will be identical to the inputted 46 | // string. 47 | Trailing string 48 | } 49 | 50 | // New constructs a new Path from its human-readable string representation. 51 | // 52 | // The syntax for paths looks something like the following: 53 | // 54 | // /shelves/:shelf/books/:book 55 | // 56 | // This would match inputs like: 57 | // 58 | // /shelves/foo/books/bar 59 | // /shelves/123/books/456 60 | // /shelves/123/books/ 61 | // /shelves//books/456 62 | // /shelves//books/ 63 | // 64 | // But not any of the following: 65 | // 66 | // /shelves/foo/books 67 | // /shelves/foo/books/bar/ 68 | // /shelves/foo/books/bar/pages/baz 69 | // /SHELVES/foo/books/bar 70 | // shelves/foo/books/bar 71 | // 72 | // Optionally, a path can allow for "trailing" segments in the input. This is 73 | // done using a segment simply named "*". For example, this path: 74 | // 75 | // /users/:user/files/* 76 | // 77 | // Would match inputs like: 78 | // 79 | // /users/foo/files/ 80 | // /users/foo/files/foo/bar/baz.txt 81 | // /users/foo/files//// 82 | // 83 | // But not: 84 | // 85 | // /users/foo 86 | // /users/foo/files 87 | // 88 | // The asterisk syntax for trailing segments only takes effect on the last 89 | // segment. If an asterisk appears in any other segment, it carries no special 90 | // meaning. 91 | // 92 | // In more formal terms, the string representation of a path is a sequence of 93 | // segments separated by slashes. Segments starting with colon (":") are treated 94 | // as "parameter" segments (see Match). 95 | // 96 | // If the final segment is just the character asterisk ("*"), it is treated as 97 | // an indication that the path accepts trailing segments, and not included in 98 | // the Segments of the return value. Instead, Trailing in the return value is 99 | // marked as true. 100 | func New(path string) Path { 101 | inSegments := strings.Split(path, "/") 102 | trailing := inSegments[len(inSegments)-1] == "*" 103 | 104 | var outSegments []Segment 105 | if trailing { 106 | outSegments = make([]Segment, len(inSegments)-1) 107 | } else { 108 | outSegments = make([]Segment, len(inSegments)) 109 | } 110 | 111 | for i := 0; i < len(outSegments); i++ { 112 | if strings.HasPrefix(inSegments[i], ":") { 113 | outSegments[i] = Segment{IsParam: true, Param: inSegments[i][1:]} 114 | } else { 115 | outSegments[i] = Segment{IsParam: false, Const: inSegments[i]} 116 | } 117 | } 118 | 119 | return Path{Segments: outSegments, Trailing: trailing} 120 | } 121 | 122 | // Match checks if the input string satisfies a Path's constraints, and returns 123 | // parameter and trailing segment information. 124 | // 125 | // The second return value indicates whether the inputted string matched the 126 | // path. The first return value is meaningful only if the match was successful. 127 | // 128 | // If the match was a success, all parameterized segments in Path have a 129 | // corresponding entry in the Params of Match. If the path allows for trailing 130 | // segments in the input, these will be in Trailing. 131 | func (p *Path) Match(s string) (Match, bool) { 132 | params := map[string]string{} 133 | 134 | for segmentIndex, segment := range p.Segments { 135 | // s[:i] is the prefix of s which contains the segment that must match 136 | // against the path. s[j:] is the suffix of s which the next iteration of 137 | // the loop will operate on. 138 | // 139 | // In "ordinary" circumstances, s[:i] is everything up to the first slash, 140 | // and s[j:] is everything after it. But when there are no remaining slashes 141 | // in the input, s[:i] is the entire string, and s[j:] is the empty string. 142 | i := strings.IndexByte(s, '/') 143 | j := i + 1 144 | if i == -1 { 145 | i = len(s) 146 | j = len(s) 147 | 148 | // If we have run out of slashes before the last element of the segments, 149 | // then the input does not match the path. 150 | // 151 | // Implicitly, allowing for trailing input effectively adds an additional 152 | // required slash to the input that's not captured by p.Segments. If 153 | // trailing input is allowed, it's never ok for an input to have fewer 154 | // slashes than the path has segments (an equal number is ok, and 155 | // corresponds to a trailing part with no slashes in it). 156 | if segmentIndex != len(p.Segments)-1 || p.Trailing { 157 | return Match{}, false 158 | } 159 | } else { 160 | // If we have slashes left over and we are not ok with trailing input, 161 | // then the input does not match the path. 162 | if segmentIndex == len(p.Segments)-1 && !p.Trailing { 163 | return Match{}, false 164 | } 165 | } 166 | 167 | if segment.IsParam { 168 | params[segment.Param] = s[:i] 169 | } else { 170 | if s[:i] != segment.Const { 171 | return Match{}, false 172 | } 173 | } 174 | 175 | s = s[j:] 176 | } 177 | 178 | return Match{Params: params, Trailing: s}, true 179 | } 180 | 181 | // Build is the inverse of Match. Given parameter and trailing segment 182 | // information, Build returns a string which satifies this information. 183 | // 184 | // The second parameter indicates whether the inputted match has the parameters 185 | // the path specifies. If any of the parameters in the path are not found in the 186 | // provided Match's Params, then false is returned. 187 | func (p *Path) Build(m Match) (string, bool) { 188 | var s strings.Builder 189 | for i, segment := range p.Segments { 190 | if segment.IsParam { 191 | if param, ok := m.Params[segment.Param]; ok { 192 | s.WriteString(param) 193 | } else { 194 | return "", false 195 | } 196 | } else { 197 | s.WriteString(segment.Const) 198 | } 199 | 200 | if i != len(p.Segments)-1 { 201 | s.WriteRune('/') 202 | } 203 | } 204 | 205 | // The trailing segment of a match does not include a leading slash. We 206 | // therefore need to add it here. 207 | // 208 | // However, if there are no segments at all in the path, then in this special 209 | // case the match's Trailing is simply the originally inputted string itself, 210 | // and so no leading slash must be inserted. 211 | if p.Trailing && len(p.Segments) > 0 { 212 | s.WriteRune('/') 213 | } 214 | 215 | s.WriteString(m.Trailing) 216 | 217 | return s.String(), true 218 | } 219 | -------------------------------------------------------------------------------- /urlpath_test.go: -------------------------------------------------------------------------------- 1 | package urlpath_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/ucarion/urlpath" 10 | ) 11 | 12 | func ExampleNew() { 13 | path := urlpath.New("users/:id/files/*") 14 | fmt.Println(path.Segments[0].Const) 15 | fmt.Println(path.Segments[1].Param) 16 | fmt.Println(path.Segments[2].Const) 17 | fmt.Println(path.Trailing) 18 | // Output: 19 | // 20 | // users 21 | // id 22 | // files 23 | // true 24 | } 25 | 26 | func ExampleMatch() { 27 | path := urlpath.New("users/:id/files/*") 28 | match, ok := path.Match("users/123/files/foo/bar/baz.txt") 29 | fmt.Println(ok) 30 | fmt.Println(match.Params) 31 | fmt.Println(match.Trailing) 32 | // Output: 33 | // 34 | // true 35 | // map[id:123] 36 | // foo/bar/baz.txt 37 | } 38 | 39 | func ExamplePath_Build() { 40 | path := urlpath.New("users/:id/files/*") 41 | built, ok := path.Build(urlpath.Match{ 42 | Params: map[string]string{"id": "123"}, 43 | Trailing: "foo/bar/baz.txt", 44 | }) 45 | fmt.Println(ok) 46 | fmt.Println(built) 47 | // Output: 48 | // 49 | // true 50 | // users/123/files/foo/bar/baz.txt 51 | } 52 | 53 | func TestNew(t *testing.T) { 54 | testCases := []struct { 55 | in string 56 | out urlpath.Path 57 | }{ 58 | { 59 | "foo", 60 | urlpath.Path{Segments: []urlpath.Segment{ 61 | urlpath.Segment{Const: "foo"}, 62 | }}, 63 | }, 64 | 65 | { 66 | "/foo", 67 | urlpath.Path{Segments: []urlpath.Segment{ 68 | urlpath.Segment{Const: ""}, 69 | urlpath.Segment{Const: "foo"}, 70 | }}, 71 | }, 72 | 73 | { 74 | ":foo", 75 | urlpath.Path{Segments: []urlpath.Segment{ 76 | urlpath.Segment{IsParam: true, Param: "foo"}, 77 | }}, 78 | }, 79 | 80 | { 81 | "/:foo", 82 | urlpath.Path{Segments: []urlpath.Segment{ 83 | urlpath.Segment{Const: ""}, 84 | urlpath.Segment{IsParam: true, Param: "foo"}, 85 | }}, 86 | }, 87 | 88 | { 89 | "foo/:bar", 90 | urlpath.Path{Segments: []urlpath.Segment{ 91 | urlpath.Segment{Const: "foo"}, 92 | urlpath.Segment{IsParam: true, Param: "bar"}, 93 | }}, 94 | }, 95 | 96 | { 97 | "foo/:foo/bar/:bar", 98 | urlpath.Path{Segments: []urlpath.Segment{ 99 | urlpath.Segment{Const: "foo"}, 100 | urlpath.Segment{IsParam: true, Param: "foo"}, 101 | urlpath.Segment{Const: "bar"}, 102 | urlpath.Segment{IsParam: true, Param: "bar"}, 103 | }}, 104 | }, 105 | 106 | { 107 | "foo/:bar/:baz/*", 108 | urlpath.Path{Trailing: true, Segments: []urlpath.Segment{ 109 | urlpath.Segment{Const: "foo"}, 110 | urlpath.Segment{IsParam: true, Param: "bar"}, 111 | urlpath.Segment{IsParam: true, Param: "baz"}, 112 | }}, 113 | }, 114 | 115 | { 116 | "/:/*", 117 | urlpath.Path{Trailing: true, Segments: []urlpath.Segment{ 118 | urlpath.Segment{Const: ""}, 119 | urlpath.Segment{IsParam: true, Param: ""}, 120 | }}, 121 | }, 122 | } 123 | 124 | for _, tt := range testCases { 125 | t.Run(tt.in, func(t *testing.T) { 126 | out := urlpath.New(tt.in) 127 | 128 | if !reflect.DeepEqual(out, tt.out) { 129 | t.Errorf("out %#v, want %#v", out, tt.out) 130 | } 131 | }) 132 | } 133 | } 134 | 135 | func TestMatch(t *testing.T) { 136 | testCases := []struct { 137 | Path string 138 | in string 139 | out urlpath.Match 140 | ok bool 141 | }{ 142 | { 143 | "foo", 144 | "foo", 145 | urlpath.Match{Params: map[string]string{}, Trailing: ""}, 146 | true, 147 | }, 148 | 149 | { 150 | "foo", 151 | "bar", 152 | urlpath.Match{}, 153 | false, 154 | }, 155 | 156 | { 157 | ":foo", 158 | "bar", 159 | urlpath.Match{Params: map[string]string{"foo": "bar"}, Trailing: ""}, 160 | true, 161 | }, 162 | 163 | { 164 | "/:foo", 165 | "/bar", 166 | urlpath.Match{Params: map[string]string{"foo": "bar"}, Trailing: ""}, 167 | true, 168 | }, 169 | 170 | { 171 | "/:foo/bar/:baz", 172 | "/foo/bar/baz", 173 | urlpath.Match{Params: map[string]string{"foo": "foo", "baz": "baz"}, Trailing: ""}, 174 | true, 175 | }, 176 | 177 | { 178 | "/:foo/bar/:baz", 179 | "/foo/bax/baz", 180 | urlpath.Match{}, 181 | false, 182 | }, 183 | 184 | { 185 | "/:foo/:bar/:baz", 186 | "/foo/bar/baz", 187 | urlpath.Match{Params: map[string]string{"foo": "foo", "bar": "bar", "baz": "baz"}, Trailing: ""}, 188 | true, 189 | }, 190 | 191 | { 192 | "/:foo/:bar/:baz", 193 | "///", 194 | urlpath.Match{Params: map[string]string{"foo": "", "bar": "", "baz": ""}, Trailing: ""}, 195 | true, 196 | }, 197 | 198 | { 199 | "/:foo/:bar/:baz", 200 | "", 201 | urlpath.Match{}, 202 | false, 203 | }, 204 | 205 | { 206 | "/:foo/bar/:baz", 207 | "/foo/bax/baz/a/b/c", 208 | urlpath.Match{}, 209 | false, 210 | }, 211 | 212 | { 213 | "/:foo/bar/:baz", 214 | "/foo/bax/baz/", 215 | urlpath.Match{}, 216 | false, 217 | }, 218 | 219 | { 220 | "/:foo/bar/:baz/*", 221 | "/foo/bar/baz/a/b/c", 222 | urlpath.Match{Params: map[string]string{"foo": "foo", "baz": "baz"}, Trailing: "a/b/c"}, 223 | true, 224 | }, 225 | 226 | { 227 | "/:foo/bar/:baz/*", 228 | "/foo/bar/baz/", 229 | urlpath.Match{Params: map[string]string{"foo": "foo", "baz": "baz"}, Trailing: ""}, 230 | true, 231 | }, 232 | 233 | { 234 | "/:foo/bar/:baz/*", 235 | "/foo/bar/baz", 236 | urlpath.Match{}, 237 | false, 238 | }, 239 | 240 | { 241 | "/:foo/:bar/:baz/*", 242 | "////", 243 | urlpath.Match{Params: map[string]string{"foo": "", "bar": "", "baz": ""}, Trailing: ""}, 244 | true, 245 | }, 246 | 247 | { 248 | "/:foo/:bar/:baz/*", 249 | "/////", 250 | urlpath.Match{Params: map[string]string{"foo": "", "bar": "", "baz": ""}, Trailing: "/"}, 251 | true, 252 | }, 253 | 254 | { 255 | "*", 256 | "", 257 | urlpath.Match{Params: map[string]string{}, Trailing: ""}, 258 | true, 259 | }, 260 | 261 | { 262 | "/*", 263 | "", 264 | urlpath.Match{}, 265 | false, 266 | }, 267 | 268 | { 269 | "*", 270 | "/", 271 | urlpath.Match{Params: map[string]string{}, Trailing: "/"}, 272 | true, 273 | }, 274 | 275 | { 276 | "/*", 277 | "/", 278 | urlpath.Match{Params: map[string]string{}, Trailing: ""}, 279 | true, 280 | }, 281 | 282 | { 283 | "*", 284 | "/a/b/c", 285 | urlpath.Match{Params: map[string]string{}, Trailing: "/a/b/c"}, 286 | true, 287 | }, 288 | 289 | { 290 | "*", 291 | "a/b/c", 292 | urlpath.Match{Params: map[string]string{}, Trailing: "a/b/c"}, 293 | true, 294 | }, 295 | 296 | // Examples from documentation 297 | { 298 | "/shelves/:shelf/books/:book", 299 | "/shelves/foo/books/bar", 300 | urlpath.Match{Params: map[string]string{"shelf": "foo", "book": "bar"}}, 301 | true, 302 | }, 303 | { 304 | "/shelves/:shelf/books/:book", 305 | "/shelves/123/books/456", 306 | urlpath.Match{Params: map[string]string{"shelf": "123", "book": "456"}}, 307 | true, 308 | }, 309 | { 310 | "/shelves/:shelf/books/:book", 311 | "/shelves/123/books/", 312 | urlpath.Match{Params: map[string]string{"shelf": "123", "book": ""}}, 313 | true, 314 | }, 315 | { 316 | "/shelves/:shelf/books/:book", 317 | "/shelves//books/456", 318 | urlpath.Match{Params: map[string]string{"shelf": "", "book": "456"}}, 319 | true, 320 | }, 321 | { 322 | "/shelves/:shelf/books/:book", 323 | "/shelves//books/", 324 | urlpath.Match{Params: map[string]string{"shelf": "", "book": ""}}, 325 | true, 326 | }, 327 | { 328 | "/shelves/:shelf/books/:book", 329 | "/shelves/foo/books", 330 | urlpath.Match{}, 331 | false, 332 | }, 333 | { 334 | "/shelves/:shelf/books/:book", 335 | "/shelves/foo/books/bar/", 336 | urlpath.Match{}, 337 | false, 338 | }, 339 | { 340 | "/shelves/:shelf/books/:book", 341 | "/shelves/foo/books/pages/baz", 342 | urlpath.Match{}, 343 | false, 344 | }, 345 | { 346 | "/shelves/:shelf/books/:book", 347 | "/SHELVES/foo/books/bar", 348 | urlpath.Match{}, 349 | false, 350 | }, 351 | { 352 | "/shelves/:shelf/books/:book", 353 | "shelves/foo/books/bar", 354 | urlpath.Match{}, 355 | false, 356 | }, 357 | { 358 | "/users/:user/files/*", 359 | "/users/foo/files/", 360 | urlpath.Match{Params: map[string]string{"user": "foo"}, Trailing: ""}, 361 | true, 362 | }, 363 | { 364 | "/users/:user/files/*", 365 | "/users/foo/files/foo/bar/baz.txt", 366 | urlpath.Match{Params: map[string]string{"user": "foo"}, Trailing: "foo/bar/baz.txt"}, 367 | true, 368 | }, 369 | { 370 | "/users/:user/files/*", 371 | "/users/foo/files////", 372 | urlpath.Match{Params: map[string]string{"user": "foo"}, Trailing: "///"}, 373 | true, 374 | }, 375 | { 376 | "/users/:user/files/*", 377 | "/users/foo", 378 | urlpath.Match{}, 379 | false, 380 | }, 381 | { 382 | "/users/:user/files/*", 383 | "/users/foo/files", 384 | urlpath.Match{}, 385 | false, 386 | }, 387 | } 388 | 389 | for _, tt := range testCases { 390 | t.Run(fmt.Sprintf("%s/%s", tt.Path, tt.in), func(t *testing.T) { 391 | path := urlpath.New(tt.Path) 392 | out, ok := path.Match(tt.in) 393 | 394 | if !reflect.DeepEqual(out, tt.out) { 395 | t.Errorf("out %#v, want %#v", out, tt.out) 396 | } 397 | 398 | if ok != tt.ok { 399 | t.Errorf("ok %#v, want %#v", ok, tt.ok) 400 | } 401 | 402 | // If no error was expected when matching the data, then we should be able 403 | // to round-trip back to the original data using Build. 404 | if tt.ok { 405 | if in, ok := path.Build(out); ok { 406 | if in != tt.in { 407 | t.Errorf("in %#v, want %#v", in, tt.in) 408 | } 409 | } else { 410 | t.Error("Build returned ok = false") 411 | } 412 | } 413 | }) 414 | } 415 | } 416 | 417 | func BenchmarkMatch(b *testing.B) { 418 | b.Run("without trailing segments", func(b *testing.B) { 419 | b.Run("urlpath", func(b *testing.B) { 420 | path := urlpath.New("/test/:foo/bar/:baz") 421 | b.ResetTimer() 422 | 423 | for i := 0; i < b.N; i++ { 424 | path.Match(fmt.Sprintf("/test/foo%d/bar/baz%d", i, i)) 425 | path.Match(fmt.Sprintf("/test/foo%d/bar/baz%d/extra", i, i)) 426 | } 427 | }) 428 | 429 | b.Run("regex", func(b *testing.B) { 430 | regex := regexp.MustCompile("/test/(?P[^/]+)/bar/(?P[^/]+)") 431 | b.ResetTimer() 432 | 433 | for i := 0; i < b.N; i++ { 434 | regex.FindStringSubmatch(fmt.Sprintf("/test/foo%d/bar/baz%d", i, i)) 435 | regex.FindStringSubmatch(fmt.Sprintf("/test/foo%d/bar/baz%d/extra", i, i)) 436 | } 437 | }) 438 | }) 439 | 440 | b.Run("with trailing segments", func(b *testing.B) { 441 | b.Run("urlpath", func(b *testing.B) { 442 | path := urlpath.New("/test/:foo/bar/:baz/*") 443 | b.ResetTimer() 444 | 445 | for i := 0; i < b.N; i++ { 446 | path.Match(fmt.Sprintf("/test/foo%d/bar/baz%d", i, i)) 447 | path.Match(fmt.Sprintf("/test/foo%d/bar/baz%d/extra", i, i)) 448 | } 449 | }) 450 | 451 | b.Run("regex", func(b *testing.B) { 452 | regex := regexp.MustCompile("/test/(?P[^/]+)/bar/(?P[^/]+)/.*") 453 | b.ResetTimer() 454 | 455 | for i := 0; i < b.N; i++ { 456 | regex.FindStringSubmatch(fmt.Sprintf("/test/foo%d/bar/baz%d", i, i)) 457 | regex.FindStringSubmatch(fmt.Sprintf("/test/foo%d/bar/baz%d/extra", i, i)) 458 | } 459 | }) 460 | }) 461 | } 462 | --------------------------------------------------------------------------------