opts.go (216 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" "os" "strings" "github.com/elastic/go-ucfg/parse" ) // Some sane value for the index fields such as input.0.foo // in order to protect against cases where user specifies input.9223372036854.foo for the key const defaultMaxIdx = 1024 // Option type implementing additional options to be passed // to go-ucfg library functions. type Option func(*options) type options struct { tag string validatorTag string pathSep string escapePath bool meta *Meta env []*Config resolvers []func(name string) (string, parse.Config, error) varexp bool noParse bool maxIdx int64 // Max index field value allowed enableNumKeys bool // Enables numeric keys, example "123" configValueHandling configHandling fieldHandlingTree *fieldHandlingTree // temporary cache of parsed splice values for lifetime of call to // Unpack/Pack/Get/... parsed valueCache activeFields *fieldSet ignoreCommas bool } type valueCache map[string]spliceValue // specific API on top of Config to handle adjusting merging behavior per fields type fieldHandlingTree Config // id used to store intermediate parse results in current execution context. // As parsing results might differ between multiple calls due to: // splice being shared between multiple configurations, or environment // changing between calls + lazy nature of cfgSplice, parsing results cannot // be stored in cfgSplice itself. type cacheID string type spliceValue struct { err error value value } // StructTag option sets the struct tag name to use for looking up // field names and options in `Unpack` and `Merge`. // The default struct tag in `config`. func StructTag(tag string) Option { return func(o *options) { o.tag = tag } } var IgnoreCommas Option = doIgnoreCommas func doIgnoreCommas(o *options) { o.ignoreCommas = true } // ValidatorTag option sets the struct tag name used to set validators // on struct fields in `Unpack`. // The default struct tag in `validate`. func ValidatorTag(tag string) Option { return func(o *options) { o.validatorTag = tag } } // PathSep sets the path separator used to split up names into a tree like hierarchy. // If PathSep is not set, field names will not be split. func PathSep(sep string) Option { return func(o *options) { o.pathSep = sep } } // EscapePath when set allows the user to escape the path using brackets. func EscapePath() Option { return func(o *options) { o.escapePath = true } } // MetaData option passes additional metadata (currently only source of the // configuration) to be stored internally (e.g. for error reporting). func MetaData(meta Meta) Option { return func(o *options) { o.meta = &meta } } // Env option adds another configuration for variable expansion to be used, if // the path to look up does not exist in the actual configuration. Env can be used // multiple times in order to add more lookup environments. func Env(e *Config) Option { return func(o *options) { o.env = append(o.env, e) } } // Resolve option adds a callback used by variable name expansion. The callback // will be called if a variable can not be resolved from within the actual configuration // or any of its environments. func Resolve(fn func(name string) (string, parse.Config, error)) Option { return func(o *options) { o.resolvers = append(o.resolvers, fn) } } // MaxIdx overwrites max index field value allowed. // By default it is limited to defaultMaxIdx value. func MaxIdx(maxIdx int64) Option { return func(o *options) { o.maxIdx = maxIdx } } // EnableNumKeys enables numeric keys, such as "1234" in the configuration. // The numeric key values are converted to array's index otherwise by default. // This feature is disabled by default for backwards compatibility. // This is useful when it's needed to support and preserve the configuration numeric string keys. func EnableNumKeys(enableNumKeys bool) Option { return func(o *options) { o.enableNumKeys = enableNumKeys } } // ResolveEnv option adds a look up callback looking up values in the available // OS environment variables. var ResolveEnv Option = doResolveEnv func doResolveEnv(o *options) { o.resolvers = append(o.resolvers, func(name string) (string, parse.Config, error) { value := os.Getenv(name) if value == "" { return "", parse.EnvConfig, ErrMissing } return value, parse.EnvConfig, nil }) } // ResolveNOOP option add a resolver that will not search the value but instead will return the // provided key wrap with the field reference syntax. This is useful if you don't to expose values // from envionment variable or other resolvers. // // Example: "mysecret" => ${mysecret}" var ResolveNOOP Option = doResolveNOOP func doResolveNOOP(o *options) { o.resolvers = append(o.resolvers, func(name string) (string, parse.Config, error) { return "${" + name + "}", parse.NoopConfig, nil }) } var ( // ReplaceValues option configures all merging and unpacking operations to // replace old dictionaries and arrays while merging. Value merging can be // overwritten in unpack by using struct tags. ReplaceValues = makeOptValueHandling(cfgReplaceValue) // ReplaceArrValues option configures merging and unpacking operations to // replace old arrays while merging. Value merging can be overwritten in unpack // by using struct tags. ReplaceArrValues = makeOptValueHandling(cfgArrReplaceValue) // AppendValues option configures all merging and unpacking operations to // merge dictionaries and append arrays to existing arrays while merging. // Value merging can be overwritten in unpack by using struct tags. AppendValues = makeOptValueHandling(cfgArrAppend) // PrependValues option configures all merging and unpacking operations to // merge dictionaries and prepend arrays to existing arrays while merging. // Value merging can be overwritten in unpack by using struct tags. PrependValues = makeOptValueHandling(cfgArrPrepend) ) func makeOptValueHandling(h configHandling) Option { return func(o *options) { o.configValueHandling = h } } var ( // FieldMergeValues option configures all merging and unpacking operations to use // the default merging behavior for the specified field. This overrides the any struct // tags during unpack for the field. Nested field names can be defined using dot // notation. FieldMergeValues = makeFieldOptValueHandling(cfgMergeValues) // FieldReplaceValues option configures all merging and unpacking operations to // replace old dictionaries and arrays while merging for the specified field. This // overrides the any struct tags during unpack for the field. Nested field names // can be defined using dot notation. FieldReplaceValues = makeFieldOptValueHandling(cfgReplaceValue) // FieldAppendValues option configures all merging and unpacking operations to // merge dictionaries and append arrays to existing arrays while merging for the // specified field. This overrides the any struct tags during unpack for the field. // Nested field names can be defined using dot notation. FieldAppendValues = makeFieldOptValueHandling(cfgArrAppend) // FieldPrependValues option configures all merging and unpacking operations to // merge dictionaries and prepend arrays to existing arrays while merging for the // specified field. This overrides the any struct tags during unpack for the field. // Nested field names can be defined using dot notation. FieldPrependValues = makeFieldOptValueHandling(cfgArrPrepend) ) func makeFieldOptValueHandling(h configHandling) func(...string) Option { return func(fieldName ...string) Option { if len(fieldName) == 0 { return func(_ *options) {} } table := make(map[string]configHandling) for _, name := range fieldName { // field value config options are rendered into a Config; the '*' represents the handling method // for everything nested under this field. if !strings.HasSuffix(name, ".*") { name = fmt.Sprintf("%s.*", name) } table[name] = h } return func(o *options) { if o.fieldHandlingTree == nil { o.fieldHandlingTree = newFieldHandlingTree() } o.fieldHandlingTree.merge(table, PathSep(o.pathSep)) } } } // VarExp option enables support for variable expansion. Resolve and Env options will only be effective if VarExp is set. var VarExp Option = doVarExp func doVarExp(o *options) { o.varexp = true } func makeOptions(opts []Option) *options { o := options{ tag: "config", validatorTag: "validate", pathSep: "", // no separator by default parsed: map[string]spliceValue{}, activeFields: newFieldSet(nil), maxIdx: defaultMaxIdx, } for _, opt := range opts { opt(&o) } return &o } func (cache valueCache) cachedValue( id cacheID, f func() (value, error), ) (value, error) { if v, ok := cache[string(id)]; ok { if v.err != nil { return nil, v.err } return v.value, nil } v, err := f() // Only primitives can be cached, allowing us to get out of infinite loop if v != nil && v.canCache() { cache[string(id)] = spliceValue{err, v} } return v, err } func newFieldHandlingTree() *fieldHandlingTree { return (*fieldHandlingTree)(New()) } func (t *fieldHandlingTree) merge(other interface{}, opts ...Option) error { cfg := (*Config)(t) return cfg.Merge(other, opts...) } func (t *fieldHandlingTree) child(fieldName string, idx int) (*fieldHandlingTree, error) { cfg := (*Config)(t) child, err := cfg.Child(fieldName, idx) if err != nil { return nil, err } return (*fieldHandlingTree)(child), nil } func (t *fieldHandlingTree) configHandling(fieldName string, idx int) (configHandling, error) { cfg := (*Config)(t) handling, err := cfg.Uint(fieldName, idx) if err != nil { return cfgDefaultHandling, err } return configHandling(handling), nil } func (t *fieldHandlingTree) wildcard() (*fieldHandlingTree, error) { return t.child("**", -1) } func (t *fieldHandlingTree) setWildcard(wildcard *fieldHandlingTree) error { cfg := (*Config)(t) return cfg.SetChild("**", -1, (*Config)(wildcard)) } func (t *fieldHandlingTree) fieldHandling(fieldName string, idx int) (configHandling, *fieldHandlingTree, bool) { child, err := t.child(fieldName, idx) if err == nil { cfgHandling, err := child.configHandling("*", -1) if err == nil { return cfgHandling, child, true } } // try wildcard match wildcard, err := t.wildcard() if err != nil { return cfgDefaultHandling, child, false } cfgHandling, cfg, ok := wildcard.fieldHandling(fieldName, idx) if ok { return cfgHandling, cfg, ok } return cfgDefaultHandling, child, ok }