From 2517944c80e7f02aeac49889c552493f88ba43c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Tue, 16 Aug 2022 09:05:50 +0300 Subject: [PATCH] :sparkles: feature: route constraints (#1998) * Segment parameters constraints and determining it's type * add parsing for constraints. * fix tests * add tests, benchs & some fixes. * fix regex & datetime tests. * clean up constraint parser, multiple constraint support. * update * regex customization. * constants, remove variadic methods. * add some benchs, refactor constraint check funtion. * more readable conditions * fix tests * precompile regex * precompile regex when parsing the route * update comments Co-authored-by: wernerr Co-authored-by: Mohab Abd El-Dayem Co-authored-by: RW --- helpers.go | 18 ++++ path.go | 297 ++++++++++++++++++++++++++++++++++++++++++++++++--- path_test.go | 260 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 563 insertions(+), 12 deletions(-) diff --git a/helpers.go b/helpers.go index e49fbfd352..f5e1ee55d8 100644 --- a/helpers.go +++ b/helpers.go @@ -695,3 +695,21 @@ const ( CookieSameSiteStrictMode = "strict" CookieSameSiteNoneMode = "none" ) + +// Route Constraints +const ( + ConstraintInt = "int" + ConstraintBool = "bool" + ConstraintFloat = "float" + ConstraintAlpha = "alpha" + ConstraintGuid = "guid" + ConstraintMinLen = "minLen" + ConstraintMaxLen = "maxLen" + ConstraintExactLen = "exactLen" + ConstraintBetweenLen = "betweenLen" + ConstraintMin = "min" + ConstraintMax = "max" + ConstraintRange = "range" + ConstraintDatetime = "datetime" + ConstraintRegex = "regex" +) diff --git a/path.go b/path.go index 21a3d6c4d0..16d1b2a37d 100644 --- a/path.go +++ b/path.go @@ -7,9 +7,13 @@ package fiber import ( + "regexp" "strconv" "strings" + "time" + "unicode" + "github.com/gofiber/fiber/v2/internal/uuid" "github.com/gofiber/fiber/v2/utils" ) @@ -33,20 +37,54 @@ type routeSegment struct { IsGreedy bool // indicates whether the parameter is greedy or not, is used with wildcard and plus IsOptional bool // indicates whether the parameter is optional or not // common information - IsLast bool // shows if the segment is the last one for the route - HasOptionalSlash bool // segment has the possibility of an optional slash - Length int // length of the parameter for segment, when its 0 then the length is undetermined + IsLast bool // shows if the segment is the last one for the route + HasOptionalSlash bool // segment has the possibility of an optional slash + Constraints []*Constraint // Constraint type if segment is a parameter, if not it will be set to noConstraint by default + Length int // length of the parameter for segment, when its 0 then the length is undetermined // future TODO: add support for optional groups "/abc(/def)?" } // different special routing signs const ( - wildcardParam byte = '*' // indicates a optional greedy parameter - plusParam byte = '+' // indicates a required greedy parameter - optionalParam byte = '?' // concludes a parameter by name and makes it optional - paramStarterChar byte = ':' // start character for a parameter with name - slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional - escapeChar byte = '\\' // escape character + wildcardParam byte = '*' // indicates a optional greedy parameter + plusParam byte = '+' // indicates a required greedy parameter + optionalParam byte = '?' // concludes a parameter by name and makes it optional + paramStarterChar byte = ':' // start character for a parameter with name + slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional + escapeChar byte = '\\' // escape character + paramConstraintStart byte = '<' // start of type constraint for a parameter + paramConstraintEnd byte = '>' // end of type constraint for a parameter + paramConstraintSeparator byte = ';' // separator of type constraints for a parameter + paramConstraintDataStart byte = '(' // start of data of type constraint for a parameter + paramConstraintDataEnd byte = ')' // end of data of type constraint for a parameter + paramConstraintDataSeparator byte = ',' // separator of datas of type constraint for a parameter +) + +// parameter constraint types +type TypeConstraint int16 + +type Constraint struct { + ID TypeConstraint + RegexCompiler *regexp.Regexp + Data []string +} + +const ( + noConstraint TypeConstraint = iota + 1 + intConstraint + boolConstraint + floatConstraint + alphaConstraint + datetimeConstraint + guidConstraint + minLenConstraint + maxLenConstraint + exactLenConstraint + betweenLenConstraint + minConstraint + maxConstraint + rangeConstraint + regexConstraint ) // list of possible parameter and segment delimiter @@ -61,6 +99,18 @@ var ( parameterDelimiterChars = append([]byte{paramStarterChar, escapeChar}, routeDelimiter...) // list of chars to find the end of a parameter parameterEndChars = append([]byte{optionalParam}, parameterDelimiterChars...) + // list of parameter constraint start + parameterConstraintStartChars = []byte{paramConstraintStart} + // list of parameter constraint end + parameterConstraintEndChars = []byte{paramConstraintEnd} + // list of parameter separator + parameterConstraintSeparatorChars = []byte{paramConstraintSeparator} + // list of parameter constraint data start + parameterConstraintDataStartChars = []byte{paramConstraintDataStart} + // list of parameter constraint data end + parameterConstraintDataEndChars = []byte{paramConstraintDataEnd} + // list of parameter constraint data separator + parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator} ) // parseRoute analyzes the route and divides it into segments for constant areas and parameters, @@ -177,8 +227,16 @@ func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPos func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam - parameterEndPosition := findNextNonEscapedCharsetPosition(pattern[1:], parameterEndChars) + var parameterEndPosition int + if strings.ContainsRune(pattern, rune(paramConstraintStart)) && strings.ContainsRune(pattern, rune(paramConstraintEnd)) { + parameterEndPosition = findNextCharsetPositionConstraint(pattern[1:], parameterEndChars) + } else { + parameterEndPosition = findNextNonEscapedCharsetPosition(pattern[1:], parameterEndChars) + } + + parameterConstraintStart := -1 + parameterConstraintEnd := -1 // handle wildcard end if isWildCard || isPlusParam { parameterEndPosition = 0 @@ -187,10 +245,53 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r } else if !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars) { parameterEndPosition++ } + + // find constraint part if exists in the parameter part and remove it + if parameterEndPosition > 0 { + parameterConstraintStart = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition], parameterConstraintStartChars) + parameterConstraintEnd = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition+1], parameterConstraintEndChars) + } + // cut params part processedPart := pattern[0 : parameterEndPosition+1] - paramName := RemoveEscapeChar(GetTrimmedParam(processedPart)) + + // Check has constraint + var constraints []*Constraint + + if hasConstraint := (parameterConstraintStart != -1 && parameterConstraintEnd != -1); hasConstraint { + constraintString := pattern[parameterConstraintStart+1 : parameterConstraintEnd] + userconstraints := strings.Split(constraintString, string(parameterConstraintSeparatorChars)) + constraints = make([]*Constraint, 0, len(userconstraints)) + + for _, c := range userconstraints { + start := findNextNonEscapedCharsetPosition(c, parameterConstraintDataStartChars) + end := findNextNonEscapedCharsetPosition(c, parameterConstraintDataEndChars) + + // Assign constraint + if start != -1 && end != -1 { + constraint := &Constraint{ + ID: getParamConstraintType(c[:start]), + Data: strings.Split(RemoveEscapeChar(c[start+1:end]), string(parameterConstraintDataSeparatorChars)), + } + + // Precompile regex if has regex constraint + if constraint.ID == regexConstraint { + constraint.RegexCompiler = regexp.MustCompile(constraint.Data[0]) + } + + constraints = append(constraints, constraint) + } else { + constraints = append(constraints, &Constraint{ + ID: getParamConstraintType(c), + Data: []string{}, + }) + } + } + + paramName = RemoveEscapeChar(GetTrimmedParam(pattern[0:parameterConstraintStart])) + } + // add access iterator to wildcard and plus if isWildCard { routeParser.wildCardCount++ @@ -200,12 +301,18 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r paramName += strconv.Itoa(routeParser.plusCount) } - return processedPart, &routeSegment{ + segment := &routeSegment{ ParamName: paramName, IsParam: true, IsOptional: isWildCard || pattern[parameterEndPosition] == optionalParam, IsGreedy: isWildCard || isPlusParam, } + + if len(constraints) > 0 { + segment.Constraints = constraints + } + + return processedPart, segment } // isInCharset check is the given character in the charset list @@ -230,6 +337,34 @@ func findNextCharsetPosition(search string, charset []byte) int { return nextPosition } +// findNextCharsetPositionConstraint search the next char position from the charset +// unlike findNextCharsetPosition, it takes care of constraint start-end chars to parse route pattern +func findNextCharsetPositionConstraint(search string, charset []byte) int { + nextPosition := -1 + constraintStart := -1 + constraintEnd := -1 + + for _, char := range charset { + pos := strings.IndexByte(search, char) + + if char == paramConstraintStart { + constraintStart = pos + } + + if char == paramConstraintEnd { + constraintEnd = pos + } + //fmt.Println(string(char)) + if pos != -1 && (pos < nextPosition || nextPosition == -1) { + if pos > constraintStart && pos < constraintEnd { + nextPosition = pos + } + } + } + + return nextPosition +} + // findNextNonEscapedCharsetPosition search the next char position from the charset and skip the escaped characters func findNextNonEscapedCharsetPosition(search string, charset []byte) int { pos := findNextCharsetPosition(search, charset) @@ -272,6 +407,14 @@ func (routeParser *routeParser) getMatch(detectionPath, path string, params *[ma } // take over the params positions params[paramsIterator] = path[:i] + + // check constraint + for _, c := range segment.Constraints { + if matched := c.CheckConstraint(params[paramsIterator]); !matched { + return false + } + } + paramsIterator++ } @@ -370,3 +513,133 @@ func RemoveEscapeChar(word string) string { } return word } + +func getParamConstraintType(constraintPart string) TypeConstraint { + switch constraintPart { + case ConstraintInt: + return intConstraint + case ConstraintBool: + return boolConstraint + case ConstraintFloat: + return floatConstraint + case ConstraintAlpha: + return alphaConstraint + case ConstraintGuid: + return guidConstraint + case ConstraintMinLen: + return minLenConstraint + case ConstraintMaxLen: + return maxLenConstraint + case ConstraintExactLen: + return exactLenConstraint + case ConstraintBetweenLen: + return betweenLenConstraint + case ConstraintMin: + return minConstraint + case ConstraintMax: + return maxConstraint + case ConstraintRange: + return rangeConstraint + case ConstraintDatetime: + return datetimeConstraint + case ConstraintRegex: + return regexConstraint + default: + return noConstraint + } + +} + +func (c *Constraint) CheckConstraint(param string) bool { + var err error + var num int + + // check data exists + needOneData := []TypeConstraint{minLenConstraint, maxLenConstraint, exactLenConstraint, minConstraint, maxConstraint, datetimeConstraint, regexConstraint} + needTwoData := []TypeConstraint{betweenLenConstraint, rangeConstraint} + + for _, data := range needOneData { + if c.ID == data && len(c.Data) == 0 { + return false + } + } + + for _, data := range needTwoData { + if c.ID == data && len(c.Data) < 2 { + return false + } + } + + // check constraints + switch c.ID { + case intConstraint: + _, err = strconv.Atoi(param) + case boolConstraint: + _, err = strconv.ParseBool(param) + case floatConstraint: + _, err = strconv.ParseFloat(param, 32) + case alphaConstraint: + for _, r := range param { + if !unicode.IsLetter(r) { + return false + } + } + case guidConstraint: + _, err = uuid.Parse(param) + case minLenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + + if len(param) < data { + return false + } + case maxLenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + + if len(param) > data { + return false + } + case exactLenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + + if len(param) != data { + return false + } + case betweenLenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + data2, _ := strconv.Atoi(c.Data[1]) + length := len(param) + if length < data || length > data2 { + return false + } + case minConstraint: + data, _ := strconv.Atoi(c.Data[0]) + num, err = strconv.Atoi(param) + + if num < data { + return false + } + case maxConstraint: + data, _ := strconv.Atoi(c.Data[0]) + num, err = strconv.Atoi(param) + + if num > data { + return false + } + case rangeConstraint: + data, _ := strconv.Atoi(c.Data[0]) + data2, _ := strconv.Atoi(c.Data[1]) + num, err = strconv.Atoi(param) + + if num < data || num > data2 { + return false + } + case datetimeConstraint: + _, err = time.Parse(c.Data[0], param) + case regexConstraint: + if match := c.RegexCompiler.MatchString(param); !match { + return false + } + } + + return err == nil +} diff --git a/path_test.go b/path_test.go index 9a237520dd..7ad514098f 100644 --- a/path_test.go +++ b/path_test.go @@ -431,6 +431,124 @@ func Test_Path_matchParams(t *testing.T) { {url: "/api", params: nil, match: false}, {url: "/api/:test", params: nil, match: false}, }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/true", params: []string{"true"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/8728382.5", params: []string{"8728382.5"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/#!?", params: []string{"#!?"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/f0fa66cc-d22e-445b-866d-1d76e776371d", params: []string{"f0fa66cc-d22e-445b-866d-1d76e776371d"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/123", params: []string{"123"}, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/ent", params: []string{"ent"}, match: true}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/e", params: []string{"e"}, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/e", params: []string{"e"}, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/1", params: []string{"1"}, match: false}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/1", params: []string{"1"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: []string{"15"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/9", params: []string{"9"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: []string{"15"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/15", params: []string{"15"}, match: false}, + {url: "/api/v1/peach", params: []string{"peach"}, match: true}, + {url: "/api/v1/p34ch", params: []string{"p34ch"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/87283827683", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/87283827683", params: []string{"8728382"}, match: false}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) } func Test_Utils_GetTrimmedParam(t *testing.T) { @@ -519,4 +637,146 @@ func Benchmark_Path_matchParams(t *testing.B) { {url: "/api/v2", params: nil, match: false}, {url: "/api/v1/", params: nil, match: false}, }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/true", params: []string{"true"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/8728382.5", params: []string{"8728382.5"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/#!?", params: []string{"#!?"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/f0fa66cc-d22e-445b-866d-1d76e776371d", params: []string{"f0fa66cc-d22e-445b-866d-1d76e776371d"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/123", params: []string{"123"}, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/ent", params: []string{"ent"}, match: true}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/e", params: []string{"e"}, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/e", params: []string{"e"}, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/1", params: []string{"1"}, match: false}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/1", params: []string{"1"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: []string{"15"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/9", params: []string{"9"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: []string{"15"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/ent", params: []string{"ent"}, match: false}, + {url: "/api/v1/15", params: []string{"15"}, match: false}, + {url: "/api/v1/peach", params: []string{"peach"}, match: true}, + {url: "/api/v1/p34ch", params: []string{"p34ch"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/87283827683", params: []string{"8728382"}, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) + benchCase("/api/v1/:param", []testparams{ + {url: "/api/v1/entity", params: []string{"entity"}, match: false}, + {url: "/api/v1/87283827683", params: []string{"8728382"}, match: false}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: false}, + }) +} + +func Test_Path_matchParams0(t *testing.T) { + t.Parallel() + type testparams struct { + url string + params []string + match bool + partialCheck bool + } + var ctxParams [maxParams]string + testCase := func(r string, cases []testparams) { + parser := parseRoute(r) + for _, c := range cases { + match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck) + utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + if match && len(c.params) > 0 { + utils.AssertEqual(t, c.params[0:len(c.params)], ctxParams[0:len(c.params)], fmt.Sprintf("route: '%s', url: '%s'", r, c.url)) + } + } + } + testCase("/api/v1/:param", []testparams{ + {url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true}, + }) }