This commit is contained in:
lealife
2017-06-22 13:18:16 +08:00
parent 2654b684df
commit b140cd538f
549 changed files with 185885 additions and 1 deletions

90
vendor/github.com/robfig/config/README.md generated vendored Normal file
View File

@@ -0,0 +1,90 @@
config
======
This package implements a basic configuration file parser language which
provides a structure similar to what you would find on Microsoft Windows INI
files.
The configuration file consists of sections, led by a "*[section]*" header and
followed by "*name: value*" entries; "*name=value*" is also accepted. Note that
leading whitespace is removed from values. The optional values can contain
format strings which refer to other values in the same section, or values in a
special *DEFAULT* section. Additional defaults can be provided on initialization
and retrieval. Comments are indicated by ";" or "#"; a comment may begin
anywhere on a line, including on the same line after parameters or section
declarations.
For example:
[My Section]
foodir: %(dir)s/whatever
dir=foo
would resolve the "*%(dir)s*" to the value of "*dir*" (*foo* in this case). All
reference expansions are done on demand.
The functionality and workflow is loosely based on the *configparser* package of
the Python Standard Library.
## Installation
go get github.com/robfig/config
## Operating instructions
Given a sample configuration file:
[DEFAULT]
host: www.example.com
protocol: http://
base-url: %(protocol)s%(host)s
[service-1]
url: %(base-url)s/some/path
delegation: on
maxclients: 200 # do not set this higher
comments: This is a multi-line
entry # And this is a comment
To read this configuration file, do:
c, _ := config.ReadDefault("config.cfg")
c.String("service-1", "url")
// result is string "http://www.example.com/some/path"
c.Int("service-1", "maxclients")
// result is int 200
c.Bool("service-1", "delegation")
// result is bool true
c.String("service-1", "comments")
// result is string "This is a multi-line\nentry"
Note the support for unfolding variables (such as *%(base-url)s*), which are read
from the special (reserved) section name *[DEFAULT]*.
A new configuration file can also be created with:
c := config.NewDefault()
c.AddSection("Section")
c.AddOption("Section", "option", "value")
c.WriteFile("config.cfg", 0644, "A header for this file")
This results in the file:
# A header for this file
[Section]
option: value
Note that sections, options and values are all case-sensitive.
## License
The source files are distributed under the [Mozilla Public License, version 2.0](http://mozilla.org/MPL/2.0/),
unless otherwise noted.
Please read the [FAQ](http://www.mozilla.org/MPL/2.0/FAQ.html)
if you have further questions regarding the license.

151
vendor/github.com/robfig/config/config.go generated vendored Normal file
View File

@@ -0,0 +1,151 @@
// Copyright 2009 The "config" Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"regexp"
"strings"
)
const (
// Default section name.
DEFAULT_SECTION = "DEFAULT"
// Maximum allowed depth when recursively substituing variable names.
_DEPTH_VALUES = 200
DEFAULT_COMMENT = "# "
ALTERNATIVE_COMMENT = "; "
DEFAULT_SEPARATOR = ":"
ALTERNATIVE_SEPARATOR = "="
)
var (
// Strings accepted as boolean.
boolString = map[string]bool{
"t": true,
"true": true,
"y": true,
"yes": true,
"on": true,
"1": true,
"f": false,
"false": false,
"n": false,
"no": false,
"off": false,
"0": false,
}
varRegExp = regexp.MustCompile(`%\(([a-zA-Z0-9_.\-]+)\)s`) // %(variable)s
envVarRegExp = regexp.MustCompile(`\${([a-zA-Z0-9_.\-]+)}`) // ${envvar}
)
// Config is the representation of configuration settings.
type Config struct {
comment string
separator string
// Sections order
lastIdSection int // Last section identifier
idSection map[string]int // Section : position
// The last option identifier used for each section.
lastIdOption map[string]int // Section : last identifier
// Section -> option : value
data map[string]map[string]*tValue
}
// tValue holds the input position for a value.
type tValue struct {
position int // Option order
v string // value
}
// New creates an empty configuration representation.
// This representation can be filled with AddSection and AddOption and then
// saved to a file using WriteFile.
//
// == Arguments
//
// comment: has to be `DEFAULT_COMMENT` or `ALTERNATIVE_COMMENT`
// separator: has to be `DEFAULT_SEPARATOR` or `ALTERNATIVE_SEPARATOR`
// preSpace: indicate if is inserted a space before of the separator
// postSpace: indicate if is added a space after of the separator
func New(comment, separator string, preSpace, postSpace bool) *Config {
if comment != DEFAULT_COMMENT && comment != ALTERNATIVE_COMMENT {
panic("comment character not valid")
}
if separator != DEFAULT_SEPARATOR && separator != ALTERNATIVE_SEPARATOR {
panic("separator character not valid")
}
// == Get spaces around separator
if preSpace {
separator = " " + separator
}
if postSpace {
separator += " "
}
//==
c := new(Config)
c.comment = comment
c.separator = separator
c.idSection = make(map[string]int)
c.lastIdOption = make(map[string]int)
c.data = make(map[string]map[string]*tValue)
c.AddSection(DEFAULT_SECTION) // Default section always exists.
return c
}
// NewDefault creates a configuration representation with values by default.
func NewDefault() *Config {
return New(DEFAULT_COMMENT, DEFAULT_SEPARATOR, false, true)
}
// Merge merges the given configuration "source" with this one ("target").
//
// Merging means that any option (under any section) from source that is not in
// target will be copied into target. When the target already has an option with
// the same name and section then it is overwritten (i.o.w. the source wins).
func (target *Config) Merge(source *Config) {
if source == nil || source.data == nil || len(source.data) == 0 {
return
}
for section, option := range source.data {
for optionName, optionValue := range option {
target.AddOption(section, optionName, optionValue.v)
}
}
}
// == Utility
func stripComments(l string) string {
// Comments are preceded by space or TAB
for _, c := range []string{" ;", "\t;", " #", "\t#"} {
if i := strings.Index(l, c); i != -1 {
l = l[0:i]
}
}
return l
}

27
vendor/github.com/robfig/config/error.go generated vendored Normal file
View File

@@ -0,0 +1,27 @@
// Copyright 2009 The "config" Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
type SectionError string
func (e SectionError) Error() string {
return "section not found: " + string(e)
}
type OptionError string
func (e OptionError) Error() string {
return "option not found: " + string(e)
}

113
vendor/github.com/robfig/config/option.go generated vendored Normal file
View File

@@ -0,0 +1,113 @@
// Copyright 2009 The "config" Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import "errors"
// AddOption adds a new option and value to the configuration.
//
// If the section is nil then uses the section by default; if it does not exist,
// it is created in advance.
//
// It returns true if the option and value were inserted, and false if the value
// was overwritten.
func (c *Config) AddOption(section string, option string, value string) bool {
c.AddSection(section) // Make sure section exists
if section == "" {
section = DEFAULT_SECTION
}
_, ok := c.data[section][option]
c.data[section][option] = &tValue{c.lastIdOption[section], value}
c.lastIdOption[section]++
return !ok
}
// RemoveOption removes a option and value from the configuration.
// It returns true if the option and value were removed, and false otherwise,
// including if the section did not exist.
func (c *Config) RemoveOption(section string, option string) bool {
if _, ok := c.data[section]; !ok {
return false
}
_, ok := c.data[section][option]
delete(c.data[section], option)
return ok
}
// HasOption checks if the configuration has the given option in the section.
// It returns false if either the option or section do not exist.
func (c *Config) HasOption(section string, option string) bool {
if _, ok := c.data[section]; !ok {
return false
}
_, okd := c.data[DEFAULT_SECTION][option]
_, oknd := c.data[section][option]
return okd || oknd
}
// Options returns the list of options available in the given section.
// It returns an error if the section does not exist and an empty list if the
// section is empty. Options within the default section are also included.
func (c *Config) Options(section string) (options []string, err error) {
if _, ok := c.data[section]; !ok {
return nil, errors.New(SectionError(section).Error())
}
// Keep a map of option names we've seen to deduplicate.
optionMap := make(map[string]struct{},
len(c.data[DEFAULT_SECTION])+len(c.data[section]))
for s, _ := range c.data[DEFAULT_SECTION] {
optionMap[s] = struct{}{}
}
for s, _ := range c.data[section] {
optionMap[s] = struct{}{}
}
// Get the keys.
i := 0
options = make([]string, len(optionMap))
for k, _ := range optionMap {
options[i] = k
i++
}
return options, nil
}
// SectionOptions returns only the list of options available in the given section.
// Unlike Options, SectionOptions doesn't return options in default section.
// It returns an error if the section doesn't exist.
func (c *Config) SectionOptions(section string) (options []string, err error) {
if _, ok := c.data[section]; !ok {
return nil, errors.New(SectionError(section).Error())
}
options = make([]string, len(c.data[section]))
i := 0
for s, _ := range c.data[section] {
options[i] = s
i++
}
return options, nil
}

100
vendor/github.com/robfig/config/read.go generated vendored Normal file
View File

@@ -0,0 +1,100 @@
// Copyright 2009 The "config" Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"bufio"
"errors"
"os"
"strings"
"unicode"
)
// _read is the base to read a file and get the configuration representation.
// That representation can be queried with GetString, etc.
func _read(fname string, c *Config) (*Config, error) {
file, err := os.Open(fname)
if err != nil {
return nil, err
}
if err = c.read(bufio.NewReader(file)); err != nil {
return nil, err
}
if err = file.Close(); err != nil {
return nil, err
}
return c, nil
}
// Read reads a configuration file and returns its representation.
// All arguments, except `fname`, are related to `New()`
func Read(fname string, comment, separator string, preSpace, postSpace bool) (*Config, error) {
return _read(fname, New(comment, separator, preSpace, postSpace))
}
// ReadDefault reads a configuration file and returns its representation.
// It uses values by default.
func ReadDefault(fname string) (*Config, error) {
return _read(fname, NewDefault())
}
// * * *
func (c *Config) read(buf *bufio.Reader) (err error) {
var section, option string
var scanner = bufio.NewScanner(buf)
for scanner.Scan() {
l := strings.TrimRightFunc(stripComments(scanner.Text()), unicode.IsSpace)
// Switch written for readability (not performance)
switch {
// Empty line and comments
case len(l) == 0, l[0] == '#', l[0] == ';':
continue
// New section. The [ must be at the start of the line
case l[0] == '[' && l[len(l)-1] == ']':
option = "" // reset multi-line value
section = strings.TrimSpace(l[1 : len(l)-1])
c.AddSection(section)
// Continuation of multi-line value
// starts with whitespace, we're in a section and working on an option
case section != "" && option != "" && (l[0] == ' ' || l[0] == '\t'):
prev, _ := c.RawString(section, option)
value := strings.TrimSpace(l)
c.AddOption(section, option, prev+"\n"+value)
// Other alternatives
default:
i := strings.IndexAny(l, "=:")
switch {
// Option and value
case i > 0 && l[0] != ' ' && l[0] != '\t': // found an =: and it's not a multiline continuation
option = strings.TrimSpace(l[0:i])
value := strings.TrimSpace(l[i+1:])
c.AddOption(section, option, value)
default:
return errors.New("could not parse line: " + l)
}
}
}
return scanner.Err()
}

88
vendor/github.com/robfig/config/section.go generated vendored Normal file
View File

@@ -0,0 +1,88 @@
// Copyright 2009 The "config" Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
// AddSection adds a new section to the configuration.
//
// If the section is nil then uses the section by default which it's already
// created.
//
// It returns true if the new section was inserted, and false if the section
// already existed.
func (c *Config) AddSection(section string) bool {
// DEFAULT_SECTION
if section == "" {
return false
}
if _, ok := c.data[section]; ok {
return false
}
c.data[section] = make(map[string]*tValue)
// Section order
c.idSection[section] = c.lastIdSection
c.lastIdSection++
return true
}
// RemoveSection removes a section from the configuration.
// It returns true if the section was removed, and false if section did not exist.
func (c *Config) RemoveSection(section string) bool {
_, ok := c.data[section]
// Default section cannot be removed.
if !ok || section == DEFAULT_SECTION {
return false
}
for o, _ := range c.data[section] {
delete(c.data[section], o) // *value
}
delete(c.data, section)
delete(c.lastIdOption, section)
delete(c.idSection, section)
return true
}
// HasSection checks if the configuration has the given section.
// (The default section always exists.)
func (c *Config) HasSection(section string) bool {
_, ok := c.data[section]
return ok
}
// Sections returns the list of sections in the configuration.
// (The default section always exists).
func (c *Config) Sections() (sections []string) {
sections = make([]string, len(c.idSection))
pos := 0 // Position in sections
for i := 0; i < c.lastIdSection; i++ {
for section, id := range c.idSection {
if id == i {
sections[pos] = section
pos++
}
}
}
return sections
}

155
vendor/github.com/robfig/config/type.go generated vendored Normal file
View File

@@ -0,0 +1,155 @@
// Copyright 2009 The "config" Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"errors"
"fmt"
"os"
"regexp"
"strconv"
"strings"
)
// Substitutes values, calculated by callback, on matching regex
func (c *Config) computeVar(beforeValue *string, regx *regexp.Regexp, headsz, tailsz int, withVar func(*string) string) (*string, error) {
var i int
computedVal := beforeValue
for i = 0; i < _DEPTH_VALUES; i++ { // keep a sane depth
vr := regx.FindStringSubmatchIndex(*computedVal)
if len(vr) == 0 {
break
}
varname := (*computedVal)[vr[headsz]:vr[headsz+1]]
varVal := withVar(&varname)
if varVal == "" {
return &varVal, errors.New(fmt.Sprintf("Option not found: %s", varname))
}
// substitute by new value and take off leading '%(' and trailing ')s'
// %(foo)s => headsz=2, tailsz=2
// ${foo} => headsz=2, tailsz=1
newVal := (*computedVal)[0:vr[headsz]-headsz] + varVal + (*computedVal)[vr[headsz+1]+tailsz:]
computedVal = &newVal
}
if i == _DEPTH_VALUES {
retVal := ""
return &retVal,
fmt.Errorf("Possible cycle while unfolding variables: max depth of %d reached", _DEPTH_VALUES)
}
return computedVal, nil
}
// Bool has the same behaviour as String but converts the response to bool.
// See "boolString" for string values converted to bool.
func (c *Config) Bool(section string, option string) (value bool, err error) {
sv, err := c.String(section, option)
if err != nil {
return false, err
}
value, ok := boolString[strings.ToLower(sv)]
if !ok {
return false, errors.New("could not parse bool value: " + sv)
}
return value, nil
}
// Float has the same behaviour as String but converts the response to float.
func (c *Config) Float(section string, option string) (value float64, err error) {
sv, err := c.String(section, option)
if err == nil {
value, err = strconv.ParseFloat(sv, 64)
}
return value, err
}
// Int has the same behaviour as String but converts the response to int.
func (c *Config) Int(section string, option string) (value int, err error) {
sv, err := c.String(section, option)
if err == nil {
value, err = strconv.Atoi(sv)
}
return value, err
}
// RawString gets the (raw) string value for the given option in the section.
// The raw string value is not subjected to unfolding, which was illustrated in
// the beginning of this documentation.
//
// It returns an error if either the section or the option do not exist.
func (c *Config) RawString(section string, option string) (value string, err error) {
if _, ok := c.data[section]; ok {
if tValue, ok := c.data[section][option]; ok {
return tValue.v, nil
}
}
return c.RawStringDefault(option)
}
// RawStringDefault gets the (raw) string value for the given option from the
// DEFAULT section.
//
// It returns an error if the option does not exist in the DEFAULT section.
func (c *Config) RawStringDefault(option string) (value string, err error) {
if tValue, ok := c.data[DEFAULT_SECTION][option]; ok {
return tValue.v, nil
}
return "", OptionError(option)
}
// String gets the string value for the given option in the section.
// If the value needs to be unfolded (see e.g. %(host)s example in the beginning
// of this documentation), then String does this unfolding automatically, up to
// _DEPTH_VALUES number of iterations.
//
// It returns an error if either the section or the option do not exist, or the
// unfolding cycled.
func (c *Config) String(section string, option string) (value string, err error) {
value, err = c.RawString(section, option)
if err != nil {
return "", err
}
// % variables
computedVal, err := c.computeVar(&value, varRegExp, 2, 2, func(varName *string) string {
lowerVar := *varName
// search variable in default section as well as current section
varVal, _ := c.data[DEFAULT_SECTION][lowerVar]
if _, ok := c.data[section][lowerVar]; ok {
varVal = c.data[section][lowerVar]
}
return varVal.v
})
value = *computedVal
if err != nil {
return value, err
}
// $ environment variables
computedVal, err = c.computeVar(&value, envVarRegExp, 2, 1, func(varName *string) string {
return os.Getenv(*varName)
})
value = *computedVal
return value, err
}

90
vendor/github.com/robfig/config/write.go generated vendored Normal file
View File

@@ -0,0 +1,90 @@
// Copyright 2009 The "config" Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"bufio"
"fmt"
"os"
"strings"
)
// WriteFile saves the configuration representation to a file.
// The desired file permissions must be passed as in os.Open. The header is a
// string that is saved as a comment in the first line of the file.
func (c *Config) WriteFile(fname string, perm os.FileMode, header string) error {
file, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
buf := bufio.NewWriter(file)
if err = c.write(buf, header); err != nil {
return err
}
buf.Flush()
return file.Close()
}
func (c *Config) write(buf *bufio.Writer, header string) (err error) {
if header != "" {
// Add comment character after of each new line.
if i := strings.Index(header, "\n"); i != -1 {
header = strings.Replace(header, "\n", "\n"+c.comment, -1)
}
if _, err = buf.WriteString(c.comment + header + "\n"); err != nil {
return err
}
}
for _, orderedSection := range c.Sections() {
for section, sectionMap := range c.data {
if section == orderedSection {
// Skip default section if empty.
if section == DEFAULT_SECTION && len(sectionMap) == 0 {
continue
}
if _, err = buf.WriteString("\n[" + section + "]\n"); err != nil {
return err
}
// Follow the input order in options.
for i := 0; i < c.lastIdOption[section]; i++ {
for option, tValue := range sectionMap {
if tValue.position == i {
if _, err = buf.WriteString(fmt.Sprint(
option, c.separator, tValue.v, "\n")); err != nil {
return err
}
c.RemoveOption(section, option)
break
}
}
}
}
}
}
if _, err = buf.WriteString("\n"); err != nil {
return err
}
return nil
}

21
vendor/github.com/robfig/pathtree/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
Copyright (C) 2013 Rob Figueiredo
All Rights Reserved.
MIT LICENSE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

228
vendor/github.com/robfig/pathtree/tree.go generated vendored Normal file
View File

@@ -0,0 +1,228 @@
// 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
}