diff --git a/constant.go b/constant.go index b78028f..e978d9f 100644 --- a/constant.go +++ b/constant.go @@ -7,40 +7,46 @@ import ( "text/template" ) -// Returns the value of the constant in the pool named 'name' as a string. -func (pool *Pool) Str(name string) string { - pool.mutex.RLock() - defer pool.mutex.RUnlock() - - if pool.defaults[name] == nil { +// Returns the value of the node as defined by path. +// If the node's default value is nil an empty string is returned. +// If the envionment variable associated with the node is not equal to an empty string that value is used instead of the node's default value. +// Templates in the node's value are parsed (see sections Template, Template Context and Example for details). +func (n *Node) Str(path ...string) string { + node := n.Node(path...) + if node == nil { return "" } - var tmpl string - if env := os.Getenv(pool.env_name(name)); env == "" { - tmpl = *pool.defaults[name] - } else { + node_fullname := node.FullName() + tmpl := node.Default() + + node.mutex.RLock() + defer node.mutex.RUnlock() + + parent := node.parent + + if env := os.Getenv(node_fullname); env != "" { tmpl = env } t, err := template.New("constant").Funcs(template.FuncMap{ - "const": func(in_name string) string { - if in_name == name { + "const": func(path ...string) string { + if len(path) == 1 && path[0] == node.name { return "" } - return pool.Str(in_name) + return parent.Str(path...) }, "list": func() []string { - consts := pool.List() + consts := parent.List() for i, cnst := range consts { - if cnst == name { + if cnst == node.name { consts = append(consts[:i], consts[i+1:]...) } } return consts }, - "isset": func(in_name string) bool { - return pool.IsSet(in_name) + "isset": func(path ...string) bool { + return parent.IsSet(path...) }, }).Parse(tmpl) @@ -56,67 +62,83 @@ func (pool *Pool) Str(name string) string { return byte_string.String() } -// Returns the value of the constant in the pool named 'name' as an integer. +// Alias of n.Str() +func (n *Node) String() string { + return n.Str() +} + +// Returns the value of n.Str(path...) as an integer. // // Follows convention of strconv.Atoi (https://golang.org/pkg/strconv/#Atoi). -func (pool *Pool) Int(name string) (val int, err error) { - val, err = strconv.Atoi(pool.Str(name)) +func (n *Node) Int(path ...string) (val int, err error) { + val, err = strconv.Atoi(n.Str(path...)) return } -// Run Int but ignore errors -func (pool *Pool) IntI(name string) (val int) { - val, _ = pool.Int(name) +// Run n.Int(path...) but ignore errors +func (n *Node) IntI(path ...string) (val int) { + val, _ = n.Int(path...) return } -// Returns the value of the constant in the pool named 'name' as a float64. +// Returns the value of n.Str(path...) as a float64. // // Follows convention of strconv.ParseFloat (https://golang.org/pkg/strconv/#ParseFloat). -func (pool *Pool) Float(name string, bitSize int) (val float64, err error) { - val, err = strconv.ParseFloat(pool.Str(name), bitSize) +func (n *Node) Float(bitSize int, path ...string) (val float64, err error) { + val, err = strconv.ParseFloat(n.Str(path...), bitSize) return } -// Run Float but ignore errors -func (pool *Pool) FloatI(name string, bitSize int) (val float64) { - val, _ = pool.Float(name, bitSize) +// Run n.Float(bitSize, path...) but ignore errors +func (n *Node) FloatI(bitSize int, path ...string) (val float64) { + val, _ = n.Float(bitSize, path...) return } -// Returns the value of the constant in the pool named 'name' as a boolean. +// Returns the value of n.Str(path...) as a boolean. // // Follows convention of strconv.ParseBool (https://golang.org/pkg/strconv/#ParseBool). -func (pool *Pool) Bool(name string) (val bool, err error) { - val, err = strconv.ParseBool(pool.Str(name)) +func (n *Node) Bool(path ...string) (val bool, err error) { + val, err = strconv.ParseBool(n.Str(path...)) return } -// Run Bool but ignore errors -func (pool *Pool) BoolI(name string) (val bool) { - val, _ = pool.Bool(name) +// Run n.Bool(path...) but ignore errors +func (n *Node) BoolI(path ...string) (val bool) { + val, _ = n.Bool(path...) return } -// Returns if the constant named 'name' is set in the pool. -func (pool *Pool) IsSet(name string) bool { - pool.mutex.RLock() - defer pool.mutex.RUnlock() +// Returns false if: the node as defined by path doesn't exist; the node's default value is nil; the node's default value it an empty string. +// Otherwise returns true. +func (n *Node) IsSet(path ...string) bool { + node := n.Node(path...) + if node == nil { + return false + } + + node.mutex.RLock() + defer node.mutex.RUnlock() - return pool.defaults[name] != nil + return node.def_val != nil && *node.def_val != "" } -// Returns the default value of the constant in the pool named 'name'. +// Returns the default value of node as defined by path. // If the default value contains templates the templates will not be parsed. // If the default value is not a string it will be converted to a string as per the strconv package (https://golang.org/pkg/strconv/). -func (pool *Pool) Default(name string) string { - if pool.defaults[name] == nil { +// If the default value is nil an empty string is returned. +func (n *Node) Default(path ...string) string { + node := n.Node(path...) + if node == nil { return "" } - return *pool.defaults[name] -} + if !node.IsSet() { + return "" + } + + node.mutex.RLock() + defer node.mutex.RUnlock() -func (pool *Pool) env_name(name string) string { - return pool.prefix + name + return *node.def_val } diff --git a/constant_test.go b/constant_test.go index 499ec8d..a3d7856 100644 --- a/constant_test.go +++ b/constant_test.go @@ -8,8 +8,9 @@ import ( "testing" ) -var pool *constant.Pool -var pool_prefix = "test_" +var node *constant.Node +var node_prefix = "test" +var node_delimiter = "_" type constPair struct { name string @@ -43,7 +44,7 @@ type invalid string var tests = []test_type{ {constPair{"string1", "string value"}, "string value", false, "string value", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, true}, {constPair{"string1", "already exists"}, "string value", true, "string value", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, true}, - {constPair{"string2", ""}, "", false, "", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, true}, + {constPair{"string2", ""}, "", false, "", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, false}, {constPair{"byte1", []byte("byte string value")}, "byte string value", false, "byte string value", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, true}, {constPair{"fmtStringer1", stringer("fmt.Stringer value")}, "fmt.Stringer value", false, "fmt.Stringer value", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, true}, {constPair{"int1", 12}, "12", false, "12", val_err{12, false}, val_err{12.0, false}, val_err{false, true}, true}, @@ -59,7 +60,7 @@ var tests = []test_type{ {constPair{"bool7", "T"}, "T", false, "T", val_err{0, true}, val_err{0.0, true}, val_err{true, false}, true}, {constPair{"bool8", "F"}, "F", false, "F", val_err{0, true}, val_err{0.0, true}, val_err{false, false}, true}, {constPair{"invalid1", invalid("not a valid type")}, "", true, "", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, false}, - {constPair{"invalid2", nil}, "", true, "", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, false}, + {constPair{"nil_value", nil}, "", false, "", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, false}, {constPair{"", "empty name"}, "", true, "", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, false}, {constPair{"2name", "name starting with number"}, "", true, "", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, false}, {constPair{"namew1thnum", "name containing number"}, "name containing number", false, "name containing number", val_err{0, true}, val_err{0.0, true}, val_err{false, true}, true}, @@ -76,13 +77,13 @@ var tests = []test_type{ } func TestMain(m *testing.M) { - pool = constant.NewPool(pool_prefix) + node = constant.NewTree(node_prefix, node_delimiter) os.Exit(m.Run()) } func TestNew(t *testing.T) { for _, test := range tests { - err := pool.New(test.pair.name, test.pair.value) + _, err := node.New(test.pair.name, test.pair.value) if (err != nil) != test.new_error { no_str := "" if !test.new_error { @@ -97,9 +98,39 @@ func TestNew(t *testing.T) { } } +func TestDelimiter(t *testing.T) { + res := node.Delimiter() + if res != node_delimiter { + t.Error( + "Expected", node_delimiter, + "got", res, + ) + } +} + +func TestName(t *testing.T) { + name := node.Name() + if name != node_prefix { + t.Error( + "Expected", node_prefix, + "got", name, + ) + } +} + +func TestFullName(t *testing.T) { + full_name := node.Name() + if full_name != node_prefix { + t.Error( + "Expected", node_prefix, + "got", full_name, + ) + } +} + func TestStr(t *testing.T) { for _, test := range tests { - str := pool.Str(test.pair.name) + str := node.Str(test.pair.name) if str != test.str { t.Error( "For", test.pair, @@ -112,7 +143,7 @@ func TestStr(t *testing.T) { func TestInt(t *testing.T) { for _, test := range tests { - val, err := pool.Int(test.pair.name) + val, err := node.Int(test.pair.name) if val != test.int_res.val || (err != nil) != test.int_res.err { t.Error( "For", test.pair, @@ -125,7 +156,7 @@ func TestInt(t *testing.T) { func TestFloat(t *testing.T) { for _, test := range tests { - val, err := pool.Float(test.pair.name, 64) + val, err := node.Float(64, test.pair.name) if val != test.float_res.val || (err != nil) != test.float_res.err { t.Error( "For", test.pair, @@ -138,7 +169,7 @@ func TestFloat(t *testing.T) { func TestBool(t *testing.T) { for _, test := range tests { - val, err := pool.Bool(test.pair.name) + val, err := node.Bool(test.pair.name) if val != test.bool_res.val || (err != nil) != test.bool_res.err { t.Error( "For", test.pair, @@ -151,11 +182,11 @@ func TestBool(t *testing.T) { func TestIsSet(t *testing.T) { for _, test := range tests { - val := pool.IsSet(test.pair.name) + val := node.IsSet(test.pair.name) if val != test.isset { t.Error( "For", test.pair, - "expected", test.new_error, + "expected", test.isset, "got", val, ) } @@ -164,7 +195,7 @@ func TestIsSet(t *testing.T) { func TestDefault(t *testing.T) { for _, test := range tests { - val := pool.Default(test.pair.name) + val := node.Default(test.pair.name) if val != test.default_s { t.Error( "For", test.pair, @@ -178,12 +209,12 @@ func TestDefault(t *testing.T) { func TestList(t *testing.T) { exp_list := make([]string, 0) for _, test := range tests { - if test.new_error == false { + if test.new_error == false && test.pair.value != nil { exp_list = append(exp_list, test.pair.name) } } - res_list := pool.List() + res_list := node.List() sort.StringSlice(exp_list).Sort() sort.StringSlice(res_list).Sort() @@ -199,12 +230,12 @@ func TestList(t *testing.T) { func TestEnvironment(t *testing.T) { exp_list := make([]string, 0) for _, test := range tests { - if test.new_error == false { - exp_list = append(exp_list, pool_prefix+test.pair.name) + if test.new_error == false && test.pair.value != nil { + exp_list = append(exp_list, node_prefix+node_delimiter+test.pair.name) } } - res_list := pool.Environment() + res_list := node.Environment() sort.StringSlice(exp_list).Sort() sort.StringSlice(res_list).Sort() @@ -219,7 +250,7 @@ func TestEnvironment(t *testing.T) { func TestDelete(t *testing.T) { for _, test := range tests { - err := pool.Delete(test.pair.name) + err := node.Delete(test.pair.name) if (err != nil) != test.new_error { no_str := "" if !test.new_error { @@ -233,7 +264,7 @@ func TestDelete(t *testing.T) { } } - list_len := len(pool.List()) + list_len := len(node.List()) if list_len != 0 { t.Error( "expected 0 items left", diff --git a/doc.go b/doc.go index eb2abd8..029d731 100644 --- a/doc.go +++ b/doc.go @@ -1,18 +1,19 @@ /* -Package constant provides a simple interface for creating an storing constants by a key in an application. -If available constants are read from the environment, to provide dynamic configuration. +Package constant provides an interface for creating and storing constants in a key based tree structure. +If available, constants are read from the environment, to provide dynamic configuration. -Package constant also provides a simple templating system to create constants based of the values of other constants. +Package constant also provides a templating system to create constants based of the values of other constants. Template -Package constant provides a simple template system using text/template (https://golang.org/pkg/text/template/) to allow constants to be expressed a combintation of other constants in the same pool. +Package constant provides a template system using text/template (https://golang.org/pkg/text/template/) to allow nodes to be expressed as a combintation of other nodes in the same context. -The following methods are available to use in a constant. +The following methods are available to use in a node. - {{ const "name" }} - Returns the value of of another constant in the same pool named 'name'. - Self reference is not allowed and returns an empty string. + {{ const "path1" ["path2" ...] }} + Returns the value of of another node in the same context as defined by + path1[, path2 ...]. Self reference is not allowed and returns an empty string. + Nodes that don't exist return an empty string. Example: `{{ const "host" }}:{{ const "port" }}` @@ -20,21 +21,21 @@ The following methods are available to use in a constant. `localhost:3306`. Caution: - Although there is a check in place to test if a constant references - itself, there is no check for cyclic dependancy. If a cyclic dependancy - is created then the program will enter an infinite loop. + Although there is a check in place to test if a node references itself, + there is no check for cyclic dependancy. If a cyclic dependancy is + created then the program will enter an infinite loop. {{ list }} - Returns a slice of all constants in the pool except itself. + Returns a slice of all nodes in the context except itself. Example: `{{ range list }}{{ . }}={{ const . }}; {{ end }}` If host=`localhost` and port=`3306` then the above template would return `host=localhost; port=3306; `. - {{ isset "name" }} - Returns whether or not a constant named 'name' is in the current pool. Same - as pool.IsSet(name). + {{ isset "path1" ["path2" ...] }} + Returns whether or not a node as defined by path1[, path2 ...] is in the + current context. Example: `{{ const "protocol" }}://{{ const "domain" }}{{if isset "port"}}:{{ const "port" }}{{end}}/{{ const "page" }}` @@ -45,67 +46,120 @@ The following methods are available to use in a constant. Or if the same constants are set as well as port=`8080` then the above template would return `http://localhost:8080/index.html`. +Template Context + +The context for a node includes the context's root node and all of its children recursively. +The root node for a context is the parent of the node. +Therefore the context for a node includes itself, its parent node, its parent's child nodes (siblings of the original node) and all the recursive children. +A node is excluded from the context if its default value is nil. + +Other nodes in the same context are referenced by their path relative to the root of the context. +This starts with the root of the context which is referenced as an empty string. +Siblings are referenced by their name ("sibling name"). +Recursive children are referenced by the names of their parents followed by their name ("sibling name", "recursive child"). + +For example if the folling tree structure is created + + ___ name: MYAPP ___ + / value: nil \ + / | \ + name: LOG name: RUNTIME name: DATABASE + value: nil value: `dev` value: `true` + / | / | \ + name: LEVEL name: FILE name: HOST name: PORT name: ADDRESS + value: 5 value: `stdout` value: `localhost` value: 3306 value: ? + | + name: PROVIDER + value: `internal` + +then 'ADDRESS' could be set to and would return + + `{{ const "" }}` -> `true` (value of parent) + `{{ const "HOST" }}` -> `localhost` + `{{ const "HOST" "PROVIDER" }}` -> `internal` + `{{ const "PORT" }}` -> `true` + `{{ const "ADDRESS" }}` -> `` (self reference not allowed) + + `{{ list }}` -> `[ HOST HOST_PROVIDER PORT]` (includes an empty string at the start) + `{{ isset "HOST" }}` -> `true` + `{{ isset "SOMETHING" }}` -> `false` + + Example -In the following example a pool is created to store constants related to MySQL. -HOST, PORT, USER and PASSWORD have standard default values while ADDRESS is set by default to equal `HOST + ":" + PORT`. +In the following example the tree from the above section (Template Context) is created. +In this example 'ADDRESS' is set by default to equal `HOST + ":" + PORT`. -Near the end HOST is updated via an envionment variable which, in turn, also updates ADDRESS. +After creation, HOST is updated via an envionment variable which, in turn, also updates the value of ADDRESS. package main import ( - "github.com/JamesStewy/constant" "fmt" + "github.com/JamesStewy/constant" "os" ) - var mysql_const *constant.Pool + var tree *constant.Node func main() { - // Create new pool to store constants related to MySQL - mysql_const = constant.NewPool("MYSQL_") + // Create new tree for my app + tree = constant.NewTree("MYAPP", "_") + + // Create LOG node + tree.New("LOG", nil) + tree.Node("LOG").New("LEVEL", 5) + tree.Node("LOG").New("FILE", "stdout") - // Set default values for HOST, PORT, USER and PASSWORD - mysql_const.New("HOST", "localhost") - mysql_const.New("PORT", 3306) - mysql_const.New("USER", "root") - mysql_const.New("PASSWORD", "") + // Create RUNTIME node + tree.New("RUNTIME", "dev") + + // Create DATABASE node (using alternate method to LOG node) + mysql_tree, _ := tree.New("DATABASE", true) + mysql_tree.New("HOST", "localhost") + mysql_tree.Node("HOST").New("PROVIDER", "internal") + mysql_tree.New("PORT", 3306) // Set ADDRESS to be equal to HOST + ":" + PORT - mysql_const.New("ADDRESS", `{{ const "HOST" }}:{{ const "PORT" }}`) + mysql_tree.New("ADDRESS", `{{ const "HOST" }}:{{ const "PORT" }}`) display_pool() // Update the MySQL host - os.Setenv("MYSQL_HOST", "mydomain.com") - fmt.Println("\nChanged MYSQL_HOST\n") + os.Setenv("MYAPP_DATABASE_HOST", "mydomain.com") + fmt.Println("\nChanged MYAPP_DATABASE_HOST to mydomain.com\n") display_pool() } func display_pool() { // Loop through each constant in the pool and display its value - for _, name := range mysql_const.List() { - // Call mysql_const.Str(name) to retrieve a constants value - fmt.Printf("%s=%s\n", name, mysql_const.Str(name)) + for _, node := range tree.Nodes() { + // Call node.Str(name) to retrieve the node's value + fmt.Printf("%s=%s\n", node.FullName(), node.Str()) } } -The above example returns the following: - - ADDRESS=localhost:3306 - HOST=localhost - PORT=3306 - USER=root - PASSWORD= - - Changed MYSQL_HOST - - USER=root - PASSWORD= - ADDRESS=mydomain.com:3306 - HOST=mydomain.com - PORT=3306 +The above example returns: + + MYAPP_LOG_LEVEL=5 + MYAPP_LOG_FILE=stdout + MYAPP_RUNTIME=dev + MYAPP_DATABASE=true + MYAPP_DATABASE_HOST=localhost + MYAPP_DATABASE_HOST_PROVIDER=internal + MYAPP_DATABASE_PORT=3306 + MYAPP_DATABASE_ADDRESS=localhost:3306 + + Changed MYAPP_DATABASE_HOST to mydomain.com + + MYAPP_LOG_LEVEL=5 + MYAPP_LOG_FILE=stdout + MYAPP_RUNTIME=dev + MYAPP_DATABASE=true + MYAPP_DATABASE_HOST=mydomain.com + MYAPP_DATABASE_HOST_PROVIDER=internal + MYAPP_DATABASE_PORT=3306 + MYAPP_DATABASE_ADDRESS=mydomain.com:3306 */ package constant diff --git a/node.go b/node.go index faf6d7d..a1f320c 100644 --- a/node.go +++ b/node.go @@ -8,101 +8,118 @@ import ( "sync" ) -// Pool represents a collection of constants. -// All constants stored in the same pool can be accessed using the template system. -type Pool struct { - mutex sync.RWMutex - prefix string - defaults map[string]*string +// A Node represents one node in a tree of constants. +// A Node can have a value and/or child nodes associated with it. +type Node struct { + mutex sync.RWMutex + name string + delimiter string + def_val *string + parent *Node + nodes map[string]*Node } -// Creates a new pool. +// Creates the root node for a new tree. // -// Prefix sets the environment variable prefix which is prepended to constants names when searching the runtime environment. -// For example if a pool has a prefix 'MYSQL_' and a constant named 'HOST' then constant 'HOST' would be set to the value of the environment variable 'MYSQL_HOST'. -func NewPool(prefix string) *Pool { - return &Pool{ - prefix: prefix, - defaults: make(map[string]*string), +// Prefix sets the environment variable prefix which is prepended to node names when searching the runtime environment. +// For example if a tree has a prefix 'MYSQL', a delimiter of '_' and a child node named 'HOST' then constant 'HOST' would be set to the value of the environment variable 'MYSQL_HOST'. +func NewTree(prefix, delimiter string) *Node { + return &Node{ + name: prefix, + delimiter: delimiter, + nodes: make(map[string]*Node), } } /* -Adds a new constant to the pool. +Adds a new child node to the node 'n'. +Returns the newly created child node if successful. name: Name of the constant. Must follow variable naming convention. -Lower case letters, uppercase letters, numbers and underscore. +Lower case letters, uppercase letters, numbers and underscores. Can't start with a number. -default_val: The default value for the constant if no environment variable is available. +def_val: The default value for the constant if no environment variable is available. -default_val must be one of the following types: +def_val must be one of the following types: string []byte fmt.Stringer (https://golang.org/pkg/fmt/#Stringer) int float64 bool + nil (no default value: the new child node will act purely as a node) */ -func (pool *Pool) New(name string, default_val interface{}) error { +func (n *Node) New(name string, def_val interface{}) (*Node, error) { if !valid_name(name) { - return errors.New("Invalid Name") + return nil, errors.New("Invalid Name") } - pool.mutex.Lock() - defer pool.mutex.Unlock() + n.mutex.Lock() + defer n.mutex.Unlock() - if pool.defaults[name] != nil { - return errors.New("Constant already exists") + if n.nodes[name] != nil { + return nil, errors.New("Already exists") } - var str_val string - switch t := default_val.(type) { - case string: - if val, ok := default_val.(string); ok { - str_val = val - } else { - return errors.New("Unabled to assert type string on default value") - } - case []byte: - if val, ok := default_val.([]byte); ok { - str_val = string(val) - } else { - return errors.New("Unabled to assert type []byte on default value") - } - case fmt.Stringer: - if val, ok := default_val.(fmt.Stringer); ok { - str_val = val.String() - } else { - return errors.New("Unabled to assert type fmt.Stringer on default value") - } - case int: - if val, ok := default_val.(int); ok { - str_val = strconv.Itoa(val) - } else { - return errors.New("Unabled to assert type int on default value") - } - case float64: - if val, ok := default_val.(float64); ok { - str_val = strconv.FormatFloat(val, 'f', -1, 64) - } else { - return errors.New("Unabled to assert type float64 on default value") - } - case bool: - if val, ok := default_val.(bool); ok { - str_val = strconv.FormatBool(val) - } else { - return errors.New("Unabled to assert type bool on default value") - } - default: - return errors.New(fmt.Sprintf("Unexpected type %T", t)) + new_node := &Node{ + name: name, + delimiter: n.delimiter, + parent: n, + nodes: make(map[string]*Node), } - pool.defaults[name] = new(string) - *pool.defaults[name] = str_val + if def_val != nil { + new_node.def_val = new(string) - return nil + var str_val string + switch t := def_val.(type) { + case string: + if val, ok := def_val.(string); ok { + str_val = val + } else { + return nil, errors.New("Unabled to assert type string on default value") + } + case []byte: + if val, ok := def_val.([]byte); ok { + str_val = string(val) + } else { + return nil, errors.New("Unabled to assert type []byte on default value") + } + case fmt.Stringer: + if val, ok := def_val.(fmt.Stringer); ok { + str_val = val.String() + } else { + return nil, errors.New("Unabled to assert type fmt.Stringer on default value") + } + case int: + if val, ok := def_val.(int); ok { + str_val = strconv.Itoa(val) + } else { + return nil, errors.New("Unabled to assert type int on default value") + } + case float64: + if val, ok := def_val.(float64); ok { + str_val = strconv.FormatFloat(val, 'f', -1, 64) + } else { + return nil, errors.New("Unabled to assert type float64 on default value") + } + case bool: + if val, ok := def_val.(bool); ok { + str_val = strconv.FormatBool(val) + } else { + return nil, errors.New("Unabled to assert type bool on default value") + } + default: + return nil, errors.New(fmt.Sprintf("Unexpected type %T", t)) + } + + *new_node.def_val = str_val + } + + n.nodes[name] = new_node + return n.nodes[name], nil } func valid_name(name string) bool { @@ -110,47 +127,149 @@ func valid_name(name string) bool { return validName.MatchString(name) } -// Deletes constant with name 'name' from the pool. -func (pool *Pool) Delete(name string) error { - pool.mutex.Lock() - defer pool.mutex.Unlock() +// Starting at n, Node traverses down the tree of constant as defined by path. +// If no node is found at path then nil is returned. +// +// If no path is specified Node returns itself (n). +// If an element of path is an empty string it is ignored. +// For example n.Node("LOGGING", "LEVEL") is the same as n.Node("", "LOGGING", "LEVEL") or n.Node("LOGGING", "", "LEVEL"). +// As a result if n.Node("") is called, Node returns itself (n). +func (n *Node) Node(path ...string) *Node { + if len(path) == 0 { + return n + } + + if path[0] == "" { + return n.Node(path[1:]...) + } + + n.mutex.RLock() + defer n.mutex.RUnlock() + + if n.nodes[path[0]] == nil { + return nil + } + return n.nodes[path[0]].Node(path[1:]...) +} + +// Returns the name for the node. +func (n *Node) Name() string { + n.mutex.RLock() + defer n.mutex.RUnlock() + + return n.name +} + +// Returns the full name for the node. +func (n *Node) FullName() string { + return n.nameOffset(0) +} + +// Returns the delimiter for the node. +func (n *Node) Delimiter() string { + n.mutex.RLock() + defer n.mutex.RUnlock() + + return n.delimiter +} + +// Returns a slice of itself and all child nodes in the node that have a non nil default value. +func (n *Node) Nodes() []*Node { + n.mutex.RLock() + defer n.mutex.RUnlock() + + var nodes []*Node + if n.def_val == nil { + nodes = make([]*Node, 0) + } else { + nodes = []*Node{n} + } - if pool.defaults[name] == nil { - return errors.New("Constant doesn't exists") + for _, node := range n.nodes { + nodes = append(nodes, node.Nodes()...) } - delete(pool.defaults, name) + return nodes +} + +// Orphans the node as defined by path. +// Sets the default value for the node to nil. +func (n *Node) Delete(path ...string) error { + node := n.Node(path...) + if node == nil { + return errors.New("Does not exist") + } + + node_full_name := node.FullName() + + node.mutex.Lock() + defer node.mutex.Unlock() + + parent := node.parent + if parent == nil { + return errors.New("Can't delete root node") + } + + parent.mutex.Lock() + defer parent.mutex.Unlock() + + delete(parent.nodes, node.name) + node.name = node_full_name + node.parent = nil + node.def_val = nil + return nil } -// Returns the prefix for the pool. -func (pool *Pool) Prefix() string { - pool.mutex.RLock() - defer pool.mutex.RUnlock() +// Returns a slice of names relative to the node 'n' for itself and all child nodes in the node that have non nil default values. +func (n *Node) List() []string { + return n.environmentOffset(len(n.path())) +} - return pool.prefix +// Returns a slice of full names for itself and all child nodes in the node that have non nil default values. +func (n *Node) Environment() []string { + return n.environmentOffset(0) } -// Returns a slice of all constants in the pool. -func (pool *Pool) List() []string { - pool.mutex.RLock() - defer pool.mutex.RUnlock() +func (n *Node) path() []string { + n.mutex.RLock() + defer n.mutex.RUnlock() - consts := make([]string, 0, len(pool.defaults)) - for c := range pool.defaults { - consts = append(consts, c) + if n.parent == nil { + return []string{n.name} } - return consts + + return append(n.parent.path(), n.name) } -// Returns a slice of all constants in the pool with the pool's prefix prepended. -func (pool *Pool) Environment() []string { - pool.mutex.RLock() - defer pool.mutex.RUnlock() +func (n *Node) pathJoin(path ...string) string { + if len(path) == 0 { + return "" + } + if len(path) == 1 { + return path[0] + } - consts := pool.List() - for i := 0; i < len(consts); i++ { - consts[i] = pool.env_name(consts[i]) + if path[1] == "" { + path[1] = path[0] + } else if path[0] != "" { + path[1] = path[0] + n.Delimiter() + path[1] } - return consts + + return n.pathJoin(path[1:]...) +} + +func (n *Node) nameOffset(offset int) string { + return n.pathJoin(n.path()[offset:]...) +} + +func (n *Node) environmentOffset(offset int) []string { + nodes := n.Nodes() + + env := make([]string, len(nodes)) + for i, node := range nodes { + env[i] = n.pathJoin(node.path()[offset:]...) + } + + return env }