You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
3.7 KiB
193 lines
3.7 KiB
1 year ago
|
package search
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
)
|
||
|
|
||
|
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 any
|
||
|
children [2]map[string]*node
|
||
|
}
|
||
|
TreeNode struct {
|
||
|
root *node
|
||
|
}
|
||
|
Result struct {
|
||
|
Item any
|
||
|
Params map[string]string
|
||
|
}
|
||
|
)
|
||
|
|
||
|
func NewTree() *TreeNode {
|
||
|
return &TreeNode{
|
||
|
root: newNode(nil),
|
||
|
}
|
||
|
}
|
||
|
func (t *TreeNode) Add(route string, item any) error {
|
||
|
if len(route) == 0 || route[0] != slash {
|
||
|
return errNotFromRoot
|
||
|
}
|
||
|
if item == nil {
|
||
|
return errEmptyItem
|
||
|
}
|
||
|
err := add(t.root, route[1:], item)
|
||
|
switch err {
|
||
|
case errDupItem:
|
||
|
return duplicatedItem(route)
|
||
|
case errDupSlash:
|
||
|
return duplicatedSlash(route)
|
||
|
default:
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
func (t *TreeNode) 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 *TreeNode) 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 {
|
||
|
continue
|
||
|
}
|
||
|
token := route[:i]
|
||
|
return n.foreach(func(k string, v *node) bool {
|
||
|
r := match(k, token)
|
||
|
if !r.found || !t.next(v, route[i+1:], result) {
|
||
|
return false
|
||
|
}
|
||
|
if r.named {
|
||
|
addParam(result, r.key, r.value)
|
||
|
}
|
||
|
return true
|
||
|
})
|
||
|
}
|
||
|
return n.foreach(func(k string, v *node) bool {
|
||
|
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) foreach(fn func(string, *node) bool) bool {
|
||
|
for _, children := range nd.children {
|
||
|
for k, v := range children {
|
||
|
if fn(k, v) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func add(nd *node, route string, item any) 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 {
|
||
|
continue
|
||
|
}
|
||
|
token := route[:i]
|
||
|
children := nd.getChildren(token)
|
||
|
if child, ok := children[token]; ok {
|
||
|
if child != nil {
|
||
|
return add(child, route[i+1:], item)
|
||
|
}
|
||
|
return errInvalidState
|
||
|
}
|
||
|
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 (nd *node) getChildren(route string) map[string]*node {
|
||
|
if len(route) > 0 && route[0] == colon {
|
||
|
return nd.children[1]
|
||
|
}
|
||
|
return nd.children[0]
|
||
|
}
|
||
|
func duplicatedItem(item string) error {
|
||
|
return fmt.Errorf("duplicated item for %s", item)
|
||
|
}
|
||
|
func duplicatedSlash(item string) error {
|
||
|
return fmt.Errorf("duplicated slash for %s", item)
|
||
|
}
|
||
|
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 any) *node {
|
||
|
return &node{
|
||
|
item: item,
|
||
|
children: [2]map[string]*node{
|
||
|
make(map[string]*node),
|
||
|
make(map[string]*node),
|
||
|
},
|
||
|
}
|
||
|
}
|