initial import
This commit is contained in:
199
core/search/searchtree.go
Normal file
199
core/search/searchtree.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package search
|
||||
|
||||
import "errors"
|
||||
|
||||
const (
|
||||
colon = ':'
|
||||
slash = '/'
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDupItem = errors.New("duplicated item")
|
||||
ErrDupSlash = errors.New("duplicated slash")
|
||||
ErrEmptyItem = errors.New("empty item")
|
||||
ErrInvalidState = errors.New("search tree is in an invalid state")
|
||||
ErrNotFromRoot = errors.New("path should start with /")
|
||||
|
||||
NotFound Result
|
||||
)
|
||||
|
||||
type (
|
||||
innerResult struct {
|
||||
key string
|
||||
value string
|
||||
named bool
|
||||
found bool
|
||||
}
|
||||
|
||||
node struct {
|
||||
item interface{}
|
||||
children [2]map[string]*node
|
||||
}
|
||||
|
||||
Tree struct {
|
||||
root *node
|
||||
}
|
||||
|
||||
Result struct {
|
||||
Item interface{}
|
||||
Params map[string]string
|
||||
}
|
||||
)
|
||||
|
||||
func NewTree() *Tree {
|
||||
return &Tree{
|
||||
root: newNode(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tree) Add(route string, item interface{}) error {
|
||||
if len(route) == 0 || route[0] != slash {
|
||||
return ErrNotFromRoot
|
||||
}
|
||||
|
||||
if item == nil {
|
||||
return ErrEmptyItem
|
||||
}
|
||||
|
||||
return add(t.root, route[1:], item)
|
||||
}
|
||||
|
||||
func (t *Tree) Search(route string) (Result, bool) {
|
||||
if len(route) == 0 || route[0] != slash {
|
||||
return NotFound, false
|
||||
}
|
||||
|
||||
var result Result
|
||||
ok := t.next(t.root, route[1:], &result)
|
||||
return result, ok
|
||||
}
|
||||
|
||||
func (t *Tree) next(n *node, route string, result *Result) bool {
|
||||
if len(route) == 0 && n.item != nil {
|
||||
result.Item = n.item
|
||||
return true
|
||||
}
|
||||
|
||||
for i := range route {
|
||||
if route[i] == slash {
|
||||
token := route[:i]
|
||||
for _, children := range n.children {
|
||||
for k, v := range children {
|
||||
if r := match(k, token); r.found {
|
||||
if t.next(v, route[i+1:], result) {
|
||||
if r.named {
|
||||
addParam(result, r.key, r.value)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, children := range n.children {
|
||||
for k, v := range children {
|
||||
if r := match(k, route); r.found && v.item != nil {
|
||||
result.Item = v.item
|
||||
if r.named {
|
||||
addParam(result, r.key, r.value)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (nd *node) getChildren(route string) map[string]*node {
|
||||
if len(route) > 0 && route[0] == colon {
|
||||
return nd.children[1]
|
||||
} else {
|
||||
return nd.children[0]
|
||||
}
|
||||
}
|
||||
|
||||
func add(nd *node, route string, item interface{}) error {
|
||||
if len(route) == 0 {
|
||||
if nd.item != nil {
|
||||
return ErrDupItem
|
||||
}
|
||||
|
||||
nd.item = item
|
||||
return nil
|
||||
}
|
||||
|
||||
if route[0] == slash {
|
||||
return ErrDupSlash
|
||||
}
|
||||
|
||||
for i := range route {
|
||||
if route[i] == slash {
|
||||
token := route[:i]
|
||||
children := nd.getChildren(token)
|
||||
if child, ok := children[token]; ok {
|
||||
if child != nil {
|
||||
return add(child, route[i+1:], item)
|
||||
} else {
|
||||
return ErrInvalidState
|
||||
}
|
||||
} else {
|
||||
child := newNode(nil)
|
||||
children[token] = child
|
||||
return add(child, route[i+1:], item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
children := nd.getChildren(route)
|
||||
if child, ok := children[route]; ok {
|
||||
if child.item != nil {
|
||||
return ErrDupItem
|
||||
}
|
||||
|
||||
child.item = item
|
||||
} else {
|
||||
children[route] = newNode(item)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addParam(result *Result, k, v string) {
|
||||
if result.Params == nil {
|
||||
result.Params = make(map[string]string)
|
||||
}
|
||||
|
||||
result.Params[k] = v
|
||||
}
|
||||
|
||||
func match(pat, token string) innerResult {
|
||||
if pat[0] == colon {
|
||||
return innerResult{
|
||||
key: pat[1:],
|
||||
value: token,
|
||||
named: true,
|
||||
found: true,
|
||||
}
|
||||
}
|
||||
|
||||
return innerResult{
|
||||
found: pat == token,
|
||||
}
|
||||
}
|
||||
|
||||
func newNode(item interface{}) *node {
|
||||
return &node{
|
||||
item: item,
|
||||
children: [2]map[string]*node{
|
||||
make(map[string]*node),
|
||||
make(map[string]*node),
|
||||
},
|
||||
}
|
||||
}
|
||||
32
core/search/searchtree_debug.go
Normal file
32
core/search/searchtree_debug.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// +build debug
|
||||
|
||||
package search
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (t *Tree) Print() {
|
||||
if t.root.item == nil {
|
||||
fmt.Println("/")
|
||||
} else {
|
||||
fmt.Printf("/:%#v\n", t.root.item)
|
||||
}
|
||||
printNode(t.root, 1)
|
||||
}
|
||||
|
||||
func printNode(n *node, depth int) {
|
||||
indent := make([]byte, depth)
|
||||
for i := 0; i < len(indent); i++ {
|
||||
indent[i] = '\t'
|
||||
}
|
||||
|
||||
for _, children := range n.children {
|
||||
for k, v := range children {
|
||||
if v.item == nil {
|
||||
fmt.Printf("%s%s\n", string(indent), k)
|
||||
} else {
|
||||
fmt.Printf("%s%s:%#v\n", string(indent), k, v.item)
|
||||
}
|
||||
printNode(v, depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
187
core/search/searchtree_test.go
Normal file
187
core/search/searchtree_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockedRoute struct {
|
||||
route string
|
||||
value int
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
routes := []mockedRoute{
|
||||
{"/", 1},
|
||||
{"/api", 2},
|
||||
{"/img", 3},
|
||||
{"/:layer1", 4},
|
||||
{"/api/users", 5},
|
||||
{"/img/jpgs", 6},
|
||||
{"/img/jpgs", 7},
|
||||
{"/api/:layer2", 8},
|
||||
{"/:layer1/:layer2", 9},
|
||||
{"/:layer1/:layer2/users", 10},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
query string
|
||||
expect int
|
||||
params map[string]string
|
||||
contains bool
|
||||
}{
|
||||
{
|
||||
query: "",
|
||||
contains: false,
|
||||
},
|
||||
{
|
||||
query: "/",
|
||||
expect: 1,
|
||||
contains: true,
|
||||
},
|
||||
{
|
||||
query: "/wildcard",
|
||||
expect: 4,
|
||||
params: map[string]string{
|
||||
"layer1": "wildcard",
|
||||
},
|
||||
contains: true,
|
||||
},
|
||||
{
|
||||
query: "/wildcard/",
|
||||
expect: 4,
|
||||
params: map[string]string{
|
||||
"layer1": "wildcard",
|
||||
},
|
||||
contains: true,
|
||||
},
|
||||
{
|
||||
query: "/a/b/c",
|
||||
contains: false,
|
||||
},
|
||||
{
|
||||
query: "/a/b",
|
||||
expect: 9,
|
||||
params: map[string]string{
|
||||
"layer1": "a",
|
||||
"layer2": "b",
|
||||
},
|
||||
contains: true,
|
||||
},
|
||||
{
|
||||
query: "/a/b/",
|
||||
expect: 9,
|
||||
params: map[string]string{
|
||||
"layer1": "a",
|
||||
"layer2": "b",
|
||||
},
|
||||
contains: true,
|
||||
},
|
||||
{
|
||||
query: "/a/b/users",
|
||||
expect: 10,
|
||||
params: map[string]string{
|
||||
"layer1": "a",
|
||||
"layer2": "b",
|
||||
},
|
||||
contains: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.query, func(t *testing.T) {
|
||||
tree := NewTree()
|
||||
for _, r := range routes {
|
||||
tree.Add(r.route, r.value)
|
||||
}
|
||||
result, ok := tree.Search(test.query)
|
||||
assert.Equal(t, test.contains, ok)
|
||||
if ok {
|
||||
actual := result.Item.(int)
|
||||
assert.EqualValues(t, test.params, result.Params)
|
||||
assert.Equal(t, test.expect, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictSearch(t *testing.T) {
|
||||
routes := []mockedRoute{
|
||||
{"/api/users", 1},
|
||||
{"/api/:layer", 2},
|
||||
}
|
||||
query := "/api/users"
|
||||
|
||||
tree := NewTree()
|
||||
for _, r := range routes {
|
||||
tree.Add(r.route, r.value)
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
result, ok := tree.Search(query)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 1, result.Item.(int))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictSearchSibling(t *testing.T) {
|
||||
routes := []mockedRoute{
|
||||
{"/api/:user/profile/name", 1},
|
||||
{"/api/:user/profile", 2},
|
||||
{"/api/:user/name", 3},
|
||||
{"/api/:layer", 4},
|
||||
}
|
||||
query := "/api/123/name"
|
||||
|
||||
tree := NewTree()
|
||||
for _, r := range routes {
|
||||
tree.Add(r.route, r.value)
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
result, ok := tree.Search(query)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 3, result.Item.(int))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddDuplicate(t *testing.T) {
|
||||
tree := NewTree()
|
||||
err := tree.Add("/a/b", 1)
|
||||
assert.Nil(t, err)
|
||||
err = tree.Add("/a/b", 2)
|
||||
assert.Equal(t, ErrDupItem, err)
|
||||
err = tree.Add("/a/b/", 2)
|
||||
assert.Equal(t, ErrDupItem, err)
|
||||
}
|
||||
|
||||
func TestPlain(t *testing.T) {
|
||||
tree := NewTree()
|
||||
err := tree.Add("/a/b", 1)
|
||||
assert.Nil(t, err)
|
||||
err = tree.Add("/a/c", 2)
|
||||
assert.Nil(t, err)
|
||||
_, ok := tree.Search("/a/d")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestSearchWithDoubleSlashes(t *testing.T) {
|
||||
tree := NewTree()
|
||||
err := tree.Add("//a", 1)
|
||||
assert.Error(t, ErrDupSlash, err)
|
||||
}
|
||||
|
||||
func TestSearchInvalidRoute(t *testing.T) {
|
||||
tree := NewTree()
|
||||
err := tree.Add("", 1)
|
||||
assert.Equal(t, ErrNotFromRoot, err)
|
||||
err = tree.Add("bad", 1)
|
||||
assert.Equal(t, ErrNotFromRoot, err)
|
||||
}
|
||||
|
||||
func TestSearchInvalidItem(t *testing.T) {
|
||||
tree := NewTree()
|
||||
err := tree.Add("/", nil)
|
||||
assert.Equal(t, ErrEmptyItem, err)
|
||||
}
|
||||
Reference in New Issue
Block a user