initial import

This commit is contained in:
kevin
2020-07-26 17:09:05 +08:00
commit 7e3a369a8f
647 changed files with 54754 additions and 0 deletions

199
core/search/searchtree.go Normal file
View 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),
},
}
}

View 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)
}
}
}

View 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)
}