ucfg.go (217 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 ucfg import ( "fmt" "reflect" "regexp" "sort" "time" ) // Config object to store hierarchical configurations into. Config can be // both a dictionary and a list holding primitive values. Primitive values // can be booleans, integers, float point numbers and strings. // // Config provides a low level interface for setting and getting settings // via SetBool, SetInt, SetUint, SetFloat, SetString, SetChild, Bool, Int, Uint, // Float, String, and Child. // // A more user-friendly high level interface is provided via Unpack and Merge. type Config struct { ctx context metadata *Meta fields *fields } type fieldOptions struct { opts *options tag tagOptions validators []validatorTag } type fields struct { d map[string]value a []value } // Meta holds additional meta data per config value. type Meta struct { Source string } var ( tConfig = reflect.TypeOf(Config{}) tConfigPtr = reflect.PtrTo(tConfig) tConfigMap = reflect.TypeOf((map[string]interface{})(nil)) tInterfaceArray = reflect.TypeOf([]interface{}(nil)) // interface types tError = reflect.TypeOf((*error)(nil)).Elem() iInitializer = reflect.TypeOf((*Initializer)(nil)).Elem() tValidator = reflect.TypeOf((*Validator)(nil)).Elem() // primitives tBool = reflect.TypeOf(true) tInt64 = reflect.TypeOf(int64(0)) tUint64 = reflect.TypeOf(uint64(0)) tFloat64 = reflect.TypeOf(float64(0)) tString = reflect.TypeOf("") tDuration = reflect.TypeOf(time.Duration(0)) tRegexp = reflect.TypeOf(regexp.Regexp{}) ) // New creates a new empty Config object. func New() *Config { return &Config{ fields: &fields{nil, nil}, } } // MustNewFrom creates a new config object normalizing and copying from into the new // Config object. MustNewFrom uses Merge to copy from. // // MustNewFrom supports the options: PathSep, MetaData, StructTag, VarExp func MustNewFrom(from interface{}, opts ...Option) *Config { c := New() if err := c.Merge(from, opts...); err != nil { panic(err) } return c } // NewFrom creates a new config object normalizing and copying from into the new // Config object. NewFrom uses Merge to copy from. // // NewFrom supports the options: PathSep, MetaData, StructTag, VarExp func NewFrom(from interface{}, opts ...Option) (*Config, error) { c := New() if err := c.Merge(from, opts...); err != nil { return nil, err } return c, nil } // IsDict checks if c has named keys. func (c *Config) IsDict() bool { return c.fields.dict() != nil } // IsArray checks if c has index only accessible settings. func (c *Config) IsArray() bool { return c.fields.array() != nil } // GetFields returns a list of all top-level named keys in c. func (c *Config) GetFields() []string { var names []string for k := range c.fields.dict() { names = append(names, k) } return names } // Has checks if a field by the given path+idx configuration exists. // Has returns an error if the path can not be resolved because a primitive // value is found in the middle of the traversal. func (c *Config) Has(name string, idx int, options ...Option) (bool, error) { opts := makeOptions(options) p := parsePathIdx(name, idx, opts) return p.Has(c, opts) } // HasField checks if c has a top-level named key name. func (c *Config) HasField(name string) bool { _, ok := c.fields.get(name) return ok } // Remove removes a setting from the config. If the configuration references // another configuration namespace, then the setting will be removed from the // linked reference. // Remove returns true if the setting was removed. If the path can't be // resolved (e.g. due to type mismatch) Remove will return an error. // // Settings can be created on Unpack via Env, Resolve, and ResolveEnv. Settings // generated dynamically on Unpack can not be removed. Remove ignores any // configured environments and will return an error if a value can not be // removed for this reason. // // The setting path is constructed from name and idx. If name is set and idx is -1, // only the name is used to access the setting by name. If name is empty, idx // must be >= 0, assuming the Config is a list. If both name and idx are set, // the name must point to a list. // // Remove supports the options: PathSep func (c *Config) Remove(name string, idx int, options ...Option) (bool, error) { opts := makeOptions(options) // ignore environments opts.env = nil opts.resolvers = nil opts.noParse = true p := parsePathIdx(name, idx, opts) return p.Remove(c, opts) } // Path gets the absolute path of c separated by sep. If c is a root-Config an // empty string will be returned. func (c *Config) Path(sep string) string { return c.ctx.path(sep) } // PathOf gets the absolute path of a potential setting field in c with name // separated by sep. func (c *Config) PathOf(field, sep string) string { return c.ctx.pathOf(field, sep) } // Parent returns the parent configuration or nil if c is already a root // Configuration. func (c *Config) Parent() *Config { ctx := c.ctx for { if ctx.parent == nil { return nil } switch p := ctx.parent.(type) { case cfgSub: return p.c default: return nil } } } // FlattenedKeys return a sorted flattened views of the set keys in the configuration func (c *Config) FlattenedKeys(opts ...Option) []string { var keys []string normalizedOptions := makeOptions(opts) if normalizedOptions.pathSep == "" { normalizedOptions.pathSep = "." } if c.IsDict() { for _, v := range c.fields.dict() { subcfg, err := v.toConfig(normalizedOptions) if err != nil { ctx := v.Context() p := ctx.path(normalizedOptions.pathSep) keys = append(keys, p) } else { newKeys := subcfg.FlattenedKeys(opts...) keys = append(keys, newKeys...) } } } else if c.IsArray() { for _, a := range c.fields.array() { scfg, err := a.toConfig(normalizedOptions) if err != nil { ctx := a.Context() p := ctx.path(normalizedOptions.pathSep) keys = append(keys, p) } else { newKeys := scfg.FlattenedKeys(opts...) keys = append(keys, newKeys...) } } } sort.Strings(keys) return keys } func (f *fields) get(name string) (value, bool) { if f.d == nil { return nil, false } v, found := f.d[name] return v, found } func (f *fields) dict() map[string]value { return f.d } func (f *fields) array() []value { return f.a } func (f *fields) del(name string) bool { _, exists := f.d[name] if exists { delete(f.d, name) } return exists } func (f *fields) delAt(i int) bool { a := f.a if i < 0 || len(a) <= i { return false } copy(a[i:], a[i+1:]) a[len(a)-1] = nil f.a = a[:len(a)-1] return true } func (f *fields) set(name string, v value) { if f.d == nil { f.d = map[string]value{} } f.d[name] = v } func (f *fields) add(v value) { f.a = append(f.a, v) } func (f *fields) setAt(idx int, parent, v value) { l := len(f.a) if idx >= l { tmp := make([]value, idx+1) copy(tmp, f.a) for i := l; i < idx; i++ { ctx := context{parent: parent, field: fmt.Sprintf("%d", i)} tmp[i] = &cfgNil{cfgPrimitive{ctx, nil}} } f.a = tmp } f.a[idx] = v } func (f *fields) append(parent value, a []value) { l := len(f.a) count := len(a) if count == 0 { return } for i := 0; i < count; i, l = i+1, l+1 { ctx := context{ parent: parent, field: fmt.Sprintf("%v", l), } f.setAt(l, parent, a[i].cpy(ctx)) } } func (o *fieldOptions) configHandling() configHandling { h := o.tag.cfgHandling if h == cfgDefaultHandling { h = o.opts.configValueHandling } return h }