diff --git a/handler.go b/handler.go index 26430ca..8b7738d 100644 --- a/handler.go +++ b/handler.go @@ -185,6 +185,8 @@ func (pset pathConfigSet) Swap(i, j int) { } func (pset pathConfigSet) find(path string) (pc *pathConfig, subpath string) { + // Fast path with binary search to retrieve exact matches + // e.g. given pset ["/", "/abc", "/xyz"], path "/def" won't match. i := sort.Search(len(pset), func(i int) bool { return pset[i].path >= path }) @@ -194,5 +196,30 @@ func (pset pathConfigSet) find(path string) (pc *pathConfig, subpath string) { if i > 0 && strings.HasPrefix(path, pset[i-1].path+"/") { return &pset[i-1], path[len(pset[i-1].path)+1:] } - return nil, "" + + // Slow path, now looking for the longest prefix/shortest subpath i.e. + // e.g. given pset ["/", "/abc/", "/abc/def/", "/xyz"/] + // * query "/abc/foo" returns "/abc/" with a subpath of "foo" + // * query "/x" returns "/" with a subpath of "x" + lenShortestSubpath := len(path) + var bestMatchConfig *pathConfig + + // After binary search with the >= lexicographic comparison, + // nothing greater than i will be a prefix of path. + max := i + for i := 0; i < max; i++ { + ps := pset[i] + if len(ps.path) >= len(path) { + // We previously didn't find the path by search, so any + // route with equal or greater length is NOT a match. + continue + } + sSubpath := strings.TrimPrefix(path, ps.path) + if len(sSubpath) < lenShortestSubpath { + subpath = sSubpath + lenShortestSubpath = len(sSubpath) + bestMatchConfig = &pset[i] + } + } + return bestMatchConfig, subpath } diff --git a/handler_test.go b/handler_test.go index 9c41bf3..b3e9a69 100644 --- a/handler_test.go +++ b/handler_test.go @@ -207,6 +207,46 @@ func TestPathConfigSetFind(t *testing.T) { want: "/portmidi", subpath: "foo", }, + { + paths: []string{"/example/helloworld", "/", "/y", "/foo"}, + query: "/x", + want: "/", + subpath: "x", + }, + { + paths: []string{"/example/helloworld", "/", "/y", "/foo"}, + query: "/", + want: "/", + subpath: "", + }, + { + paths: []string{"/example/helloworld", "/", "/y", "/foo"}, + query: "/example", + want: "/", + subpath: "example", + }, + { + paths: []string{"/example/helloworld", "/", "/y", "/foo"}, + query: "/example/foo", + want: "/", + subpath: "example/foo", + }, + { + paths: []string{"/example/helloworld", "/", "/y", "/foo"}, + query: "/y", + want: "/y", + }, + { + paths: []string{"/example/helloworld", "/", "/y", "/foo"}, + query: "/x/y/", + want: "/", + subpath: "x/y/", + }, + { + paths: []string{"/example/helloworld", "/y", "/foo"}, + query: "/x", + want: "", + }, } emptyToNil := func(s string) string { if s == "" {