🚀 improve routing behavior

pull/715/head
ReneWerner87 2020-08-07 21:42:53 +02:00
parent 30081db4fa
commit b8ca4c9b8a
3 changed files with 114 additions and 45 deletions

102
path.go
View File

@ -21,16 +21,19 @@ type routeParser struct {
// paramsSeg holds the segment metadata // paramsSeg holds the segment metadata
type routeSegment struct { type routeSegment struct {
Param string ParamName string
Const string Const string
IsParam bool IsParam bool
IsWildcard bool IsWildcard bool
IsGreedy bool
IsOptional bool IsOptional bool
IsLast bool IsLast bool
// TODO: add support for optional groups ?
} }
const ( const (
wildcardParam byte = '*' wildcardParam byte = '*'
plusParam byte = '+'
optionalParam byte = '?' optionalParam byte = '?'
slashDelimiter byte = '/' slashDelimiter byte = '/'
paramStarterChar byte = ':' paramStarterChar byte = ':'
@ -42,7 +45,7 @@ var (
// TODO '(' ')' delimiters for regex patterns // TODO '(' ')' delimiters for regex patterns
routeDelimiter = []byte{slashDelimiter, '-', '.'} routeDelimiter = []byte{slashDelimiter, '-', '.'}
// list of chars for the parameter recognising // 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 // list of chars at the end of the parameter
parameterDelimiterChars = append([]byte{paramStarterChar}, routeDelimiter...) parameterDelimiterChars = append([]byte{paramStarterChar}, routeDelimiter...)
// list of chars to find the end of a parameter // list of chars to find the end of a parameter
@ -61,7 +64,7 @@ func parseRoute(pattern string) routeParser {
// handle the parameter part // handle the parameter part
if nextParamPosition == 0 { if nextParamPosition == 0 {
processedPart, seg := analyseParameterPart(pattern) 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 { } else {
processedPart, seg := analyseConstantPart(pattern, nextParamPosition) processedPart, seg := analyseConstantPart(pattern, nextParamPosition)
segList, part = append(segList, seg), processedPart 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 // analyseParameterPart find the parameter end and create the route segment
func analyseParameterPart(pattern string) (string, routeSegment) { func analyseParameterPart(pattern string) (string, routeSegment) {
isWildCard := pattern[0] == wildcardParam isWildCard := pattern[0] == wildcardParam
isPlusParam := pattern[0] == plusParam
parameterEndPosition := findNextCharsetPosition(pattern[1:], parameterEndChars) parameterEndPosition := findNextCharsetPosition(pattern[1:], parameterEndChars)
// handle wildcard end // handle wildcard end
if isWildCard { if isWildCard || isPlusParam {
parameterEndPosition = 0 parameterEndPosition = 0
} else if parameterEndPosition == -1 { } else if parameterEndPosition == -1 {
parameterEndPosition = len(pattern) - 1 parameterEndPosition = len(pattern) - 1
@ -127,10 +131,10 @@ func analyseParameterPart(pattern string) (string, routeSegment) {
processedPart := pattern[0 : parameterEndPosition+1] processedPart := pattern[0 : parameterEndPosition+1]
return processedPart, routeSegment{ return processedPart, routeSegment{
Param: utils.GetTrimmedParam(processedPart), ParamName: utils.GetTrimmedParam(processedPart),
IsParam: true, IsParam: true,
IsOptional: isWildCard || pattern[parameterEndPosition] == optionalParam, 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 { if !segment.IsOptional && i == 0 {
return nil, false return nil, false
} }
// take over the params positions
paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i
paramsIterator++ paramsIterator++
} else { } else {
// check const segment // check const segment
optionalPart := false optionalPart := false
i = len(segment.Const) 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 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 { if segment.IsLast || p.segs[index+1].IsOptional {
i-- i--
optionalPart = true 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) { if optionalPart == false && (partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const) {
return nil, false 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 // 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 { func findParamLen(s string, segments []routeSegment, currIndex int) int {
if segments[currIndex].IsLast { if segments[currIndex].IsLast {
if segments[currIndex].IsWildcard { return findParamLenForLastSegment(s, segments[currIndex])
return len(s)
}
if i := strings.IndexByte(s, slashDelimiter); i != -1 {
return i
} }
return len(s) compareSeg := segments[currIndex+1]
} nextConstSegInd := currIndex + 1
// "/api/*/:param" - "/api/joker/batman/robin/1" -> "joker/batman/robin", "1" // check if parameter segments are directly after each other
// "/api/*/:param" - "/api/joker/batman" -> "joker", "batman" if compareSeg.IsParam {
// "/api/*/:param" - "/api/joker-batman-robin/1" -> "joker-batman-robin", "1" // and if one of them is greedy
nextSeg := segments[currIndex+1] if segments[currIndex].IsGreedy || compareSeg.IsGreedy {
// check next segment // search for the next segment that contains a constant part, so that it can be used later
if nextSeg.IsParam {
if segments[currIndex].IsWildcard || nextSeg.IsWildcard {
// greedy logic
for i := currIndex + 1; i < len(segments); i++ { for i := currIndex + 1; i < len(segments); i++ {
if false == segments[i].IsParam { if false == segments[i].IsParam {
nextSeg = segments[i] nextConstSegInd = i
break break
} }
} }
@ -254,12 +252,27 @@ func findParamLen(s string, segments []routeSegment, currIndex int) int {
return 1 return 1
} }
} }
// get the length to the next constant part
if false == nextSeg.IsParam { return findParamLenUntilNextConstSeg(s, currIndex, nextConstSegInd, segments)
searchString := nextSeg.Const
if len(searchString) > 1 {
searchString = utils.TrimRight(nextSeg.Const, slashDelimiter)
} }
// 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 == compareSeg.IsParam {
searchString := compareSeg.Const
if len(searchString) > 1 {
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 { if constPosition := strings.Index(s, searchString); constPosition != -1 {
return constPosition return constPosition
} }
@ -268,6 +281,35 @@ func findParamLen(s string, segments []routeSegment, currIndex int) int {
return len(s) 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 // performance tricks
// creates predefined arrays that are used to match the request routes so that no allocations need to be made // 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) 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 // to assign a separate range to each request
var startParamList, startParamPosList uint32 = 0, 0 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 // getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use
func getAllocFreeParamsPos(allocLen int) [][2]int { func getAllocFreeParamsPos(allocLen int) [][2]int {
size := uint32(allocLen) size := uint32(allocLen)
@ -291,6 +332,7 @@ func getAllocFreeParamsPos(allocLen int) [][2]int {
return paramsPositions 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 // getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use
func getAllocFreeParams(allocLen int) []string { func getAllocFreeParams(allocLen int) []string {
size := uint32(allocLen) size := uint32(allocLen)

View File

@ -41,6 +41,14 @@ func Test_Path_matchParams(t *testing.T) {
{url: "/api/v2", params: nil, match: false}, {url: "/api/v2", params: nil, match: false},
{url: "/api/v1/", 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{ testCase("/api/v1/:param?", []testparams{
{url: "/api/v1", params: []string{""}, match: true}, {url: "/api/v1", params: []string{""}, match: true},
{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{ testCase("/foo*bar", []testparams{
{url: "/foofaselbar", params: []string{"fasel"}, match: true}, {url: "/foofaselbar", params: []string{"fasel"}, match: true},
{url: "/foobar", params: []string{""}, 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{ testCase("/a*cde*g/", []testparams{
{url: "/abbbcdefffg", params: []string{"bbb", "fff"}, match: true}, {url: "/abbbcdefffg", params: []string{"bbb", "fff"}, match: true},
@ -225,6 +238,15 @@ func Test_Path_matchParams(t *testing.T) {
testCase("/config/*.json", []testparams{ testCase("/config/*.json", []testparams{
{url: "/config/abc.json", params: []string{"abc"}, match: true}, {url: "/config/abc.json", params: []string{"abc"}, match: true},
{url: "/config/efg.json", params: []string{"efg"}, 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/efg.csv", params: nil, match: false},
{url: "config/abc.json", params: nil, match: false}, {url: "config/abc.json", params: nil, match: false},
{url: "/config", 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},
{url: "xyz/", params: nil, match: false}, {url: "xyz/", params: nil, match: false},
}) })
// TODO: fix this
testCase("/api/*/:param?", []testparams{ testCase("/api/*/:param?", []testparams{
{url: "/api/", params: []string{"", ""}, match: true}, {url: "/api/", params: []string{"", ""}, match: true},
{url: "/api/joker", params: []string{"joker", ""}, 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", params: []string{"joker//batman", "batman"}, 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},// 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},// TODO: fix it {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},// TODO: fix it {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},// TODO: fix it {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},// TODO: fix it {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},// TODO: fix it {url: "/api/joker-batman-robin-1", params: []string{"joker-batman-robin-1", ""}, match: true},
{url: "/api", params: []string{"", ""}, match: true}, {url: "/api", params: []string{"", ""}, match: true},
}) })
testCase("/api/*/:param", []testparams{ testCase("/api/*/:param", []testparams{
{url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, {url: "/api/test/abc", params: []string{"test", "abc"}, 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},// 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},// TODO: fix it {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},// TODO: fix it {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/joker-batman-robin-1", params: nil, match: false},
{url: "/api", 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{ testCase("/api/*/:param/:param2", []testparams{
{url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true}, {url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true},
{url: "/api/joker/batman", params: nil, match: false}, {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/joker-batman-robin-1", params: nil, match: false},
{url: "/api/test/abc", 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", 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", params: []string{"joker/batman", "robin", "1"}, match: true},
//{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/2", params: []string{"joker/batman/robin", "1", "2"}, match: true},
{url: "/api", params: nil, match: false}, {url: "/api", params: nil, match: false},
{url: "/api/:test", params: nil, match: false}, {url: "/api/:test", params: nil, match: false},
}) })

View File

@ -98,8 +98,8 @@ func Test_Route_Match_Root(t *testing.T) {
func Test_Route_Match_Parser(t *testing.T) { func Test_Route_Match_Parser(t *testing.T) {
app := New() app := New()
app.Get("/foo/:Param", func(ctx *Ctx) { app.Get("/foo/:ParamName", func(ctx *Ctx) {
ctx.Send(ctx.Params("Param")) ctx.Send(ctx.Params("ParamName"))
}) })
app.Get("/Foobar/*", func(ctx *Ctx) { app.Get("/Foobar/*", func(ctx *Ctx) {
ctx.Send(ctx.Params("*")) ctx.Send(ctx.Params("*"))