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 <rene@gofiber.io>

Co-authored-by: Mohab Abd El-Dayem <mohab.m.mohamed@gmail.com>
Co-authored-by: RW <rene@gofiber.io>
pull/2017/head
M. Efe Çetin 2022-08-16 09:05:50 +03:00 committed by GitHub
parent 95abdacba0
commit 2517944c80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 563 additions and 12 deletions

View File

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

297
path.go
View File

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

View File

@ -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<int>", []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<bool>", []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<float>", []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<alpha>", []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<guid>", []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<minLen>", []testparams{
{url: "/api/v1/entity", params: []string{"entity"}, match: false},
{url: "/api/v1/8728382", params: []string{"8728382"}, match: false},
})
testCase("/api/v1/:param<minLen(5)>", []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<maxLen(5)>", []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<exactLen(5)>", []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<betweenLen(1)>", []testparams{
{url: "/api/v1/entity", params: []string{"entity"}, match: false},
{url: "/api/v1/ent", params: []string{"ent"}, match: false},
})
testCase("/api/v1/:param<betweenLen(2,5)>", []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<betweenLen(2,5)>", []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<min(5)>", []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<max(5)>", []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<range(5,10)>", []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<datetime(2006\\-01\\-02)>", []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<regex(p\\([a\\-z]\\+\\)ch)>", []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<int;bool((>", []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<int;max(3000)>", []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<int;maxLen(10)>", []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<int;range(10,30)>", []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<int>", []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<bool>", []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<float>", []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<alpha>", []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<guid>", []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<minLen>", []testparams{
{url: "/api/v1/entity", params: []string{"entity"}, match: false},
{url: "/api/v1/8728382", params: []string{"8728382"}, match: false},
})
benchCase("/api/v1/:param<minLen(5)>", []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<maxLen(5)>", []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<exactLen(5)>", []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<betweenLen(1)>", []testparams{
{url: "/api/v1/entity", params: []string{"entity"}, match: false},
{url: "/api/v1/ent", params: []string{"ent"}, match: false},
})
benchCase("/api/v1/:param<betweenLen(2,5)>", []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<betweenLen(2,5)>", []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<min(5)>", []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<max(5)>", []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<range(5,10)>", []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<datetime(2006\\-01\\-02)>", []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<regex(p\\([a\\-z]\\+\\)ch)>", []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<int;bool((>", []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<int;max(3000)>", []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<int;maxLen(10)>", []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<int;range(10,30)>", []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<datetime(2006-01-02)>", []testparams{
{url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true},
})
}