Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feature: route constraints #1998

Merged
merged 17 commits into from Aug 16, 2022
258 changes: 247 additions & 11 deletions path.go
Expand Up @@ -7,9 +7,15 @@
package fiber

import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"unicode"

"github.com/gofiber/fiber/v2/internal/uuid"
"github.com/gofiber/fiber/v2/utils"
)

Expand All @@ -33,20 +39,53 @@ 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
Data []string
}

const (
noConstraint TypeConstraint = iota
intConstraint
boolConstraint
floatConstraint
alphaConstraint
datetimeConstraint
guidConstraint
minLengthConstraint
maxLengthConstraint
exactLengthConstraint
BetweenLengthConstraint
minConstraint
maxConstraint
rangeConstraint
regexConstraint
)

// list of possible parameter and segment delimiter
Expand All @@ -59,6 +98,18 @@ var (
parameterDelimiterChars = append([]byte{paramStarterChar}, 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,
Expand Down Expand Up @@ -176,6 +227,8 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r
isWildCard := pattern[0] == wildcardParam
isPlusParam := pattern[0] == plusParam
parameterEndPosition := findNextNonEscapedCharsetPosition(pattern[1:], parameterEndChars)
parameterConstraintStart := -1
parameterConstraintEnd := -1

// handle wildcard end
if isWildCard || isPlusParam {
Expand All @@ -185,10 +238,47 @@ 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 {
constraints = append(constraints, &Constraint{
ID: getParamConstraintType(c[:start]),
Data: strings.Split(RemoveEscapeChar(c[start+1:end]), string(parameterConstraintDataSeparatorChars)),
})
} 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++
Expand All @@ -198,12 +288,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
Expand Down Expand Up @@ -270,6 +366,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++
}

Expand Down Expand Up @@ -368,3 +472,135 @@ func RemoveEscapeChar(word string) string {
}
return word
}

func getParamConstraintType(constraintPart string) TypeConstraint {
switch constraintPart {
case "int":
efectn marked this conversation as resolved.
Show resolved Hide resolved
return intConstraint
case "bool":
return boolConstraint
case "float":
return floatConstraint
case "alpha":
return alphaConstraint
case "guid":
return guidConstraint
case "minLen":
return minLengthConstraint
case "maxLen":
return maxLengthConstraint
case "exactLen":
return exactLengthConstraint
case "betweenLen":
return BetweenLengthConstraint
case "min":
return minConstraint
case "max":
return maxConstraint
case "range":
return rangeConstraint
case "datetime":
return datetimeConstraint
case "regex":
return regexConstraint
default:
return noConstraint
}

}

func (c *Constraint) CheckConstraint(param string) bool {
var err error
var num int

// check data exists
needOneData := []TypeConstraint{minLengthConstraint, maxLengthConstraint, exactLengthConstraint, minConstraint, maxConstraint, datetimeConstraint, regexConstraint}
needTwoData := []TypeConstraint{BetweenLengthConstraint, 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
}
}

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) {
err = errors.New("")
efectn marked this conversation as resolved.
Show resolved Hide resolved
}
}
case guidConstraint:
_, err = uuid.Parse(param)
case minLengthConstraint:
data, _ := strconv.Atoi(c.Data[0])

if len(param) < data {
err = errors.New("")
}
case maxLengthConstraint:
data, _ := strconv.Atoi(c.Data[0])

if len(param) > data {
err = errors.New("")
}
case exactLengthConstraint:
data, _ := strconv.Atoi(c.Data[0])

if len(param) != data {
err = errors.New("")
}
case BetweenLengthConstraint:
data, _ := strconv.Atoi(c.Data[0])
data2, _ := strconv.Atoi(c.Data[1])
length := len(param)

if !(length >= data && length <= data2) {
err = errors.New("")
}
case minConstraint:
data, _ := strconv.Atoi(c.Data[0])
num, err = strconv.Atoi(param)

if num < data {
err = errors.New("")
}
case maxConstraint:
data, _ := strconv.Atoi(c.Data[0])
num, err = strconv.Atoi(param)

if num > data {
err = errors.New("")
}
case rangeConstraint:
data, _ := strconv.Atoi(c.Data[0])
data2, _ := strconv.Atoi(c.Data[1])
num, err = strconv.Atoi(param)

if !(num >= data && num <= data2) {
err = errors.New("")
}
case datetimeConstraint:
fmt.Print(time.Parse(c.Data[0], param))
_, err = time.Parse(c.Data[0], param)
case regexConstraint:
match, _ := regexp.MatchString(c.Data[0], param)
if !match {
err = errors.New("")
}
}

return err == nil
Copy link
Member Author

@efectn efectn Aug 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we skip or return false when consantraint not found?

}