🚀 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

98
path.go
View File

@ -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 findParamLenForLastSegment(s, segments[currIndex])
}
return len(s)
}
// "/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)

View File

@ -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},
})

View File

@ -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("*"))