229 lines
6.4 KiB
Go
229 lines
6.4 KiB
Go
// pathtree implements a tree for fast path lookup.
|
|
//
|
|
// Restrictions
|
|
//
|
|
// - Paths must be a '/'-separated list of strings, like a URL or Unix filesystem.
|
|
// - All paths must begin with a '/'.
|
|
// - Path elements may not contain a '/'.
|
|
// - Path elements beginning with a ':' or '*' will be interpreted as wildcards.
|
|
// - Trailing slashes are inconsequential.
|
|
//
|
|
// Wildcards
|
|
//
|
|
// Wildcards are named path elements that may match any strings in that
|
|
// location. Two different kinds of wildcards are permitted:
|
|
// - :var - names beginning with ':' will match any single path element.
|
|
// - *var - names beginning with '*' will match one or more path elements.
|
|
// (however, no path elements may come after a star wildcard)
|
|
//
|
|
// Extensions
|
|
//
|
|
// Single element wildcards in the last path element can optionally end with an
|
|
// extension. This allows for routes like '/users/:id.json', which will not
|
|
// conflict with '/users/:id'.
|
|
//
|
|
// Algorithm
|
|
//
|
|
// Paths are mapped to the tree in the following way:
|
|
// - Each '/' is a Node in the tree. The root node is the leading '/'.
|
|
// - Each Node has edges to other nodes. The edges are named according to the
|
|
// possible path elements at that depth in the path.
|
|
// - Any Node may have an associated Leaf. Leafs are terminals containing the
|
|
// data associated with the path as traversed from the root to that Node.
|
|
//
|
|
// Edges are implemented as a map from the path element name to the next node in
|
|
// the path.
|
|
package pathtree
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
)
|
|
|
|
type Node struct {
|
|
edges map[string]*Node // the various path elements leading out of this node.
|
|
wildcard *Node // if set, this node had a wildcard as its path element.
|
|
leaf *Leaf // if set, this is a terminal node for this leaf.
|
|
extensions map[string]*Leaf // if set, this is a terminal node with a leaf that ends in a specific extension.
|
|
star *Leaf // if set, this path ends in a star.
|
|
leafs int // counter for # leafs in the tree
|
|
}
|
|
|
|
type Leaf struct {
|
|
Value interface{} // the value associated with this node
|
|
Wildcards []string // the wildcard names, in order they appear in the path
|
|
order int // the order this leaf was added
|
|
}
|
|
|
|
// New returns a new path tree.
|
|
func New() *Node {
|
|
return &Node{edges: make(map[string]*Node)}
|
|
}
|
|
|
|
// Add a path and its associated value to the tree.
|
|
// - key must begin with "/"
|
|
// - key must not duplicate any existing key.
|
|
// Returns an error if those conditions do not hold.
|
|
func (n *Node) Add(key string, val interface{}) error {
|
|
if key == "" || key[0] != '/' {
|
|
return errors.New("Path must begin with /")
|
|
}
|
|
n.leafs++
|
|
return n.add(n.leafs, splitPath(key), nil, val)
|
|
}
|
|
|
|
// Adds a leaf to a terminal node.
|
|
// If the last wildcard contains an extension, add it to the 'extensions' map.
|
|
func (n *Node) addLeaf(leaf *Leaf) error {
|
|
extension := stripExtensionFromLastSegment(leaf.Wildcards)
|
|
if extension != "" {
|
|
if n.extensions == nil {
|
|
n.extensions = make(map[string]*Leaf)
|
|
}
|
|
if n.extensions[extension] != nil {
|
|
return errors.New("duplicate path")
|
|
}
|
|
n.extensions[extension] = leaf
|
|
return nil
|
|
}
|
|
|
|
if n.leaf != nil {
|
|
return errors.New("duplicate path")
|
|
}
|
|
n.leaf = leaf
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) add(order int, elements, wildcards []string, val interface{}) error {
|
|
if len(elements) == 0 {
|
|
leaf := &Leaf{
|
|
order: order,
|
|
Value: val,
|
|
Wildcards: wildcards,
|
|
}
|
|
return n.addLeaf(leaf)
|
|
}
|
|
|
|
var el string
|
|
el, elements = elements[0], elements[1:]
|
|
if el == "" {
|
|
return errors.New("empty path elements are not allowed")
|
|
}
|
|
|
|
// Handle wildcards.
|
|
switch el[0] {
|
|
case ':':
|
|
if n.wildcard == nil {
|
|
n.wildcard = New()
|
|
}
|
|
return n.wildcard.add(order, elements, append(wildcards, el[1:]), val)
|
|
case '*':
|
|
if n.star != nil {
|
|
return errors.New("duplicate path")
|
|
}
|
|
n.star = &Leaf{
|
|
order: order,
|
|
Value: val,
|
|
Wildcards: append(wildcards, el[1:]),
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// It's a normal path element.
|
|
e, ok := n.edges[el]
|
|
if !ok {
|
|
e = New()
|
|
n.edges[el] = e
|
|
}
|
|
|
|
return e.add(order, elements, wildcards, val)
|
|
}
|
|
|
|
// Find a given path. Any wildcards traversed along the way are expanded and
|
|
// returned, along with the value.
|
|
func (n *Node) Find(key string) (leaf *Leaf, expansions []string) {
|
|
if len(key) == 0 || key[0] != '/' {
|
|
return nil, nil
|
|
}
|
|
|
|
return n.find(splitPath(key), nil)
|
|
}
|
|
|
|
func (n *Node) find(elements, exp []string) (leaf *Leaf, expansions []string) {
|
|
if len(elements) == 0 {
|
|
// If this node has explicit extensions, check if the path matches one.
|
|
if len(exp) > 0 && n.extensions != nil {
|
|
lastExp := exp[len(exp)-1]
|
|
prefix, extension := extensionForPath(lastExp)
|
|
if leaf := n.extensions[extension]; leaf != nil {
|
|
exp[len(exp)-1] = prefix
|
|
return leaf, exp
|
|
}
|
|
}
|
|
return n.leaf, exp
|
|
}
|
|
|
|
// If this node has a star, calculate the star expansions in advance.
|
|
var starExpansion string
|
|
if n.star != nil {
|
|
starExpansion = strings.Join(elements, "/")
|
|
}
|
|
|
|
// Peel off the next element and look up the associated edge.
|
|
var el string
|
|
el, elements = elements[0], elements[1:]
|
|
if nextNode, ok := n.edges[el]; ok {
|
|
leaf, expansions = nextNode.find(elements, exp)
|
|
}
|
|
|
|
// Handle colon
|
|
if n.wildcard != nil {
|
|
wildcardLeaf, wildcardExpansions := n.wildcard.find(elements, append(exp, el))
|
|
if wildcardLeaf != nil && (leaf == nil || leaf.order > wildcardLeaf.order) {
|
|
leaf = wildcardLeaf
|
|
expansions = wildcardExpansions
|
|
}
|
|
}
|
|
|
|
// Handle star
|
|
if n.star != nil && (leaf == nil || leaf.order > n.star.order) {
|
|
leaf = n.star
|
|
expansions = append(exp, starExpansion)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func extensionForPath(path string) (string, string) {
|
|
dotPosition := strings.LastIndex(path, ".")
|
|
if dotPosition != -1 {
|
|
return path[:dotPosition], path[dotPosition:]
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func splitPath(key string) []string {
|
|
elements := strings.Split(key, "/")
|
|
if elements[0] == "" {
|
|
elements = elements[1:]
|
|
}
|
|
if elements[len(elements)-1] == "" {
|
|
elements = elements[:len(elements)-1]
|
|
}
|
|
return elements
|
|
}
|
|
|
|
// stripExtensionFromLastSegment determines if a string slice representing a path
|
|
// ends with a file extension, removes the extension from the input, and returns it.
|
|
func stripExtensionFromLastSegment(segments []string) string {
|
|
if len(segments) == 0 {
|
|
return ""
|
|
}
|
|
lastSegment := segments[len(segments)-1]
|
|
prefix, extension := extensionForPath(lastSegment)
|
|
if extension != "" {
|
|
segments[len(segments)-1] = prefix
|
|
}
|
|
return extension
|
|
}
|