util/http: add trie matching func
Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
parent
dd29bf457e
commit
d0f2bc8346
201
util/http/trie.go
Normal file
201
util/http/trie.go
Normal file
@ -0,0 +1,201 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Tree is a trie tree.
|
||||
type Trie struct {
|
||||
rmu sync.RWMutex
|
||||
node *node
|
||||
rcache map[string]*regexp.Regexp
|
||||
}
|
||||
|
||||
// node is a node of tree
|
||||
type node struct {
|
||||
actions map[string]interface{} // key is method, val is handler interface
|
||||
children map[string]*node // key is label of next nodes
|
||||
label string
|
||||
}
|
||||
|
||||
const (
|
||||
pathRoot string = "/"
|
||||
pathDelimiter string = "/"
|
||||
paramDelimiter string = ":"
|
||||
leftPtnDelimiter string = "{"
|
||||
rightPtnDelimiter string = "}"
|
||||
ptnWildcard string = "(.+)"
|
||||
)
|
||||
|
||||
// NewTree creates a new trie tree.
|
||||
func NewTrie() *Trie {
|
||||
return &Trie{
|
||||
node: &node{
|
||||
label: pathRoot,
|
||||
actions: make(map[string]interface{}),
|
||||
children: make(map[string]*node),
|
||||
},
|
||||
rcache: make(map[string]*regexp.Regexp),
|
||||
}
|
||||
}
|
||||
|
||||
// Insert inserts a route definition to tree.
|
||||
func (t *Trie) Insert(methods []string, path string, handler interface{}) {
|
||||
curNode := t.node
|
||||
if path == pathRoot {
|
||||
curNode.label = path
|
||||
for _, method := range methods {
|
||||
curNode.actions[method] = handler
|
||||
}
|
||||
return
|
||||
}
|
||||
ep := splitPath(path)
|
||||
for i, p := range ep {
|
||||
nextNode, ok := curNode.children[p]
|
||||
if ok {
|
||||
curNode = nextNode
|
||||
}
|
||||
// Create a new node.
|
||||
if !ok {
|
||||
curNode.children[p] = &node{
|
||||
label: p,
|
||||
actions: make(map[string]interface{}),
|
||||
children: make(map[string]*node),
|
||||
}
|
||||
curNode = curNode.children[p]
|
||||
}
|
||||
// last loop.
|
||||
// If there is already registered data, overwrite it.
|
||||
if i == len(ep)-1 {
|
||||
curNode.label = p
|
||||
for _, method := range methods {
|
||||
curNode.actions[method] = handler
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search searches a path from a tree.
|
||||
func (t *Trie) Search(method string, path string) (interface{}, map[string]string, bool) {
|
||||
params := make(map[string]string)
|
||||
|
||||
curNode := t.node
|
||||
for _, p := range splitPath(path) {
|
||||
nextNode, ok := curNode.children[p]
|
||||
if ok {
|
||||
curNode = nextNode
|
||||
continue
|
||||
}
|
||||
if len(curNode.children) == 0 {
|
||||
if curNode.label != p {
|
||||
// no matching path was found.
|
||||
return nil, nil, false
|
||||
}
|
||||
break
|
||||
}
|
||||
isParamMatch := false
|
||||
for c := range curNode.children {
|
||||
if string([]rune(c)[0]) == leftPtnDelimiter {
|
||||
ptn := getPattern(c)
|
||||
t.rmu.RLock()
|
||||
reg, ok := t.rcache[ptn]
|
||||
t.rmu.RUnlock()
|
||||
if !ok {
|
||||
var err error
|
||||
reg, err = regexp.Compile(ptn)
|
||||
if err != nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
t.rmu.Lock()
|
||||
t.rcache[ptn] = reg
|
||||
t.rmu.Unlock()
|
||||
}
|
||||
if reg.Match([]byte(p)) {
|
||||
pn := getParamName(c)
|
||||
params[pn] = p
|
||||
curNode = curNode.children[c]
|
||||
isParamMatch = true
|
||||
break
|
||||
}
|
||||
// no matching param was found.
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
if !isParamMatch {
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
if path == pathRoot {
|
||||
if len(curNode.actions) == 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
|
||||
handler, ok := curNode.actions[method]
|
||||
if !ok || handler == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
return handler, params, true
|
||||
}
|
||||
|
||||
// getPattern gets a pattern from a label
|
||||
// {id:[^\d+$]} -> ^\d+$
|
||||
// {id} -> (.+)
|
||||
func getPattern(label string) string {
|
||||
leftI := strings.Index(label, leftPtnDelimiter)
|
||||
rightI := strings.Index(label, paramDelimiter)
|
||||
// if label doesn't have any pattern, return wild card pattern as default.
|
||||
if leftI == -1 || rightI == -1 {
|
||||
return ptnWildcard
|
||||
}
|
||||
return label[rightI+1 : len(label)-1]
|
||||
}
|
||||
|
||||
// getParamName gets a parameter from a label
|
||||
// {id:[^\d+$]} -> id
|
||||
// {id} -> id
|
||||
func getParamName(label string) string {
|
||||
leftI := strings.Index(label, leftPtnDelimiter)
|
||||
rightI := func(l string) int {
|
||||
r := []rune(l)
|
||||
|
||||
var n int
|
||||
|
||||
loop:
|
||||
for i := 0; i < len(r); i++ {
|
||||
n = i
|
||||
switch string(r[i]) {
|
||||
case paramDelimiter:
|
||||
n = i
|
||||
break loop
|
||||
case rightPtnDelimiter:
|
||||
n = i
|
||||
break loop
|
||||
}
|
||||
|
||||
if i == len(r)-1 {
|
||||
n = i + 1
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}(label)
|
||||
|
||||
return label[leftI+1 : rightI]
|
||||
}
|
||||
|
||||
// splitPath removes an empty value in slice.
|
||||
func splitPath(path string) []string {
|
||||
s := strings.Split(path, pathDelimiter)
|
||||
var r []string
|
||||
for _, str := range s {
|
||||
if str != "" {
|
||||
r = append(r, str)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
42
util/http/trie_test.go
Normal file
42
util/http/trie_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTrieNoMatchMethod(t *testing.T) {
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{id}", nil)
|
||||
|
||||
_, _, ok := tr.Search(http.MethodPost, "/v1/create")
|
||||
if ok {
|
||||
t.Fatalf("must be not found error")
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct{}
|
||||
|
||||
func TestTrieMatchRegexp(t *testing.T) {
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{category}/{id:[0-9]+}", &handler{})
|
||||
|
||||
_, params, ok := tr.Search(http.MethodPut, "/v1/create/test_cat/12345")
|
||||
if !ok {
|
||||
t.Fatalf("route not found")
|
||||
} else if len(params) != 2 {
|
||||
t.Fatalf("param matching error %v", params)
|
||||
} else if params["category"] != "test_cat" {
|
||||
t.Fatalf("param matching error %v", params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrieMatchRegexpFail(t *testing.T) {
|
||||
tr := NewTrie()
|
||||
tr.Insert([]string{http.MethodPut}, "/v1/create/{id:[a-z]+}", &handler{})
|
||||
|
||||
_, _, ok := tr.Search(http.MethodPut, "/v1/create/12345")
|
||||
if ok {
|
||||
t.Fatalf("route must not be not found")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user