mirror of https://github.com/gofiber/fiber.git
move path matching and registration in separate file "path.go"
parent
f816c6706f
commit
e674d679df
|
@ -0,0 +1,251 @@
|
|||
// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
|
||||
// 🤖 Github Repository: https://github.com/gofiber/fiber
|
||||
// 📌 API Documentation: https://docs.gofiber.io
|
||||
|
||||
package fiber
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
utils "github.com/gofiber/utils"
|
||||
)
|
||||
|
||||
// ⚠️ This path parser was based on urlpath by @ucarion (MIT License).
|
||||
// 💖 Modified for the Fiber router by @renanbastos93 & @renewerner87
|
||||
// 🤖 ucarion/urlpath - renanbastos93/fastpath - renewerner87/fastpath
|
||||
|
||||
// routeParser holds the path segments and param names
|
||||
type routeParser struct {
|
||||
segs []paramSeg
|
||||
params []string
|
||||
}
|
||||
|
||||
// paramsSeg holds the segment metadata
|
||||
type paramSeg struct {
|
||||
Param string
|
||||
Const string
|
||||
IsParam bool
|
||||
IsOptional bool
|
||||
IsLast bool
|
||||
EndChar byte
|
||||
}
|
||||
|
||||
// list of possible parameter and segment delimiter
|
||||
// slash has a special role, unlike the other parameters it must not be interpreted as a parameter
|
||||
var routeDelimiter = []byte{'/', '-', '.'}
|
||||
|
||||
const wildcardParam string = "*"
|
||||
|
||||
// parseRoute analyzes the route and divides it into segments for constant areas and parameters,
|
||||
// this information is needed later when assigning the requests to the declared routes
|
||||
func parseRoute(pattern string) (p routeParser) {
|
||||
var out []paramSeg
|
||||
var params []string
|
||||
|
||||
part, delimiterPos := "", 0
|
||||
for len(pattern) > 0 && delimiterPos != -1 {
|
||||
delimiterPos = findNextRouteSegmentEnd(pattern)
|
||||
if delimiterPos != -1 {
|
||||
part = pattern[:delimiterPos]
|
||||
} else {
|
||||
part = pattern
|
||||
}
|
||||
|
||||
partLen, lastSeg := len(part), len(out)-1
|
||||
if partLen == 0 { // skip empty parts
|
||||
if len(pattern) > 0 {
|
||||
// remove first char
|
||||
pattern = pattern[1:]
|
||||
}
|
||||
continue
|
||||
}
|
||||
// is parameter ?
|
||||
if part[0] == '*' || part[0] == ':' {
|
||||
out = append(out, paramSeg{
|
||||
Param: utils.GetTrimmedParam(part),
|
||||
IsParam: true,
|
||||
IsOptional: part == wildcardParam || part[partLen-1] == '?',
|
||||
})
|
||||
lastSeg = len(out) - 1
|
||||
params = append(params, out[lastSeg].Param)
|
||||
// combine const segments
|
||||
} else if lastSeg >= 0 && !out[lastSeg].IsParam {
|
||||
out[lastSeg].Const += string(out[lastSeg].EndChar) + part
|
||||
// create new const segment
|
||||
} else {
|
||||
out = append(out, paramSeg{
|
||||
Const: part,
|
||||
})
|
||||
lastSeg = len(out) - 1
|
||||
}
|
||||
|
||||
if delimiterPos != -1 && len(pattern) >= delimiterPos+1 {
|
||||
out[lastSeg].EndChar = pattern[delimiterPos]
|
||||
pattern = pattern[delimiterPos+1:]
|
||||
} else {
|
||||
// last default char
|
||||
out[lastSeg].EndChar = '/'
|
||||
}
|
||||
}
|
||||
if len(out) > 0 {
|
||||
out[len(out)-1].IsLast = true
|
||||
}
|
||||
|
||||
p = routeParser{segs: out, params: params}
|
||||
return
|
||||
}
|
||||
|
||||
// findNextRouteSegmentEnd searches in the route for the next end position for a segment
|
||||
func findNextRouteSegmentEnd(search string) int {
|
||||
nextPosition := -1
|
||||
for _, delimiter := range routeDelimiter {
|
||||
if pos := strings.IndexByte(search, delimiter); pos != -1 && (pos < nextPosition || nextPosition == -1) {
|
||||
nextPosition = pos
|
||||
}
|
||||
}
|
||||
|
||||
return nextPosition
|
||||
}
|
||||
|
||||
// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
|
||||
func (p *routeParser) getMatch(s string, partialCheck bool) ([][2]int, bool) {
|
||||
lenKeys := len(p.params)
|
||||
paramsPositions := getAllocFreeParamsPos(lenKeys)
|
||||
var i, j, paramsIterator, partLen, paramStart int
|
||||
if len(s) > 0 {
|
||||
s = s[1:]
|
||||
paramStart++
|
||||
}
|
||||
for index, segment := range p.segs {
|
||||
partLen = len(s)
|
||||
// check parameter
|
||||
if segment.IsParam {
|
||||
// determine parameter length
|
||||
if segment.Param == wildcardParam {
|
||||
if segment.IsLast {
|
||||
i = partLen
|
||||
} else {
|
||||
i = findWildcardParamLen(s, p.segs, index)
|
||||
}
|
||||
} else {
|
||||
i = strings.IndexByte(s, segment.EndChar)
|
||||
}
|
||||
if i == -1 {
|
||||
i = partLen
|
||||
}
|
||||
|
||||
if !segment.IsOptional && i == 0 {
|
||||
return nil, false
|
||||
// special case for not slash end character
|
||||
} else if i > 0 && partLen >= i && segment.EndChar != '/' && s[i-1] == '/' {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i
|
||||
paramsIterator++
|
||||
} else {
|
||||
// check const segment
|
||||
i = len(segment.Const)
|
||||
if partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const || (partLen > i && s[i] != segment.EndChar) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// reduce founded part from the string
|
||||
if partLen > 0 {
|
||||
j = i + 1
|
||||
if segment.IsLast || partLen < j {
|
||||
j = i
|
||||
}
|
||||
paramStart += j
|
||||
|
||||
s = s[j:]
|
||||
}
|
||||
}
|
||||
if len(s) != 0 && !partialCheck {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return paramsPositions, true
|
||||
}
|
||||
|
||||
// paramsForPos get parameters for the given positions from the given path
|
||||
func (p *routeParser) paramsForPos(path string, paramsPositions [][2]int) []string {
|
||||
size := len(paramsPositions)
|
||||
params := getAllocFreeParams(size)
|
||||
for i, positions := range paramsPositions {
|
||||
if positions[0] != positions[1] && len(path) >= positions[1] {
|
||||
params[i] = path[positions[0]:positions[1]]
|
||||
} else {
|
||||
params[i] = ""
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// findWildcardParamLen for the expressjs wildcard behavior (right to left greedy)
|
||||
// look at the other segments and take what is left for the wildcard from right to left
|
||||
func findWildcardParamLen(s string, segments []paramSeg, currIndex int) int {
|
||||
// "/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"
|
||||
endChar := segments[currIndex].EndChar
|
||||
neededEndChars := 0
|
||||
// count the needed chars for the other segments
|
||||
for i := currIndex + 1; i < len(segments); i++ {
|
||||
if segments[i].EndChar == endChar {
|
||||
neededEndChars++
|
||||
}
|
||||
}
|
||||
// remove the part the other segments still need
|
||||
for {
|
||||
pos := strings.LastIndexByte(s, endChar)
|
||||
if pos != -1 {
|
||||
s = s[:pos]
|
||||
}
|
||||
neededEndChars--
|
||||
if neededEndChars <= 0 || pos == -1 {
|
||||
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)
|
||||
|
||||
// positions parameter that moves further and further to the right and remains atomic over all simultaneous requests
|
||||
// to assign a separate range to each request
|
||||
var startParamList, startParamPosList uint32 = 0, 0
|
||||
|
||||
// getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use
|
||||
func getAllocFreeParamsPos(allocLen int) [][2]int {
|
||||
size := uint32(allocLen)
|
||||
start := atomic.AddUint32(&startParamPosList, size)
|
||||
if (start + 10) >= uint32(len(paramsPosDummy)) {
|
||||
atomic.StoreUint32(&startParamPosList, 0)
|
||||
return getAllocFreeParamsPos(allocLen)
|
||||
}
|
||||
start -= size
|
||||
allocLen += int(start)
|
||||
paramsPositions := paramsPosDummy[start:allocLen:allocLen]
|
||||
return paramsPositions
|
||||
}
|
||||
|
||||
// getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use
|
||||
func getAllocFreeParams(allocLen int) []string {
|
||||
size := uint32(allocLen)
|
||||
start := atomic.AddUint32(&startParamList, size)
|
||||
if (start + 10) >= uint32(len(paramsPosDummy)) {
|
||||
atomic.StoreUint32(&startParamList, 0)
|
||||
return getAllocFreeParams(allocLen)
|
||||
}
|
||||
start -= size
|
||||
allocLen += int(start)
|
||||
params := paramsDummy[start:allocLen:allocLen]
|
||||
return params
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
|
||||
// 📝 Github Repository: https://github.com/gofiber/fiber
|
||||
// 📌 API Documentation: https://docs.gofiber.io
|
||||
|
||||
package fiber
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
utils "github.com/gofiber/utils"
|
||||
)
|
||||
|
||||
// go test -race -run Test_Path_matchParams
|
||||
func Test_Path_matchParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testparams struct {
|
||||
url string
|
||||
params []string
|
||||
match bool
|
||||
partialCheck bool
|
||||
}
|
||||
testCase := func(r string, cases []testparams) {
|
||||
parser := parseRoute(r)
|
||||
for _, c := range cases {
|
||||
paramsPos, match := parser.getMatch(c.url, c.partialCheck)
|
||||
utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||
if match && paramsPos != nil {
|
||||
utils.AssertEqual(t, c.params, parser.paramsForPos(c.url, paramsPos), fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||
} else {
|
||||
utils.AssertEqual(t, true, nil == paramsPos, fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||
}
|
||||
}
|
||||
}
|
||||
testCase("/api/v1/:param/*", []testparams{
|
||||
{url: "/api/v1/entity", params: []string{"entity", ""}, match: true},
|
||||
{url: "/api/v1/entity/", params: []string{"entity", ""}, match: true},
|
||||
{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},
|
||||
{url: "/api/v1/optional", params: []string{"optional"}, match: true},
|
||||
{url: "/api/v", params: nil, match: false},
|
||||
{url: "/api/v2", params: nil, match: false},
|
||||
{url: "/api/xyz", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/*", []testparams{
|
||||
{url: "/api/v1", params: []string{""}, match: true},
|
||||
{url: "/api/v1/", params: []string{""}, match: true},
|
||||
{url: "/api/v1/entity", params: []string{"entity"}, match: true},
|
||||
{url: "/api/v1/entity/1/2", params: []string{"entity/1/2"}, match: true},
|
||||
{url: "/api/v1/Entity/1/2", params: []string{"Entity/1/2"}, match: true},
|
||||
{url: "/api/v", params: nil, match: false},
|
||||
{url: "/api/v2", params: nil, match: false},
|
||||
{url: "/api/abc", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:param", []testparams{
|
||||
{url: "/api/v1/entity", params: []string{"entity"}, match: true},
|
||||
{url: "/api/v1/entity/8728382", params: nil, match: false},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:param-:param2", []testparams{
|
||||
{url: "/api/v1/entity-entity2", params: []string{"entity", "entity2"}, match: true},
|
||||
{url: "/api/v1/entity/8728382", params: nil, match: false},
|
||||
{url: "/api/v1/entity-8728382", params: []string{"entity", "8728382"}, match: true},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:filename.:extension", []testparams{
|
||||
{url: "/api/v1/test.pdf", params: []string{"test", "pdf"}, match: true},
|
||||
{url: "/api/v1/test/pdf", params: nil, match: false},
|
||||
{url: "/api/v1/test-pdf", params: nil, match: false},
|
||||
{url: "/api/v1/test_pdf", params: nil, match: false},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/const", []testparams{
|
||||
{url: "/api/v1/const", params: []string{}, match: true},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
{url: "/api/v1/something", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:param/abc/*", []testparams{
|
||||
{url: "/api/v1/well/abc/wildcard", params: []string{"well", "wildcard"}, match: true},
|
||||
{url: "/api/v1/well/abc/", params: []string{"well", ""}, match: true},
|
||||
{url: "/api/v1/well/abc", params: []string{"well", ""}, match: true},
|
||||
{url: "/api/v1/well/ttt", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/:day/:month?/:year?", []testparams{
|
||||
{url: "/api/1", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1//", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/-/", params: []string{"1", "-", ""}, match: true},
|
||||
{url: "/api/1-", params: []string{"1-", "", ""}, match: true},
|
||||
{url: "/api/1.", params: []string{"1.", "", ""}, match: true},
|
||||
{url: "/api/1/2", params: []string{"1", "2", ""}, match: true},
|
||||
{url: "/api/1/2/3", params: []string{"1", "2", "3"}, match: true},
|
||||
{url: "/api/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/:day.:month?.:year?", []testparams{
|
||||
{url: "/api/1", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/", params: nil, match: false},
|
||||
{url: "/api/1.", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1.2", params: []string{"1", "2", ""}, match: true},
|
||||
{url: "/api/1.2.3", params: []string{"1", "2", "3"}, match: true},
|
||||
{url: "/api/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/:day-:month?-:year?", []testparams{
|
||||
{url: "/api/1", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/", params: nil, match: false},
|
||||
{url: "/api/1-", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1-/", params: nil, match: false},
|
||||
{url: "/api/1-/-", params: nil, match: false},
|
||||
{url: "/api/1-2", params: []string{"1", "2", ""}, match: true},
|
||||
{url: "/api/1-2-3", params: []string{"1", "2", "3"}, match: true},
|
||||
{url: "/api/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/*", []testparams{
|
||||
{url: "/api/", params: []string{""}, match: true},
|
||||
{url: "/api/joker", params: []string{"joker"}, match: true},
|
||||
{url: "/api", params: []string{""}, match: true},
|
||||
{url: "/api/v1/entity", params: []string{"v1/entity"}, match: true},
|
||||
{url: "/api2/v1/entity", params: nil, match: false},
|
||||
{url: "/api_ignore/v1/entity", params: nil, match: false},
|
||||
})
|
||||
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"}, 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},
|
||||
{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/:param2", []testparams{
|
||||
{url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true},
|
||||
{url: "/api/joker/batman", 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},
|
||||
{url: "/api/joker/batman/robin/2/1", params: []string{"joker/batman/robin", "2", "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("/partialCheck/foo/bar/:param", []testparams{
|
||||
{url: "/partialCheck/foo/bar/test", params: []string{"test"}, match: true, partialCheck: true},
|
||||
{url: "/partialCheck/foo/bar/test/test2", params: []string{"test"}, match: true, partialCheck: true},
|
||||
{url: "/partialCheck/foo/bar", params: nil, match: false, partialCheck: true},
|
||||
{url: "/partiaFoo", params: nil, match: false, partialCheck: true},
|
||||
})
|
||||
testCase("/api/*/:param/:param2", []testparams{
|
||||
{url: "/api/test/abc", params: nil, match: false},
|
||||
{url: "/api/joker/batman", 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},
|
||||
{url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true},
|
||||
{url: "/api", params: nil, match: false},
|
||||
})
|
||||
testCase("/", []testparams{
|
||||
{url: "/api", params: nil, match: false},
|
||||
{url: "", params: []string{}, match: true},
|
||||
{url: "/", params: []string{}, match: true},
|
||||
})
|
||||
testCase("/config/abc.json", []testparams{
|
||||
{url: "/config/abc.json", params: []string{}, match: true},
|
||||
{url: "config/abc.json", params: nil, match: false},
|
||||
{url: "/config/efg.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/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},
|
||||
})
|
||||
testCase("/xyz", []testparams{
|
||||
{url: "xyz", params: nil, match: false},
|
||||
{url: "xyz/", params: nil, match: false},
|
||||
})
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////
|
||||
///////////////// BENCHMARKS /////////////////
|
||||
//////////////////////////////////////////////
|
||||
// go test -v -run=^$ -bench=Benchmark_Path_ -benchmem -count=3
|
||||
|
||||
// func Benchmark_Path_paramsForPos(b *testing.B) {
|
||||
// // TODO
|
||||
// }
|
||||
|
||||
// func Benchmark_Path_matchParams(b *testing.B) {
|
||||
// // TODO
|
||||
// }
|
244
utils.go
244
utils.go
|
@ -10,7 +10,6 @@ import (
|
|||
"hash/crc32"
|
||||
"net"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
utils "github.com/gofiber/utils"
|
||||
|
@ -166,249 +165,6 @@ var getBytesImmutable = func(s string) (b []byte) {
|
|||
return []byte(s)
|
||||
}
|
||||
|
||||
//region route registration and matching #-#-#-#-#-#-#-#-#-#-#-#-#-#
|
||||
|
||||
// ⚠️ This path parser was based on urlpath by @ucarion (MIT License).
|
||||
// 💖 Modified for the Fiber router by @renanbastos93 & @renewerner87
|
||||
// 🤖 ucarion/urlpath - renanbastos93/fastpath - renewerner87/fastpath
|
||||
|
||||
// routeParser holds the path segments and param names
|
||||
type routeParser struct {
|
||||
segs []paramSeg
|
||||
params []string
|
||||
}
|
||||
|
||||
// paramsSeg holds the segment metadata
|
||||
type paramSeg struct {
|
||||
Param string
|
||||
Const string
|
||||
IsParam bool
|
||||
IsOptional bool
|
||||
IsLast bool
|
||||
EndChar byte
|
||||
}
|
||||
|
||||
// list of possible parameter and segment delimiter
|
||||
// slash has a special role, unlike the other parameters it must not be interpreted as a parameter
|
||||
var routeDelimiter = []byte{'/', '-', '.'}
|
||||
|
||||
const wildcardParam string = "*"
|
||||
|
||||
// parseRoute analyzes the route and divides it into segments for constant areas and parameters,
|
||||
// this information is needed later when assigning the requests to the declared routes
|
||||
func parseRoute(pattern string) (p routeParser) {
|
||||
var out []paramSeg
|
||||
var params []string
|
||||
|
||||
part, delimiterPos := "", 0
|
||||
for len(pattern) > 0 && delimiterPos != -1 {
|
||||
delimiterPos = findNextRouteSegmentEnd(pattern)
|
||||
if delimiterPos != -1 {
|
||||
part = pattern[:delimiterPos]
|
||||
} else {
|
||||
part = pattern
|
||||
}
|
||||
|
||||
partLen, lastSeg := len(part), len(out)-1
|
||||
if partLen == 0 { // skip empty parts
|
||||
if len(pattern) > 0 {
|
||||
// remove first char
|
||||
pattern = pattern[1:]
|
||||
}
|
||||
continue
|
||||
}
|
||||
// is parameter ?
|
||||
if part[0] == '*' || part[0] == ':' {
|
||||
out = append(out, paramSeg{
|
||||
Param: utils.GetTrimmedParam(part),
|
||||
IsParam: true,
|
||||
IsOptional: part == wildcardParam || part[partLen-1] == '?',
|
||||
})
|
||||
lastSeg = len(out) - 1
|
||||
params = append(params, out[lastSeg].Param)
|
||||
// combine const segments
|
||||
} else if lastSeg >= 0 && !out[lastSeg].IsParam {
|
||||
out[lastSeg].Const += string(out[lastSeg].EndChar) + part
|
||||
// create new const segment
|
||||
} else {
|
||||
out = append(out, paramSeg{
|
||||
Const: part,
|
||||
})
|
||||
lastSeg = len(out) - 1
|
||||
}
|
||||
|
||||
if delimiterPos != -1 && len(pattern) >= delimiterPos+1 {
|
||||
out[lastSeg].EndChar = pattern[delimiterPos]
|
||||
pattern = pattern[delimiterPos+1:]
|
||||
} else {
|
||||
// last default char
|
||||
out[lastSeg].EndChar = '/'
|
||||
}
|
||||
}
|
||||
if len(out) > 0 {
|
||||
out[len(out)-1].IsLast = true
|
||||
}
|
||||
|
||||
p = routeParser{segs: out, params: params}
|
||||
return
|
||||
}
|
||||
|
||||
// findNextRouteSegmentEnd searches in the route for the next end position for a segment
|
||||
func findNextRouteSegmentEnd(search string) int {
|
||||
nextPosition := -1
|
||||
for _, delimiter := range routeDelimiter {
|
||||
if pos := strings.IndexByte(search, delimiter); pos != -1 && (pos < nextPosition || nextPosition == -1) {
|
||||
nextPosition = pos
|
||||
}
|
||||
}
|
||||
|
||||
return nextPosition
|
||||
}
|
||||
|
||||
// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
|
||||
func (p *routeParser) getMatch(s string, partialCheck bool) ([][2]int, bool) {
|
||||
lenKeys := len(p.params)
|
||||
paramsPositions := getAllocFreeParamsPos(lenKeys)
|
||||
var i, j, paramsIterator, partLen, paramStart int
|
||||
if len(s) > 0 {
|
||||
s = s[1:]
|
||||
paramStart++
|
||||
}
|
||||
for index, segment := range p.segs {
|
||||
partLen = len(s)
|
||||
// check parameter
|
||||
if segment.IsParam {
|
||||
// determine parameter length
|
||||
if segment.Param == wildcardParam {
|
||||
if segment.IsLast {
|
||||
i = partLen
|
||||
} else {
|
||||
i = findWildcardParamLen(s, p.segs, index)
|
||||
}
|
||||
} else {
|
||||
i = strings.IndexByte(s, segment.EndChar)
|
||||
}
|
||||
if i == -1 {
|
||||
i = partLen
|
||||
}
|
||||
|
||||
if !segment.IsOptional && i == 0 {
|
||||
return nil, false
|
||||
// special case for not slash end character
|
||||
} else if i > 0 && partLen >= i && segment.EndChar != '/' && s[i-1] == '/' {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
paramsPositions[paramsIterator][0], paramsPositions[paramsIterator][1] = paramStart, paramStart+i
|
||||
paramsIterator++
|
||||
} else {
|
||||
// check const segment
|
||||
i = len(segment.Const)
|
||||
if partLen < i || (i == 0 && partLen > 0) || s[:i] != segment.Const || (partLen > i && s[i] != segment.EndChar) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// reduce founded part from the string
|
||||
if partLen > 0 {
|
||||
j = i + 1
|
||||
if segment.IsLast || partLen < j {
|
||||
j = i
|
||||
}
|
||||
paramStart += j
|
||||
|
||||
s = s[j:]
|
||||
}
|
||||
}
|
||||
if len(s) != 0 && !partialCheck {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return paramsPositions, true
|
||||
}
|
||||
|
||||
// paramsForPos get parameters for the given positions from the given path
|
||||
func (p *routeParser) paramsForPos(path string, paramsPositions [][2]int) []string {
|
||||
size := len(paramsPositions)
|
||||
params := getAllocFreeParams(size)
|
||||
for i, positions := range paramsPositions {
|
||||
if positions[0] != positions[1] && len(path) >= positions[1] {
|
||||
params[i] = path[positions[0]:positions[1]]
|
||||
} else {
|
||||
params[i] = ""
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// findWildcardParamLen for the expressjs wildcard behavior (right to left greedy)
|
||||
// look at the other segments and take what is left for the wildcard from right to left
|
||||
func findWildcardParamLen(s string, segments []paramSeg, currIndex int) int {
|
||||
// "/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"
|
||||
endChar := segments[currIndex].EndChar
|
||||
neededEndChars := 0
|
||||
// count the needed chars for the other segments
|
||||
for i := currIndex + 1; i < len(segments); i++ {
|
||||
if segments[i].EndChar == endChar {
|
||||
neededEndChars++
|
||||
}
|
||||
}
|
||||
// remove the part the other segments still need
|
||||
for {
|
||||
pos := strings.LastIndexByte(s, endChar)
|
||||
if pos != -1 {
|
||||
s = s[:pos]
|
||||
}
|
||||
neededEndChars--
|
||||
if neededEndChars <= 0 || pos == -1 {
|
||||
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)
|
||||
|
||||
// positions parameter that moves further and further to the right and remains atomic over all simultaneous requests
|
||||
// to assign a separate range to each request
|
||||
var startParamList, startParamPosList uint32 = 0, 0
|
||||
|
||||
// getAllocFreeParamsPos fetches a slice area from the predefined slice, which is currently not in use
|
||||
func getAllocFreeParamsPos(allocLen int) [][2]int {
|
||||
size := uint32(allocLen)
|
||||
start := atomic.AddUint32(&startParamPosList, size)
|
||||
if (start + 10) >= uint32(len(paramsPosDummy)) {
|
||||
atomic.StoreUint32(&startParamPosList, 0)
|
||||
return getAllocFreeParamsPos(allocLen)
|
||||
}
|
||||
start -= size
|
||||
allocLen += int(start)
|
||||
paramsPositions := paramsPosDummy[start:allocLen:allocLen]
|
||||
return paramsPositions
|
||||
}
|
||||
|
||||
// getAllocFreeParams fetches a slice area from the predefined slice, which is currently not in use
|
||||
func getAllocFreeParams(allocLen int) []string {
|
||||
size := uint32(allocLen)
|
||||
start := atomic.AddUint32(&startParamList, size)
|
||||
if (start + 10) >= uint32(len(paramsPosDummy)) {
|
||||
atomic.StoreUint32(&startParamList, 0)
|
||||
return getAllocFreeParams(allocLen)
|
||||
}
|
||||
start -= size
|
||||
allocLen += int(start)
|
||||
params := paramsDummy[start:allocLen:allocLen]
|
||||
return params
|
||||
}
|
||||
|
||||
//endregion #-#-#-#-#-#-#-#-#-#-#-#-#-#
|
||||
|
||||
// HTTP methods and their unique INTs
|
||||
var methodINT = map[string]int{
|
||||
MethodGet: 0,
|
||||
|
|
200
utils_test.go
200
utils_test.go
|
@ -5,7 +5,6 @@
|
|||
package fiber
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
utils "github.com/gofiber/utils"
|
||||
|
@ -86,193 +85,6 @@ func Test_Utils_getGroupPath(t *testing.T) {
|
|||
// // TODO
|
||||
// }
|
||||
|
||||
// go test -race -run Test_Utils_matchParams
|
||||
func Test_Utils_matchParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testparams struct {
|
||||
url string
|
||||
params []string
|
||||
match bool
|
||||
partialCheck bool
|
||||
}
|
||||
testCase := func(r string, cases []testparams) {
|
||||
parser := parseRoute(r)
|
||||
for _, c := range cases {
|
||||
paramsPos, match := parser.getMatch(c.url, c.partialCheck)
|
||||
utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||
if match && paramsPos != nil {
|
||||
utils.AssertEqual(t, c.params, parser.paramsForPos(c.url, paramsPos), fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||
} else {
|
||||
utils.AssertEqual(t, true, nil == paramsPos, fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||
}
|
||||
}
|
||||
}
|
||||
testCase("/api/v1/:param/*", []testparams{
|
||||
{url: "/api/v1/entity", params: []string{"entity", ""}, match: true},
|
||||
{url: "/api/v1/entity/", params: []string{"entity", ""}, match: true},
|
||||
{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},
|
||||
{url: "/api/v1/optional", params: []string{"optional"}, match: true},
|
||||
{url: "/api/v", params: nil, match: false},
|
||||
{url: "/api/v2", params: nil, match: false},
|
||||
{url: "/api/xyz", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/*", []testparams{
|
||||
{url: "/api/v1", params: []string{""}, match: true},
|
||||
{url: "/api/v1/", params: []string{""}, match: true},
|
||||
{url: "/api/v1/entity", params: []string{"entity"}, match: true},
|
||||
{url: "/api/v1/entity/1/2", params: []string{"entity/1/2"}, match: true},
|
||||
{url: "/api/v1/Entity/1/2", params: []string{"Entity/1/2"}, match: true},
|
||||
{url: "/api/v", params: nil, match: false},
|
||||
{url: "/api/v2", params: nil, match: false},
|
||||
{url: "/api/abc", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:param", []testparams{
|
||||
{url: "/api/v1/entity", params: []string{"entity"}, match: true},
|
||||
{url: "/api/v1/entity/8728382", params: nil, match: false},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:param-:param2", []testparams{
|
||||
{url: "/api/v1/entity-entity2", params: []string{"entity", "entity2"}, match: true},
|
||||
{url: "/api/v1/entity/8728382", params: nil, match: false},
|
||||
{url: "/api/v1/entity-8728382", params: []string{"entity", "8728382"}, match: true},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:filename.:extension", []testparams{
|
||||
{url: "/api/v1/test.pdf", params: []string{"test", "pdf"}, match: true},
|
||||
{url: "/api/v1/test/pdf", params: nil, match: false},
|
||||
{url: "/api/v1/test-pdf", params: nil, match: false},
|
||||
{url: "/api/v1/test_pdf", params: nil, match: false},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/const", []testparams{
|
||||
{url: "/api/v1/const", params: []string{}, match: true},
|
||||
{url: "/api/v1", params: nil, match: false},
|
||||
{url: "/api/v1/", params: nil, match: false},
|
||||
{url: "/api/v1/something", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/v1/:param/abc/*", []testparams{
|
||||
{url: "/api/v1/well/abc/wildcard", params: []string{"well", "wildcard"}, match: true},
|
||||
{url: "/api/v1/well/abc/", params: []string{"well", ""}, match: true},
|
||||
{url: "/api/v1/well/abc", params: []string{"well", ""}, match: true},
|
||||
{url: "/api/v1/well/ttt", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/:day/:month?/:year?", []testparams{
|
||||
{url: "/api/1", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1//", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/-/", params: []string{"1", "-", ""}, match: true},
|
||||
{url: "/api/1-", params: []string{"1-", "", ""}, match: true},
|
||||
{url: "/api/1.", params: []string{"1.", "", ""}, match: true},
|
||||
{url: "/api/1/2", params: []string{"1", "2", ""}, match: true},
|
||||
{url: "/api/1/2/3", params: []string{"1", "2", "3"}, match: true},
|
||||
{url: "/api/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/:day.:month?.:year?", []testparams{
|
||||
{url: "/api/1", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/", params: nil, match: false},
|
||||
{url: "/api/1.", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1.2", params: []string{"1", "2", ""}, match: true},
|
||||
{url: "/api/1.2.3", params: []string{"1", "2", "3"}, match: true},
|
||||
{url: "/api/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/:day-:month?-:year?", []testparams{
|
||||
{url: "/api/1", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1/", params: nil, match: false},
|
||||
{url: "/api/1-", params: []string{"1", "", ""}, match: true},
|
||||
{url: "/api/1-/", params: nil, match: false},
|
||||
{url: "/api/1-/-", params: nil, match: false},
|
||||
{url: "/api/1-2", params: []string{"1", "2", ""}, match: true},
|
||||
{url: "/api/1-2-3", params: []string{"1", "2", "3"}, match: true},
|
||||
{url: "/api/", params: nil, match: false},
|
||||
})
|
||||
testCase("/api/*", []testparams{
|
||||
{url: "/api/", params: []string{""}, match: true},
|
||||
{url: "/api/joker", params: []string{"joker"}, match: true},
|
||||
{url: "/api", params: []string{""}, match: true},
|
||||
{url: "/api/v1/entity", params: []string{"v1/entity"}, match: true},
|
||||
{url: "/api2/v1/entity", params: nil, match: false},
|
||||
{url: "/api_ignore/v1/entity", params: nil, match: false},
|
||||
})
|
||||
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"}, 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},
|
||||
{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/:param2", []testparams{
|
||||
{url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true},
|
||||
{url: "/api/joker/batman", 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},
|
||||
{url: "/api/joker/batman/robin/2/1", params: []string{"joker/batman/robin", "2", "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("/partialCheck/foo/bar/:param", []testparams{
|
||||
{url: "/partialCheck/foo/bar/test", params: []string{"test"}, match: true, partialCheck: true},
|
||||
{url: "/partialCheck/foo/bar/test/test2", params: []string{"test"}, match: true, partialCheck: true},
|
||||
{url: "/partialCheck/foo/bar", params: nil, match: false, partialCheck: true},
|
||||
{url: "/partiaFoo", params: nil, match: false, partialCheck: true},
|
||||
})
|
||||
testCase("/api/*/:param/:param2", []testparams{
|
||||
{url: "/api/test/abc", params: nil, match: false},
|
||||
{url: "/api/joker/batman", 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},
|
||||
{url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true},
|
||||
{url: "/api", params: nil, match: false},
|
||||
})
|
||||
testCase("/", []testparams{
|
||||
{url: "/api", params: nil, match: false},
|
||||
{url: "", params: []string{}, match: true},
|
||||
{url: "/", params: []string{}, match: true},
|
||||
})
|
||||
testCase("/config/abc.json", []testparams{
|
||||
{url: "/config/abc.json", params: []string{}, match: true},
|
||||
{url: "config/abc.json", params: nil, match: false},
|
||||
{url: "/config/efg.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/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},
|
||||
})
|
||||
testCase("/xyz", []testparams{
|
||||
{url: "xyz", params: nil, match: false},
|
||||
{url: "xyz/", params: nil, match: false},
|
||||
})
|
||||
}
|
||||
|
||||
// func Test_Utils_getTrimmedParam(t *testing.T) {
|
||||
// // TODO
|
||||
// }
|
||||
|
@ -304,15 +116,3 @@ func Benchmark_Utils_getGroupPath(b *testing.B) {
|
|||
// func Benchmark_Utils_parseTokenList(b *testing.B) {
|
||||
// // TODO
|
||||
// }
|
||||
|
||||
// func Benchmark_Utils_getParams(b *testing.B) {
|
||||
// // TODO
|
||||
// }
|
||||
|
||||
// func Benchmark_Utils_matchParams(b *testing.B) {
|
||||
// // TODO
|
||||
// }
|
||||
|
||||
// func Benchmark_Utils_getCharPos(b *testing.B) {
|
||||
// // TODO
|
||||
// }
|
||||
|
|
Loading…
Reference in New Issue