From e674d679dfa4d666953c92604b9d9a55f4b2b1f2 Mon Sep 17 00:00:00 2001 From: wernerr Date: Sun, 14 Jun 2020 12:37:09 +0200 Subject: [PATCH] move path matching and registration in separate file "path.go" --- path.go | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++ path_test.go | 212 ++++++++++++++++++++++++++++++++++++++++++ utils.go | 244 ------------------------------------------------ utils_test.go | 200 ---------------------------------------- 4 files changed, 463 insertions(+), 444 deletions(-) create mode 100644 path.go create mode 100644 path_test.go diff --git a/path.go b/path.go new file mode 100644 index 00000000..a6b98c09 --- /dev/null +++ b/path.go @@ -0,0 +1,251 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "strings" + "sync/atomic" + + utils "github.com/gofiber/utils" +) + +// ⚠️ This path parser was based on urlpath by @ucarion (MIT License). +// 💖 Modified for the Fiber router by @renanbastos93 & @renewerner87 +// 🤖 ucarion/urlpath - renanbastos93/fastpath - renewerner87/fastpath + +// routeParser holds the path segments and param names +type routeParser struct { + segs []paramSeg + params []string +} + +// paramsSeg holds the segment metadata +type paramSeg struct { + Param string + Const string + IsParam bool + IsOptional bool + IsLast bool + EndChar byte +} + +// list of possible parameter and segment delimiter +// slash has a special role, unlike the other parameters it must not be interpreted as a parameter +var routeDelimiter = []byte{'/', '-', '.'} + +const wildcardParam string = "*" + +// parseRoute analyzes the route and divides it into segments for constant areas and parameters, +// this information is needed later when assigning the requests to the declared routes +func parseRoute(pattern string) (p routeParser) { + var out []paramSeg + var params []string + + part, delimiterPos := "", 0 + for len(pattern) > 0 && delimiterPos != -1 { + delimiterPos = findNextRouteSegmentEnd(pattern) + if delimiterPos != -1 { + part = pattern[:delimiterPos] + } else { + part = pattern + } + + partLen, lastSeg := len(part), len(out)-1 + if partLen == 0 { // skip empty parts + if len(pattern) > 0 { + // remove first char + pattern = pattern[1:] + } + continue + } + // is parameter ? + if part[0] == '*' || part[0] == ':' { + out = append(out, paramSeg{ + Param: utils.GetTrimmedParam(part), + IsParam: true, + IsOptional: part == wildcardParam || part[partLen-1] == '?', + }) + lastSeg = len(out) - 1 + params = append(params, out[lastSeg].Param) + // combine const segments + } else if lastSeg >= 0 && !out[lastSeg].IsParam { + out[lastSeg].Const += string(out[lastSeg].EndChar) + part + // create new const segment + } else { + out = append(out, paramSeg{ + Const: part, + }) + lastSeg = len(out) - 1 + } + + if delimiterPos != -1 && len(pattern) >= delimiterPos+1 { + out[lastSeg].EndChar = pattern[delimiterPos] + pattern = pattern[delimiterPos+1:] + } else { + // last default char + out[lastSeg].EndChar = '/' + } + } + if len(out) > 0 { + out[len(out)-1].IsLast = true + } + + p = routeParser{segs: out, params: params} + return +} + +// findNextRouteSegmentEnd searches in the route for the next end position for a segment +func findNextRouteSegmentEnd(search string) int { + nextPosition := -1 + for _, delimiter := range routeDelimiter { + if pos := strings.IndexByte(search, delimiter); pos != -1 && (pos < nextPosition || nextPosition == -1) { + nextPosition = pos + } + } + + return nextPosition +} + +// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions +func (p *routeParser) getMatch(s string, partialCheck bool) ([][2]int, bool) { + lenKeys := len(p.params) + paramsPositions := getAllocFreeParamsPos(lenKeys) + var i, j, paramsIterator, partLen, paramStart int + if len(s) > 0 { + s = s[1:] + paramStart++ + } + for index, segment := range p.segs { + partLen = len(s) + // check parameter + if segment.IsParam { + // determine parameter length + if segment.Param == wildcardParam { + if segment.IsLast { + i = partLen + } else { + i = findWildcardParamLen(s, p.segs, index) + } + } else { + i = strings.IndexByte(s, segment.EndChar) + } + if i == -1 { + i = partLen + } + + if !segment.IsOptional && i == 0 { + return nil, false + // special case for not slash end character + } else if i > 0 && partLen >= i && segment.EndChar != '/' && s[i-1] == '/' { + return nil, false + } + + paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i + paramsIterator++ + } else { + // check const segment + i = len(segment.Const) + if partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const || (partLen > i && s[i] != segment.EndChar) { + return nil, false + } + } + + // reduce founded part from the string + if partLen > 0 { + j = i + 1 + if segment.IsLast || partLen < j { + j = i + } + paramStart += j + + s = s[j:] + } + } + if len(s) != 0 && !partialCheck { + return nil, false + } + + return paramsPositions, true +} + +// paramsForPos get parameters for the given positions from the given path +func (p *routeParser) paramsForPos(path string, paramsPositions [][2]int) []string { + size := len(paramsPositions) + params := getAllocFreeParams(size) + for i, positions := range paramsPositions { + if positions[0] != positions[1] && len(path) >= positions[1] { + params[i] = path[positions[0]:positions[1]] + } else { + params[i] = "" + } + } + + return params +} + +// findWildcardParamLen for the expressjs wildcard behavior (right to left greedy) +// look at the other segments and take what is left for the wildcard from right to left +func findWildcardParamLen(s string, segments []paramSeg, currIndex int) int { + // "/api/*/:param" - "/api/joker/batman/robin/1" -> "joker/batman/robin", "1" + // "/api/*/:param" - "/api/joker/batman" -> "joker", "batman" + // "/api/*/:param" - "/api/joker-batman-robin/1" -> "joker-batman-robin", "1" + endChar := segments[currIndex].EndChar + neededEndChars := 0 + // count the needed chars for the other segments + for i := currIndex + 1; i < len(segments); i++ { + if segments[i].EndChar == endChar { + neededEndChars++ + } + } + // remove the part the other segments still need + for { + pos := strings.LastIndexByte(s, endChar) + if pos != -1 { + s = s[:pos] + } + neededEndChars-- + if neededEndChars <= 0 || pos == -1 { + break + } + } + + return len(s) +} + +// performance tricks +// creates predefined arrays that are used to match the request routes so that no allocations need to be made +var paramsDummy, paramsPosDummy = make([]string, 100000), make([][2]int, 100000) + +// positions parameter that moves further and further to the right and remains atomic over all simultaneous requests +// to assign a separate range to each request +var startParamList, startParamPosList uint32 = 0, 0 + +// getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use +func getAllocFreeParamsPos(allocLen int) [][2]int { + size := uint32(allocLen) + start := atomic.AddUint32(&startParamPosList, size) + if (start + 10) >= uint32(len(paramsPosDummy)) { + atomic.StoreUint32(&startParamPosList, 0) + return getAllocFreeParamsPos(allocLen) + } + start -= size + allocLen += int(start) + paramsPositions := paramsPosDummy[start:allocLen:allocLen] + return paramsPositions +} + +// getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use +func getAllocFreeParams(allocLen int) []string { + size := uint32(allocLen) + start := atomic.AddUint32(&startParamList, size) + if (start + 10) >= uint32(len(paramsPosDummy)) { + atomic.StoreUint32(&startParamList, 0) + return getAllocFreeParams(allocLen) + } + start -= size + allocLen += int(start) + params := paramsDummy[start:allocLen:allocLen] + return params +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 00000000..e764e825 --- /dev/null +++ b/path_test.go @@ -0,0 +1,212 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📝 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "fmt" + "testing" + + utils "github.com/gofiber/utils" +) + +// go test -race -run Test_Path_matchParams +func Test_Path_matchParams(t *testing.T) { + t.Parallel() + type testparams struct { + url string + params []string + match bool + partialCheck bool + } + testCase := func(r string, cases []testparams) { + parser := parseRoute(r) + for _, c := range cases { + paramsPos, match := parser.getMatch(c.url, c.partialCheck) + utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + if match && paramsPos != nil { + utils.AssertEqual(t, c.params, parser.paramsForPos(c.url, paramsPos), fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + } else { + utils.AssertEqual(t, true, nil == paramsPos, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + } + } + } + testCase("/api/v1/:param/*", []testparams{ + {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }) + testCase("/api/v1/:param?", []testparams{ + {url: "/api/v1", params: []string{""}, match: true}, + {url: "/api/v1/", params: []string{""}, match: true}, + {url: "/api/v1/optional", params: []string{"optional"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/xyz", params: nil, match: false}, + }) + testCase("/api/v1/*", []testparams{ + {url: "/api/v1", params: []string{""}, match: true}, + {url: "/api/v1/", params: []string{""}, match: true}, + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/entity/1/2", params: []string{"entity/1/2"}, match: true}, + {url: "/api/v1/Entity/1/2", params: []string{"Entity/1/2"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/abc", params: nil, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/entity/8728382", params: nil, match: false}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }) + testCase("/api/v1/:param-:param2", []testparams{ + {url: "/api/v1/entity-entity2", params: []string{"entity", "entity2"}, match: true}, + {url: "/api/v1/entity/8728382", params: nil, match: false}, + {url: "/api/v1/entity-8728382", params: []string{"entity", "8728382"}, match: true}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }) + testCase("/api/v1/:filename.:extension", []testparams{ + {url: "/api/v1/test.pdf", params: []string{"test", "pdf"}, match: true}, + {url: "/api/v1/test/pdf", params: nil, match: false}, + {url: "/api/v1/test-pdf", params: nil, match: false}, + {url: "/api/v1/test_pdf", params: nil, match: false}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }) + testCase("/api/v1/const", []testparams{ + {url: "/api/v1/const", params: []string{}, match: true}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + {url: "/api/v1/something", params: nil, match: false}, + }) + testCase("/api/v1/:param/abc/*", []testparams{ + {url: "/api/v1/well/abc/wildcard", params: []string{"well", "wildcard"}, match: true}, + {url: "/api/v1/well/abc/", params: []string{"well", ""}, match: true}, + {url: "/api/v1/well/abc", params: []string{"well", ""}, match: true}, + {url: "/api/v1/well/ttt", params: nil, match: false}, + }) + testCase("/api/:day/:month?/:year?", []testparams{ + {url: "/api/1", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/", params: []string{"1", "", ""}, match: true}, + {url: "/api/1//", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/-/", params: []string{"1", "-", ""}, match: true}, + {url: "/api/1-", params: []string{"1-", "", ""}, match: true}, + {url: "/api/1.", params: []string{"1.", "", ""}, match: true}, + {url: "/api/1/2", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1/2/3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }) + testCase("/api/:day.:month?.:year?", []testparams{ + {url: "/api/1", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/", params: nil, match: false}, + {url: "/api/1.", params: []string{"1", "", ""}, match: true}, + {url: "/api/1.2", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1.2.3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }) + testCase("/api/:day-:month?-:year?", []testparams{ + {url: "/api/1", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/", params: nil, match: false}, + {url: "/api/1-", params: []string{"1", "", ""}, match: true}, + {url: "/api/1-/", params: nil, match: false}, + {url: "/api/1-/-", params: nil, match: false}, + {url: "/api/1-2", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1-2-3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }) + testCase("/api/*", []testparams{ + {url: "/api/", params: []string{""}, match: true}, + {url: "/api/joker", params: []string{"joker"}, match: true}, + {url: "/api", params: []string{""}, match: true}, + {url: "/api/v1/entity", params: []string{"v1/entity"}, match: true}, + {url: "/api2/v1/entity", params: nil, match: false}, + {url: "/api_ignore/v1/entity", params: nil, match: false}, + }) + testCase("/api/*/:param?", []testparams{ + {url: "/api/", params: []string{"", ""}, match: true}, + {url: "/api/joker", params: []string{"joker", ""}, match: true}, + {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, + {url: "/api/joker//batman", params: []string{"joker/", "batman"}, match: true}, + {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker/batman/robin/1/", params: []string{"joker/batman/robin/1", ""}, match: true}, + {url: "/api/joker-batman/robin/1", params: []string{"joker-batman/robin", "1"}, match: true}, + {url: "/api/joker-batman-robin/1", params: []string{"joker-batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: []string{"joker-batman-robin-1", ""}, match: true}, + {url: "/api", params: []string{"", ""}, match: true}, + }) + testCase("/api/*/:param", []testparams{ + {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, + {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, + {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker/batman-robin/1", params: []string{"joker/batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: nil, match: false}, + {url: "/api", params: nil, match: false}, + }) + testCase("/api/*/:param/:param2", []testparams{ + {url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true}, + {url: "/api/joker/batman", params: nil, match: false}, + {url: "/api/joker/batman/robin", params: []string{"joker", "batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman", "robin", "1"}, match: true}, + {url: "/api/joker/batman/robin/2/1", params: []string{"joker/batman/robin", "2", "1"}, match: true}, + {url: "/api/joker/batman-robin/1", params: []string{"joker", "batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: nil, match: false}, + {url: "/api", params: nil, match: false}, + }) + testCase("/partialCheck/foo/bar/:param", []testparams{ + {url: "/partialCheck/foo/bar/test", params: []string{"test"}, match: true, partialCheck: true}, + {url: "/partialCheck/foo/bar/test/test2", params: []string{"test"}, match: true, partialCheck: true}, + {url: "/partialCheck/foo/bar", params: nil, match: false, partialCheck: true}, + {url: "/partiaFoo", params: nil, match: false, partialCheck: true}, + }) + testCase("/api/*/:param/:param2", []testparams{ + {url: "/api/test/abc", params: nil, match: false}, + {url: "/api/joker/batman", params: nil, match: false}, + {url: "/api/joker/batman/robin", params: []string{"joker", "batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman", "robin", "1"}, match: true}, + {url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true}, + {url: "/api", params: nil, match: false}, + }) + testCase("/", []testparams{ + {url: "/api", params: nil, match: false}, + {url: "", params: []string{}, match: true}, + {url: "/", params: []string{}, match: true}, + }) + testCase("/config/abc.json", []testparams{ + {url: "/config/abc.json", params: []string{}, match: true}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config/efg.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }) + testCase("/config/*.json", []testparams{ + {url: "/config/abc.json", params: []string{"abc"}, match: true}, + {url: "/config/efg.json", params: []string{"efg"}, match: true}, + {url: "/config/efg.csv", params: nil, match: false}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }) + testCase("/xyz", []testparams{ + {url: "xyz", params: nil, match: false}, + {url: "xyz/", params: nil, match: false}, + }) +} + +////////////////////////////////////////////// +///////////////// BENCHMARKS ///////////////// +////////////////////////////////////////////// +// go test -v -run=^$ -bench=Benchmark_Path_ -benchmem -count=3 + +// func Benchmark_Path_paramsForPos(b *testing.B) { +// // TODO +// } + +// func Benchmark_Path_matchParams(b *testing.B) { +// // TODO +// } diff --git a/utils.go b/utils.go index 01ccb9b2..a2e70569 100644 --- a/utils.go +++ b/utils.go @@ -10,7 +10,6 @@ import ( "hash/crc32" "net" "strings" - "sync/atomic" "time" utils "github.com/gofiber/utils" @@ -166,249 +165,6 @@ var getBytesImmutable = func(s string) (b []byte) { return []byte(s) } -//region route registration and matching #-#-#-#-#-#-#-#-#-#-#-#-#-# - -// ⚠️ This path parser was based on urlpath by @ucarion (MIT License). -// 💖 Modified for the Fiber router by @renanbastos93 & @renewerner87 -// 🤖 ucarion/urlpath - renanbastos93/fastpath - renewerner87/fastpath - -// routeParser holds the path segments and param names -type routeParser struct { - segs []paramSeg - params []string -} - -// paramsSeg holds the segment metadata -type paramSeg struct { - Param string - Const string - IsParam bool - IsOptional bool - IsLast bool - EndChar byte -} - -// list of possible parameter and segment delimiter -// slash has a special role, unlike the other parameters it must not be interpreted as a parameter -var routeDelimiter = []byte{'/', '-', '.'} - -const wildcardParam string = "*" - -// parseRoute analyzes the route and divides it into segments for constant areas and parameters, -// this information is needed later when assigning the requests to the declared routes -func parseRoute(pattern string) (p routeParser) { - var out []paramSeg - var params []string - - part, delimiterPos := "", 0 - for len(pattern) > 0 && delimiterPos != -1 { - delimiterPos = findNextRouteSegmentEnd(pattern) - if delimiterPos != -1 { - part = pattern[:delimiterPos] - } else { - part = pattern - } - - partLen, lastSeg := len(part), len(out)-1 - if partLen == 0 { // skip empty parts - if len(pattern) > 0 { - // remove first char - pattern = pattern[1:] - } - continue - } - // is parameter ? - if part[0] == '*' || part[0] == ':' { - out = append(out, paramSeg{ - Param: utils.GetTrimmedParam(part), - IsParam: true, - IsOptional: part == wildcardParam || part[partLen-1] == '?', - }) - lastSeg = len(out) - 1 - params = append(params, out[lastSeg].Param) - // combine const segments - } else if lastSeg >= 0 && !out[lastSeg].IsParam { - out[lastSeg].Const += string(out[lastSeg].EndChar) + part - // create new const segment - } else { - out = append(out, paramSeg{ - Const: part, - }) - lastSeg = len(out) - 1 - } - - if delimiterPos != -1 && len(pattern) >= delimiterPos+1 { - out[lastSeg].EndChar = pattern[delimiterPos] - pattern = pattern[delimiterPos+1:] - } else { - // last default char - out[lastSeg].EndChar = '/' - } - } - if len(out) > 0 { - out[len(out)-1].IsLast = true - } - - p = routeParser{segs: out, params: params} - return -} - -// findNextRouteSegmentEnd searches in the route for the next end position for a segment -func findNextRouteSegmentEnd(search string) int { - nextPosition := -1 - for _, delimiter := range routeDelimiter { - if pos := strings.IndexByte(search, delimiter); pos != -1 && (pos < nextPosition || nextPosition == -1) { - nextPosition = pos - } - } - - return nextPosition -} - -// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions -func (p *routeParser) getMatch(s string, partialCheck bool) ([][2]int, bool) { - lenKeys := len(p.params) - paramsPositions := getAllocFreeParamsPos(lenKeys) - var i, j, paramsIterator, partLen, paramStart int - if len(s) > 0 { - s = s[1:] - paramStart++ - } - for index, segment := range p.segs { - partLen = len(s) - // check parameter - if segment.IsParam { - // determine parameter length - if segment.Param == wildcardParam { - if segment.IsLast { - i = partLen - } else { - i = findWildcardParamLen(s, p.segs, index) - } - } else { - i = strings.IndexByte(s, segment.EndChar) - } - if i == -1 { - i = partLen - } - - if !segment.IsOptional && i == 0 { - return nil, false - // special case for not slash end character - } else if i > 0 && partLen >= i && segment.EndChar != '/' && s[i-1] == '/' { - return nil, false - } - - paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i - paramsIterator++ - } else { - // check const segment - i = len(segment.Const) - if partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const || (partLen > i && s[i] != segment.EndChar) { - return nil, false - } - } - - // reduce founded part from the string - if partLen > 0 { - j = i + 1 - if segment.IsLast || partLen < j { - j = i - } - paramStart += j - - s = s[j:] - } - } - if len(s) != 0 && !partialCheck { - return nil, false - } - - return paramsPositions, true -} - -// paramsForPos get parameters for the given positions from the given path -func (p *routeParser) paramsForPos(path string, paramsPositions [][2]int) []string { - size := len(paramsPositions) - params := getAllocFreeParams(size) - for i, positions := range paramsPositions { - if positions[0] != positions[1] && len(path) >= positions[1] { - params[i] = path[positions[0]:positions[1]] - } else { - params[i] = "" - } - } - - return params -} - -// findWildcardParamLen for the expressjs wildcard behavior (right to left greedy) -// look at the other segments and take what is left for the wildcard from right to left -func findWildcardParamLen(s string, segments []paramSeg, currIndex int) int { - // "/api/*/:param" - "/api/joker/batman/robin/1" -> "joker/batman/robin", "1" - // "/api/*/:param" - "/api/joker/batman" -> "joker", "batman" - // "/api/*/:param" - "/api/joker-batman-robin/1" -> "joker-batman-robin", "1" - endChar := segments[currIndex].EndChar - neededEndChars := 0 - // count the needed chars for the other segments - for i := currIndex + 1; i < len(segments); i++ { - if segments[i].EndChar == endChar { - neededEndChars++ - } - } - // remove the part the other segments still need - for { - pos := strings.LastIndexByte(s, endChar) - if pos != -1 { - s = s[:pos] - } - neededEndChars-- - if neededEndChars <= 0 || pos == -1 { - break - } - } - - return len(s) -} - -// performance tricks -// creates predefined arrays that are used to match the request routes so that no allocations need to be made -var paramsDummy, paramsPosDummy = make([]string, 100000), make([][2]int, 100000) - -// positions parameter that moves further and further to the right and remains atomic over all simultaneous requests -// to assign a separate range to each request -var startParamList, startParamPosList uint32 = 0, 0 - -// getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use -func getAllocFreeParamsPos(allocLen int) [][2]int { - size := uint32(allocLen) - start := atomic.AddUint32(&startParamPosList, size) - if (start + 10) >= uint32(len(paramsPosDummy)) { - atomic.StoreUint32(&startParamPosList, 0) - return getAllocFreeParamsPos(allocLen) - } - start -= size - allocLen += int(start) - paramsPositions := paramsPosDummy[start:allocLen:allocLen] - return paramsPositions -} - -// getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use -func getAllocFreeParams(allocLen int) []string { - size := uint32(allocLen) - start := atomic.AddUint32(&startParamList, size) - if (start + 10) >= uint32(len(paramsPosDummy)) { - atomic.StoreUint32(&startParamList, 0) - return getAllocFreeParams(allocLen) - } - start -= size - allocLen += int(start) - params := paramsDummy[start:allocLen:allocLen] - return params -} - -//endregion #-#-#-#-#-#-#-#-#-#-#-#-#-# - // HTTP methods and their unique INTs var methodINT = map[string]int{ MethodGet: 0, diff --git a/utils_test.go b/utils_test.go index ee87560d..396bdb9e 100644 --- a/utils_test.go +++ b/utils_test.go @@ -5,7 +5,6 @@ package fiber import ( - "fmt" "testing" utils "github.com/gofiber/utils" @@ -86,193 +85,6 @@ func Test_Utils_getGroupPath(t *testing.T) { // // TODO // } -// go test -race -run Test_Utils_matchParams -func Test_Utils_matchParams(t *testing.T) { - t.Parallel() - type testparams struct { - url string - params []string - match bool - partialCheck bool - } - testCase := func(r string, cases []testparams) { - parser := parseRoute(r) - for _, c := range cases { - paramsPos, match := parser.getMatch(c.url, c.partialCheck) - utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) - if match && paramsPos != nil { - utils.AssertEqual(t, c.params, parser.paramsForPos(c.url, paramsPos), fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) - } else { - utils.AssertEqual(t, true, nil == paramsPos, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) - } - } - } - testCase("/api/v1/:param/*", []testparams{ - {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, - {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/:param?", []testparams{ - {url: "/api/v1", params: []string{""}, match: true}, - {url: "/api/v1/", params: []string{""}, match: true}, - {url: "/api/v1/optional", params: []string{"optional"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/xyz", params: nil, match: false}, - }) - testCase("/api/v1/*", []testparams{ - {url: "/api/v1", params: []string{""}, match: true}, - {url: "/api/v1/", params: []string{""}, match: true}, - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/entity/1/2", params: []string{"entity/1/2"}, match: true}, - {url: "/api/v1/Entity/1/2", params: []string{"Entity/1/2"}, match: true}, - {url: "/api/v", params: nil, match: false}, - {url: "/api/v2", params: nil, match: false}, - {url: "/api/abc", params: nil, match: false}, - }) - testCase("/api/v1/:param", []testparams{ - {url: "/api/v1/entity", params: []string{"entity"}, match: true}, - {url: "/api/v1/entity/8728382", params: nil, match: false}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/:param-:param2", []testparams{ - {url: "/api/v1/entity-entity2", params: []string{"entity", "entity2"}, match: true}, - {url: "/api/v1/entity/8728382", params: nil, match: false}, - {url: "/api/v1/entity-8728382", params: []string{"entity", "8728382"}, match: true}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/:filename.:extension", []testparams{ - {url: "/api/v1/test.pdf", params: []string{"test", "pdf"}, match: true}, - {url: "/api/v1/test/pdf", params: nil, match: false}, - {url: "/api/v1/test-pdf", params: nil, match: false}, - {url: "/api/v1/test_pdf", params: nil, match: false}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - }) - testCase("/api/v1/const", []testparams{ - {url: "/api/v1/const", params: []string{}, match: true}, - {url: "/api/v1", params: nil, match: false}, - {url: "/api/v1/", params: nil, match: false}, - {url: "/api/v1/something", params: nil, match: false}, - }) - testCase("/api/v1/:param/abc/*", []testparams{ - {url: "/api/v1/well/abc/wildcard", params: []string{"well", "wildcard"}, match: true}, - {url: "/api/v1/well/abc/", params: []string{"well", ""}, match: true}, - {url: "/api/v1/well/abc", params: []string{"well", ""}, match: true}, - {url: "/api/v1/well/ttt", params: nil, match: false}, - }) - testCase("/api/:day/:month?/:year?", []testparams{ - {url: "/api/1", params: []string{"1", "", ""}, match: true}, - {url: "/api/1/", params: []string{"1", "", ""}, match: true}, - {url: "/api/1//", params: []string{"1", "", ""}, match: true}, - {url: "/api/1/-/", params: []string{"1", "-", ""}, match: true}, - {url: "/api/1-", params: []string{"1-", "", ""}, match: true}, - {url: "/api/1.", params: []string{"1.", "", ""}, match: true}, - {url: "/api/1/2", params: []string{"1", "2", ""}, match: true}, - {url: "/api/1/2/3", params: []string{"1", "2", "3"}, match: true}, - {url: "/api/", params: nil, match: false}, - }) - testCase("/api/:day.:month?.:year?", []testparams{ - {url: "/api/1", params: []string{"1", "", ""}, match: true}, - {url: "/api/1/", params: nil, match: false}, - {url: "/api/1.", params: []string{"1", "", ""}, match: true}, - {url: "/api/1.2", params: []string{"1", "2", ""}, match: true}, - {url: "/api/1.2.3", params: []string{"1", "2", "3"}, match: true}, - {url: "/api/", params: nil, match: false}, - }) - testCase("/api/:day-:month?-:year?", []testparams{ - {url: "/api/1", params: []string{"1", "", ""}, match: true}, - {url: "/api/1/", params: nil, match: false}, - {url: "/api/1-", params: []string{"1", "", ""}, match: true}, - {url: "/api/1-/", params: nil, match: false}, - {url: "/api/1-/-", params: nil, match: false}, - {url: "/api/1-2", params: []string{"1", "2", ""}, match: true}, - {url: "/api/1-2-3", params: []string{"1", "2", "3"}, match: true}, - {url: "/api/", params: nil, match: false}, - }) - testCase("/api/*", []testparams{ - {url: "/api/", params: []string{""}, match: true}, - {url: "/api/joker", params: []string{"joker"}, match: true}, - {url: "/api", params: []string{""}, match: true}, - {url: "/api/v1/entity", params: []string{"v1/entity"}, match: true}, - {url: "/api2/v1/entity", params: nil, match: false}, - {url: "/api_ignore/v1/entity", params: nil, match: false}, - }) - testCase("/api/*/:param?", []testparams{ - {url: "/api/", params: []string{"", ""}, match: true}, - {url: "/api/joker", params: []string{"joker", ""}, match: true}, - {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, - {url: "/api/joker//batman", params: []string{"joker/", "batman"}, match: true}, - {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, - {url: "/api/joker/batman/robin/1/", params: []string{"joker/batman/robin/1", ""}, match: true}, - {url: "/api/joker-batman/robin/1", params: []string{"joker-batman/robin", "1"}, match: true}, - {url: "/api/joker-batman-robin/1", params: []string{"joker-batman-robin", "1"}, match: true}, - {url: "/api/joker-batman-robin-1", params: []string{"joker-batman-robin-1", ""}, match: true}, - {url: "/api", params: []string{"", ""}, match: true}, - }) - testCase("/api/*/:param", []testparams{ - {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, - {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, - {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, - {url: "/api/joker/batman-robin/1", params: []string{"joker/batman-robin", "1"}, match: true}, - {url: "/api/joker-batman-robin-1", params: nil, match: false}, - {url: "/api", params: nil, match: false}, - }) - testCase("/api/*/:param/:param2", []testparams{ - {url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true}, - {url: "/api/joker/batman", params: nil, match: false}, - {url: "/api/joker/batman/robin", params: []string{"joker", "batman", "robin"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman", "robin", "1"}, match: true}, - {url: "/api/joker/batman/robin/2/1", params: []string{"joker/batman/robin", "2", "1"}, match: true}, - {url: "/api/joker/batman-robin/1", params: []string{"joker", "batman-robin", "1"}, match: true}, - {url: "/api/joker-batman-robin-1", params: nil, match: false}, - {url: "/api", params: nil, match: false}, - }) - testCase("/partialCheck/foo/bar/:param", []testparams{ - {url: "/partialCheck/foo/bar/test", params: []string{"test"}, match: true, partialCheck: true}, - {url: "/partialCheck/foo/bar/test/test2", params: []string{"test"}, match: true, partialCheck: true}, - {url: "/partialCheck/foo/bar", params: nil, match: false, partialCheck: true}, - {url: "/partiaFoo", params: nil, match: false, partialCheck: true}, - }) - testCase("/api/*/:param/:param2", []testparams{ - {url: "/api/test/abc", params: nil, match: false}, - {url: "/api/joker/batman", params: nil, match: false}, - {url: "/api/joker/batman/robin", params: []string{"joker", "batman", "robin"}, match: true}, - {url: "/api/joker/batman/robin/1", params: []string{"joker/batman", "robin", "1"}, match: true}, - {url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true}, - {url: "/api", params: nil, match: false}, - }) - testCase("/", []testparams{ - {url: "/api", params: nil, match: false}, - {url: "", params: []string{}, match: true}, - {url: "/", params: []string{}, match: true}, - }) - testCase("/config/abc.json", []testparams{ - {url: "/config/abc.json", params: []string{}, match: true}, - {url: "config/abc.json", params: nil, match: false}, - {url: "/config/efg.json", params: nil, match: false}, - {url: "/config", params: nil, match: false}, - }) - testCase("/config/*.json", []testparams{ - {url: "/config/abc.json", params: []string{"abc"}, match: true}, - {url: "/config/efg.json", params: []string{"efg"}, match: true}, - {url: "/config/efg.csv", params: nil, match: false}, - {url: "config/abc.json", params: nil, match: false}, - {url: "/config", params: nil, match: false}, - }) - testCase("/xyz", []testparams{ - {url: "xyz", params: nil, match: false}, - {url: "xyz/", params: nil, match: false}, - }) -} - // func Test_Utils_getTrimmedParam(t *testing.T) { // // TODO // } @@ -304,15 +116,3 @@ func Benchmark_Utils_getGroupPath(b *testing.B) { // func Benchmark_Utils_parseTokenList(b *testing.B) { // // TODO // } - -// func Benchmark_Utils_getParams(b *testing.B) { -// // TODO -// } - -// func Benchmark_Utils_matchParams(b *testing.B) { -// // TODO -// } - -// func Benchmark_Utils_getCharPos(b *testing.B) { -// // TODO -// }