diff --git a/path.go b/path.go index 828b005a..5409b144 100644 --- a/path.go +++ b/path.go @@ -21,16 +21,19 @@ type routeParser struct { // paramsSeg holds the segment metadata type routeSegment struct { - Param string + ParamName string Const string IsParam bool IsWildcard bool + IsGreedy bool IsOptional bool IsLast bool + // TODO: add support for optional groups ? } const ( wildcardParam byte = '*' + plusParam byte = '+' optionalParam byte = '?' slashDelimiter byte = '/' paramStarterChar byte = ':' @@ -42,7 +45,7 @@ var ( // TODO '(' ')' delimiters for regex patterns routeDelimiter = []byte{slashDelimiter, '-', '.'} // list of chars for the parameter recognising - parameterStartChars = []byte{wildcardParam, paramStarterChar} + parameterStartChars = []byte{wildcardParam, plusParam, paramStarterChar} // list of chars at the end of the parameter parameterDelimiterChars = append([]byte{paramStarterChar}, routeDelimiter...) // list of chars to find the end of a parameter @@ -61,7 +64,7 @@ func parseRoute(pattern string) routeParser { // handle the parameter part if nextParamPosition == 0 { processedPart, seg := analyseParameterPart(pattern) - params, segList, part = append(params, seg.Param), append(segList, seg), processedPart + params, segList, part = append(params, seg.ParamName), append(segList, seg), processedPart } else { processedPart, seg := analyseConstantPart(pattern, nextParamPosition) segList, part = append(segList, seg), processedPart @@ -114,9 +117,10 @@ func analyseConstantPart(pattern string, nextParamPosition int) (string, routeSe // analyseParameterPart find the parameter end and create the route segment func analyseParameterPart(pattern string) (string, routeSegment) { isWildCard := pattern[0] == wildcardParam + isPlusParam := pattern[0] == plusParam parameterEndPosition := findNextCharsetPosition(pattern[1:], parameterEndChars) // handle wildcard end - if isWildCard { + if isWildCard || isPlusParam { parameterEndPosition = 0 } else if parameterEndPosition == -1 { parameterEndPosition = len(pattern) - 1 @@ -127,10 +131,10 @@ func analyseParameterPart(pattern string) (string, routeSegment) { processedPart := pattern[0 : parameterEndPosition+1] return processedPart, routeSegment{ - Param: utils.GetTrimmedParam(processedPart), + ParamName: utils.GetTrimmedParam(processedPart), IsParam: true, IsOptional: isWildCard || pattern[parameterEndPosition] == optionalParam, - IsWildcard: isWildCard, + IsGreedy: isWildCard || isPlusParam, } } @@ -171,20 +175,21 @@ func (p *routeParser) getMatch(s string, partialCheck bool) ([][2]int, bool) { if !segment.IsOptional && i == 0 { return nil, false } - + // take over the params positions paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i paramsIterator++ } else { // check const segment optionalPart := false i = len(segment.Const) + // check if the end of the segment is a optional slash and then if the segement is optional or the last one if i > 0 && partLen == i-1 && segment.Const[i-1] == slashDelimiter && s[:i-1] == segment.Const[:i-1] { if segment.IsLast || p.segs[index+1].IsOptional { i-- optionalPart = true } } - + // is optional part or the const part must match with the given string if optionalPart == false && (partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const) { return nil, false } @@ -226,26 +231,19 @@ func (p *routeParser) paramsForPos(path string, paramsPositions [][2]int) []stri // look at the other segments and take what is left for the wildcard from right to left func findParamLen(s string, segments []routeSegment, currIndex int) int { if segments[currIndex].IsLast { - if segments[currIndex].IsWildcard { - return len(s) - } - if i := strings.IndexByte(s, slashDelimiter); i != -1 { - return i - } - - return len(s) + return findParamLenForLastSegment(s, segments[currIndex]) } - // "/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" - nextSeg := segments[currIndex+1] - // check next segment - if nextSeg.IsParam { - if segments[currIndex].IsWildcard || nextSeg.IsWildcard { - // greedy logic + + compareSeg := segments[currIndex+1] + nextConstSegInd := currIndex + 1 + // check if parameter segments are directly after each other + if compareSeg.IsParam { + // and if one of them is greedy + if segments[currIndex].IsGreedy || compareSeg.IsGreedy { + // search for the next segment that contains a constant part, so that it can be used later for i := currIndex + 1; i < len(segments); i++ { if false == segments[i].IsParam { - nextSeg = segments[i] + nextConstSegInd = i break } } @@ -254,12 +252,27 @@ func findParamLen(s string, segments []routeSegment, currIndex int) int { return 1 } } + + return findParamLenUntilNextConstSeg(s, currIndex, nextConstSegInd, segments) +} + +// findParamLenUntilNextConstSeg Search the parameters until the next constant part +func findParamLenUntilNextConstSeg(s string, currIndex, nextConstSegInd int, segments []routeSegment) int { + compareSeg := segments[nextConstSegInd] // get the length to the next constant part - if false == nextSeg.IsParam { - searchString := nextSeg.Const + if false == compareSeg.IsParam { + searchString := compareSeg.Const if len(searchString) > 1 { - searchString = utils.TrimRight(nextSeg.Const, slashDelimiter) + searchString = utils.TrimRight(compareSeg.Const, slashDelimiter) } + // special logic for greedy params + if segments[currIndex].IsGreedy { + searchCount := strings.Count(s, searchString) + if searchCount > 1 { + return findGreedyParamLen(s, searchString, searchCount, nextConstSegInd, segments) + } + } + if constPosition := strings.Index(s, searchString); constPosition != -1 { return constPosition } @@ -268,6 +281,35 @@ func findParamLen(s string, segments []routeSegment, currIndex int) int { return len(s) } +// findParamLenForLastSegment get the length of the parameter if it is the last segment +func findParamLenForLastSegment(s string, seg routeSegment) int { + if seg.IsGreedy { + return len(s) + } + if i := strings.IndexByte(s, slashDelimiter); i != -1 { + return i + } + + return len(s) +} + +// findGreedyParamLen get the length of the parameter for greedy segments from right to left +func findGreedyParamLen(s, searchString string, searchCount, compareSegIndex int, segments []routeSegment) int { + // check all from right to left segments + for i := len(segments) - 1; i >= compareSegIndex && searchCount > 0; i-- { + if false == segments[i].IsParam && segments[i].Const == segments[compareSegIndex].Const { + searchCount-- + if constPosition := strings.LastIndex(s, searchString); constPosition != -1 { + s = s[:constPosition] + } else { + 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) @@ -276,7 +318,6 @@ var paramsDummy, paramsPosDummy = make([]string, 100000), make([][2]int, 100000) // to assign a separate range to each request var startParamList, startParamPosList uint32 = 0, 0 -// TODO: replace it with bytebufferpool and release the parameter buffers in ctx release function // getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use func getAllocFreeParamsPos(allocLen int) [][2]int { size := uint32(allocLen) @@ -291,6 +332,7 @@ func getAllocFreeParamsPos(allocLen int) [][2]int { return paramsPositions } +// TODO: replace it with bytebufferpool and release the parameter buffers in ctx release function // getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use func getAllocFreeParams(allocLen int) []string { size := uint32(allocLen) diff --git a/path_test.go b/path_test.go index 119126d7..2798147e 100644 --- a/path_test.go +++ b/path_test.go @@ -41,6 +41,14 @@ func Test_Path_matchParams(t *testing.T) { {url: "/api/v2", params: nil, match: false}, {url: "/api/v1/", params: nil, match: false}, }) + testCase("/api/v1/:param/+", []testparams{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/entity/", params: nil, match: false}, + {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}, @@ -126,7 +134,12 @@ func Test_Path_matchParams(t *testing.T) { testCase("/foo*bar", []testparams{ {url: "/foofaselbar", params: []string{"fasel"}, match: true}, {url: "/foobar", params: []string{""}, match: true}, - {url: "/", params: []string{""}, match: false}, + {url: "/", params: nil, match: false}, + }) + testCase("/foo+bar", []testparams{ + {url: "/foofaselbar", params: []string{"fasel"}, match: true}, + {url: "/foobar", params: nil, match: false}, + {url: "/", params: nil, match: false}, }) testCase("/a*cde*g/", []testparams{ {url: "/abbbcdefffg", params: []string{"bbb", "fff"}, match: true}, @@ -225,6 +238,15 @@ func Test_Path_matchParams(t *testing.T) { testCase("/config/*.json", []testparams{ {url: "/config/abc.json", params: []string{"abc"}, match: true}, {url: "/config/efg.json", params: []string{"efg"}, match: true}, + {url: "/config/.json", params: []string{""}, 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("/config/+.json", []testparams{ + {url: "/config/abc.json", params: []string{"abc"}, match: true}, + {url: "/config/.json", params: nil, match: false}, + {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}, @@ -233,29 +255,34 @@ func Test_Path_matchParams(t *testing.T) { {url: "xyz", params: nil, match: false}, {url: "xyz/", params: nil, match: false}, }) - // TODO: fix this 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", "batman"}, match: true},// TODO: fix it - //{url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true},// TODO: fix it - //{url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true},// TODO: fix it - //{url: "/api/joker/batman/robin/1/", params: []string{"joker/batman/robin/1", ""}, match: true},// TODO: fix it - //{url: "/api/joker-batman/robin/1", params: []string{"joker-batman/robin", "1"}, match: true},// TODO: fix it - //{url: "/api/joker-batman-robin/1", params: []string{"joker-batman-robin", "1"}, match: true},// TODO: fix it - //{url: "/api/joker-batman-robin-1", params: []string{"joker-batman-robin-1", ""}, match: true},// TODO: fix it + {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},// TODO: fix it - //{url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true},// TODO: fix it - //{url: "/api/joker/batman-robin/1", params: []string{"joker/batman-robin", "1"}, match: true},// TODO: fix it + {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", []testparams{ + {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker", 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}, @@ -263,8 +290,8 @@ func Test_Path_matchParams(t *testing.T) { {url: "/api/joker-batman-robin-1", params: nil, match: false}, {url: "/api/test/abc", 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}, // TODO: fix it - //{url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true},// TODO: fix it + {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}, {url: "/api/:test", params: nil, match: false}, }) diff --git a/router_test.go b/router_test.go index ece8309e..18f18574 100644 --- a/router_test.go +++ b/router_test.go @@ -98,8 +98,8 @@ func Test_Route_Match_Root(t *testing.T) { func Test_Route_Match_Parser(t *testing.T) { app := New() - app.Get("/foo/:Param", func(ctx *Ctx) { - ctx.Send(ctx.Params("Param")) + app.Get("/foo/:ParamName", func(ctx *Ctx) { + ctx.Send(ctx.Params("ParamName")) }) app.Get("/Foobar/*", func(ctx *Ctx) { ctx.Send(ctx.Params("*"))