Files
leanote/vendor/github.com/revel/pathtree/tree.go
2017-11-30 19:55:33 +08:00

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
}