Skip to content

Commit

Permalink
Support optional parameters (#37)
Browse files Browse the repository at this point in the history
* support optional parameters

* add tests

* update readme
  • Loading branch information
abahmed committed Jun 14, 2020
1 parent 8063505 commit c9f2fad
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 26 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ func main() {
fmt.Printf("%s\n", ctx.Params.GetString("user"))
})

// Handler with optional parameter
gb.Get("/search/:pattern?", func(ctx *gearbox.Context) {
fmt.Printf("%s\n", ctx.Params.GetString("pattern"))
})

// Handler with regex parameter
gb.Get("/book/:name:([a-z]+[0-3])", func(ctx *gearbox.Context) {
fmt.Printf("%s\n", ctx.Params.GetString("name"))
Expand Down
70 changes: 44 additions & 26 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ const (
)

type param struct {
Name []byte
Value string
Type paramType
Name []byte
Value string
Type paramType
IsOptional bool
}

type endpoint struct {
Expand All @@ -40,9 +41,9 @@ type routerFallback struct {
}

type matchParamsResult struct {
matched bool
handlers handlersChain
params tst
Matched bool
Handlers handlersChain
Params tst
}

// validateRoutePath makes sure that path complies with path's rules
Expand All @@ -59,21 +60,22 @@ func validateRoutePath(path []byte) error {
}

params := newTST()
parts := bytes.Split(path, []byte("/"))
parts := bytes.Split(trimPath(path), []byte("/"))
partsLen := len(parts)
for i := 1; i < partsLen; i++ {
for i := 0; i < partsLen; i++ {
if len(parts[i]) == 0 {
continue
}

if p := parseParameter(parts[i]); p != nil {
if p.Type == ptParam || p.Type == ptRegexp {
if p.Type == ptMatchAll && i != partsLen-1 {
return fmt.Errorf("* must be in the end of path")
} else if p.IsOptional && i != partsLen-1 {
return fmt.Errorf("only last parameter can be optional")
} else if p.Type == ptParam || p.Type == ptRegexp {
if pName := params.Get(p.Name); pName != nil {
return fmt.Errorf("parameter is duplicated")
}
params.Set(p.Name, true)
} else if p.Type == ptMatchAll && i != partsLen-1 {
return fmt.Errorf("* must be in the end of path")
}
}
}
Expand Down Expand Up @@ -129,7 +131,8 @@ func createEmptyRouteNode(name []byte) *routeNode {
// parseParameter parses path part into param struct, or returns nil if it's
// not a parameter
func parseParameter(pathPart []byte) *param {
if len(pathPart) == 0 {
pathPartLen := len(pathPart)
if pathPartLen == 0 {
return nil
}

Expand All @@ -141,19 +144,26 @@ func parseParameter(pathPart []byte) *param {
}
}

isOptional := pathPart[pathPartLen-1] == '?'
if isOptional {
pathPart = pathPart[0 : pathPartLen-1]
}

params := bytes.Split(pathPart, []byte(":"))
paramsLen := len(params)

if paramsLen == 2 && len(params[0]) == 0 { // Just a parameter
return &param{
Name: params[1],
Type: ptParam,
Name: params[1],
Type: ptParam,
IsOptional: isOptional,
}
} else if paramsLen == 3 && len(params[0]) == 0 { // Regex parameter
return &param{
Name: params[1],
Value: string(params[2]),
Type: ptRegexp,
Name: params[1],
Value: string(params[2]),
Type: ptRegexp,
IsOptional: isOptional,
}
}

Expand Down Expand Up @@ -310,17 +320,25 @@ func matchEndpointParams(ep *endpoint, paths [][]byte, pathIndex int) (tst, bool
endpointParamsLen := len(endpointParams)
pathsLen := len(paths)

//matched := false
paramIdx := 0
for paramIdx < endpointParamsLen {
if endpointParams[paramIdx].Type == ptMatchAll {
// Last parameter, so we can return
return paramDic, true
}

// path has ended and there is more parameters to match
if pathIndex >= pathsLen {
// If it's optional means this is the last parameter.
if endpointParams[paramIdx].IsOptional {
return paramDic, true
}

return nil, false
}

//
if len(paths[pathIndex]) == 0 {
pathIndex++
continue
Expand All @@ -331,7 +349,7 @@ func matchEndpointParams(ep *endpoint, paths [][]byte, pathIndex int) (tst, bool
} else if endpointParams[paramIdx].Type == ptRegexp {
if match, _ := regexp.Match(endpointParams[paramIdx].Value, paths[pathIndex]); match {
paramDic.Set(endpointParams[paramIdx].Name, paths[pathIndex])
} else {
} else if !endpointParams[paramIdx].IsOptional {
return nil, false
}
}
Expand All @@ -353,16 +371,16 @@ func matchNodeEndpoints(node *routeNode, method []byte, paths [][]byte,
if endpoints, ok := node.Endpoints.Get(method).([]*endpoint); ok {
for j := range endpoints {
if params, matched := matchEndpointParams(endpoints[j], paths, pathIndex); matched {
result.matched = true
result.params = params
result.handlers = endpoints[j].Handlers
result.Matched = true
result.Params = params
result.Handlers = endpoints[j].Handlers
wg.Done()
return
}
}
}

result.matched = false
result.Matched = false
wg.Done()
}

Expand All @@ -384,7 +402,7 @@ func (gb *gearbox) matchRouteAgainstRegistered(method, path []byte) (handlersCha
// Try to get from cache
cacheKey := append(method, trimmedPath...)
if cacheResult, ok := gb.cache.Get(cacheKey).(*matchParamsResult); ok {
return cacheResult.handlers, cacheResult.params
return cacheResult.Handlers, cacheResult.Params
}

paths := bytes.Split(trimmedPath, []byte("/"))
Expand Down Expand Up @@ -420,12 +438,12 @@ func (gb *gearbox) matchRouteAgainstRegistered(method, path []byte) (handlersCha

// Return longest prefix match
for i := lastMatchedNodesIndex - 1; i >= 0; i-- {
if lastMatchedNodes[i].matched {
if lastMatchedNodes[i].Matched {
go func(key []byte, matchResult *matchParamsResult) {
gb.cache.Set(key, matchResult)
}(append(make([]byte, 0, len(cacheKey)), cacheKey...), lastMatchedNodes[i])

return lastMatchedNodes[i].handlers, lastMatchedNodes[i].params
return lastMatchedNodes[i].Handlers, lastMatchedNodes[i].Params
}
}

Expand Down
12 changes: 12 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ func TestConstructRoutingTree(t *testing.T) {
{method: []byte(MethodGet), path: []byte("/books/search/:pattern"), handler: emptyHandlersChain},
{method: []byte(MethodGet), path: []byte("/books/search/:pattern1/:pattern2/:pattern3"), handler: emptyHandlersChain},
{method: []byte(MethodGet), path: []byte("/books/search/*"), handler: emptyHandlersChain},
{method: []byte(MethodGet), path: []byte("/account/:name?"), handler: emptyHandlersChain},
{method: []byte(MethodGet), path: []byte("/profile/:name:([a-z]+)?"), handler: emptyHandlersChain},
{method: []byte(MethodGet), path: []byte("/order/:name1/:name2:([a-z]+)?"), handler: emptyHandlersChain},
}

// register routes
Expand Down Expand Up @@ -333,6 +336,15 @@ func TestConstructRoutingTree(t *testing.T) {
{method: []byte(MethodGet), path: []byte("/books/search/123"), match: true, params: map[string]string{"pattern": "123"}},
{method: []byte(MethodGet), path: []byte("/books/search/test1/test2/test3"), match: true, params: map[string]string{"pattern1": "test1", "pattern2": "test2", "pattern3": "test3"}},
{method: []byte(MethodGet), path: []byte("/books/search/test/test2"), match: true, params: make(map[string]string)},
{method: []byte(MethodGet), path: []byte("/books/search/test/test2"), match: true, params: make(map[string]string)},
{method: []byte(MethodGet), path: []byte("/account/testuser"), match: true, params: map[string]string{"name": "testuser"}},
{method: []byte(MethodGet), path: []byte("/account"), match: true, params: make(map[string]string)},
{method: []byte(MethodPut), path: []byte("/account/test1/test2"), match: false, params: make(map[string]string)},
{method: []byte(MethodGet), path: []byte("/profile/testuser"), match: true, params: map[string]string{"name": "testuser"}},
{method: []byte(MethodGet), path: []byte("/profile"), match: true, params: make(map[string]string)},
{method: []byte(MethodGet), path: []byte("/order/test1"), match: true, params: map[string]string{"name1": "test1"}},
{method: []byte(MethodGet), path: []byte("/order/test1/test2/"), match: true, params: map[string]string{"name1": "test1", "name2": "test2"}},
{method: []byte(MethodPut), path: []byte("/order/test1/test2/test3"), match: false, params: make(map[string]string)},
}

// test matching routes
Expand Down

0 comments on commit c9f2fad

Please sign in to comment.