package koanf import ( "bytes" "fmt" "sort" "strconv" "github.com/knadh/koanf/maps" "github.com/mitchellh/copystructure" "github.com/mitchellh/mapstructure" ) // Koanf is the configuration apparatus. type Koanf struct { confMap map[string]interface{} confMapFlat map[string]interface{} keyMap KeyMap conf Conf } // Conf is the Koanf configuration. type Conf struct { // Delim is the delimiter to use // when specifying config key paths, for instance a . for `parent.child.key` // or a / for `parent/child/key`. Delim string // StrictMerge makes the merging behavior strict. // Meaning when loading two files that have the same key, // the first loaded file will define the desired type, and if the second file loads // a different type will cause an error. StrictMerge bool } // KeyMap represents a map of flattened delimited keys and the non-delimited // parts as their slices. For nested keys, the map holds all levels of path combinations. // For example, the nested structure `parent -> child -> key` will produce the map: // parent.child.key => [parent, child, key] // parent.child => [parent, child] // parent => [parent] type KeyMap map[string][]string // UnmarshalConf represents configuration options used by // Unmarshal() to unmarshal conf maps into arbitrary structs. type UnmarshalConf struct { // Tag is the struct field tag to unmarshal. // `koanf` is used if left empty. Tag string // If this is set to true, instead of unmarshalling nested structures // based on the key path, keys are taken literally to unmarshal into // a flat struct. For example: // ``` // type MyStuff struct { // Child1Name string `koanf:"parent1.child1.name"` // Child2Name string `koanf:"parent2.child2.name"` // Type string `koanf:"json"` // } // ``` FlatPaths bool DecoderConfig *mapstructure.DecoderConfig } // New returns a new instance of Koanf. delim is the delimiter to use // when specifying config key paths, for instance a . for `parent.child.key` // or a / for `parent/child/key`. func New(delim string) *Koanf { return NewWithConf(Conf{ Delim: delim, StrictMerge: false, }) } // NewWithConf returns a new instance of Koanf based on the Conf. func NewWithConf(conf Conf) *Koanf { return &Koanf{ confMap: make(map[string]interface{}), confMapFlat: make(map[string]interface{}), keyMap: make(KeyMap), conf: conf, } } // Load takes a Provider that either provides a parsed config map[string]interface{} // in which case pa (Parser) can be nil, or raw bytes to be parsed, where a Parser // can be provided to parse. Additionally, options can be passed which modify the // load behavior, such as passing a custom merge function. func (ko *Koanf) Load(p Provider, pa Parser, opts ...Option) error { var ( mp map[string]interface{} err error ) if p == nil { return fmt.Errorf("load received a nil provider") } // No Parser is given. Call the Provider's Read() method to get // the config map. if pa == nil { mp, err = p.Read() if err != nil { return err } } else { // There's a Parser. Get raw bytes from the Provider to parse. b, err := p.ReadBytes() if err != nil { return err } mp, err = pa.Unmarshal(b) if err != nil { return err } } return ko.merge(mp, newOptions(opts)) } // Keys returns the slice of all flattened keys in the loaded configuration // sorted alphabetically. func (ko *Koanf) Keys() []string { out := make([]string, 0, len(ko.confMapFlat)) for k := range ko.confMapFlat { out = append(out, k) } sort.Strings(out) return out } // KeyMap returns a map of flattened keys and the individual parts of the // key as slices. eg: "parent.child.key" => ["parent", "child", "key"] func (ko *Koanf) KeyMap() KeyMap { out := make(KeyMap, len(ko.keyMap)) for key, parts := range ko.keyMap { out[key] = make([]string, len(parts)) copy(out[key][:], parts[:]) } return out } // All returns a map of all flattened key paths and their values. // Note that it uses maps.Copy to create a copy that uses // json.Marshal which changes the numeric types to float64. func (ko *Koanf) All() map[string]interface{} { return maps.Copy(ko.confMapFlat) } // Raw returns a copy of the full raw conf map. // Note that it uses maps.Copy to create a copy that uses // json.Marshal which changes the numeric types to float64. func (ko *Koanf) Raw() map[string]interface{} { return maps.Copy(ko.confMap) } // Sprint returns a key -> value string representation // of the config map with keys sorted alphabetically. func (ko *Koanf) Sprint() string { b := bytes.Buffer{} for _, k := range ko.Keys() { b.Write([]byte(fmt.Sprintf("%s -> %v\n", k, ko.confMapFlat[k]))) } return b.String() } // Print prints a key -> value string representation // of the config map with keys sorted alphabetically. func (ko *Koanf) Print() { fmt.Print(ko.Sprint()) } // Cut cuts the config map at a given key path into a sub map and // returns a new Koanf instance with the cut config map loaded. // For instance, if the loaded config has a path that looks like // parent.child.sub.a.b, `Cut("parent.child")` returns a new Koanf // instance with the config map `sub.a.b` where everything above // `parent.child` are cut out. func (ko *Koanf) Cut(path string) *Koanf { out := make(map[string]interface{}) // Cut only makes sense if the requested key path is a map. if v, ok := ko.Get(path).(map[string]interface{}); ok { out = v } n := New(ko.conf.Delim) _ = n.merge(out, new(options)) return n } // Copy returns a copy of the Koanf instance. func (ko *Koanf) Copy() *Koanf { return ko.Cut("") } // Merge merges the config map of a given Koanf instance into // the current instance. func (ko *Koanf) Merge(in *Koanf) error { return ko.merge(in.Raw(), new(options)) } // MergeAt merges the config map of a given Koanf instance into // the current instance as a sub map, at the given key path. // If all or part of the key path is missing, it will be created. // If the key path is `""`, this is equivalent to Merge. func (ko *Koanf) MergeAt(in *Koanf, path string) error { // No path. Merge the two config maps. if path == "" { return ko.Merge(in) } // Unflatten the config map with the given key path. n := maps.Unflatten(map[string]interface{}{ path: in.Raw(), }, ko.conf.Delim) return ko.merge(n, new(options)) } // Set sets the value at a specific key. func (ko *Koanf) Set(key string, val interface{}) error { // Unflatten the config map with the given key path. n := maps.Unflatten(map[string]interface{}{ key: val, }, ko.conf.Delim) return ko.merge(n, new(options)) } // Marshal takes a Parser implementation and marshals the config map into bytes, // for example, to TOML or JSON bytes. func (ko *Koanf) Marshal(p Parser) ([]byte, error) { return p.Marshal(ko.Raw()) } // Unmarshal unmarshals a given key path into the given struct using // the mapstructure lib. If no path is specified, the whole map is unmarshalled. // `koanf` is the struct field tag used to match field names. To customize, // use UnmarshalWithConf(). It uses the mitchellh/mapstructure package. func (ko *Koanf) Unmarshal(path string, o interface{}) error { return ko.UnmarshalWithConf(path, o, UnmarshalConf{}) } // UnmarshalWithConf is like Unmarshal but takes configuration params in UnmarshalConf. // See mitchellh/mapstructure's DecoderConfig for advanced customization // of the unmarshal behaviour. func (ko *Koanf) UnmarshalWithConf(path string, o interface{}, c UnmarshalConf) error { if c.DecoderConfig == nil { c.DecoderConfig = &mapstructure.DecoderConfig{ DecodeHook: mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), mapstructure.TextUnmarshallerHookFunc()), Metadata: nil, Result: o, WeaklyTypedInput: true, } } if c.Tag == "" { c.DecoderConfig.TagName = "koanf" } else { c.DecoderConfig.TagName = c.Tag } d, err := mapstructure.NewDecoder(c.DecoderConfig) if err != nil { return err } // Unmarshal using flat key paths. mp := ko.Get(path) if c.FlatPaths { if f, ok := mp.(map[string]interface{}); ok { fmp, _ := maps.Flatten(f, nil, ko.conf.Delim) mp = fmp } } return d.Decode(mp) } // Delete removes all nested values from a given path. // Clears all keys/values if no path is specified. // Every empty, key on the path, is recursively deleted. func (ko *Koanf) Delete(path string) { // No path. Erase the entire map. if path == "" { ko.confMap = make(map[string]interface{}) ko.confMapFlat = make(map[string]interface{}) ko.keyMap = make(KeyMap) return } // Does the path exist? p, ok := ko.keyMap[path] if !ok { return } maps.Delete(ko.confMap, p) // Update the flattened version as well. ko.confMapFlat, ko.keyMap = maps.Flatten(ko.confMap, nil, ko.conf.Delim) ko.keyMap = populateKeyParts(ko.keyMap, ko.conf.Delim) } // Get returns the raw, uncast interface{} value of a given key path // in the config map. If the key path does not exist, nil is returned. func (ko *Koanf) Get(path string) interface{} { // No path. Return the whole conf map. if path == "" { return ko.Raw() } // Does the path exist? p, ok := ko.keyMap[path] if !ok { return nil } res := maps.Search(ko.confMap, p) // Non-reference types are okay to return directly. // Other types are "copied" with maps.Copy or json.Marshal // that change the numeric types to float64. switch v := res.(type) { case int, int8, int16, int32, int64, float32, float64, string, bool: return v case map[string]interface{}: return maps.Copy(v) } out, _ := copystructure.Copy(&res) if ptrOut, ok := out.(*interface{}); ok { return *ptrOut } return out } // Slices returns a list of Koanf instances constructed out of a // []map[string]interface{} interface at the given path. func (ko *Koanf) Slices(path string) []*Koanf { out := []*Koanf{} if path == "" { return out } // Does the path exist? sl, ok := ko.Get(path).([]interface{}) if !ok { return out } for _, s := range sl { mp, ok := s.(map[string]interface{}) if !ok { continue } k := New(ko.conf.Delim) _ = k.merge(mp, new(options)) out = append(out, k) } return out } // Exists returns true if the given key path exists in the conf map. func (ko *Koanf) Exists(path string) bool { _, ok := ko.keyMap[path] return ok } // MapKeys returns a sorted string list of keys in a map addressed by the // given path. If the path is not a map, an empty string slice is // returned. func (ko *Koanf) MapKeys(path string) []string { var ( out = []string{} o = ko.Get(path) ) if o == nil { return out } mp, ok := o.(map[string]interface{}) if !ok { return out } out = make([]string, 0, len(mp)) for k := range mp { out = append(out, k) } sort.Strings(out) return out } // Delim returns delimiter in used by this instance of Koanf. func (ko *Koanf) Delim() string { return ko.conf.Delim } func (ko *Koanf) merge(c map[string]interface{}, opts *options) error { maps.IntfaceKeysToStrings(c) if opts.merge != nil { if err := opts.merge(c, ko.confMap); err != nil { return err } } else if ko.conf.StrictMerge { if err := maps.MergeStrict(c, ko.confMap); err != nil { return err } } else { maps.Merge(c, ko.confMap) } // Maintain a flattened version as well. ko.confMapFlat, ko.keyMap = maps.Flatten(ko.confMap, nil, ko.conf.Delim) ko.keyMap = populateKeyParts(ko.keyMap, ko.conf.Delim) return nil } // toInt64 takes an interface value and if it is an integer type, // converts and returns int64. If it's any other type, // forces it to a string and attempts to an strconv.Atoi // to get an integer out. func toInt64(v interface{}) (int64, error) { switch i := v.(type) { case int: return int64(i), nil case int8: return int64(i), nil case int16: return int64(i), nil case int32: return int64(i), nil case int64: return i, nil } // Force it to a string and try to convert. f, err := strconv.ParseFloat(fmt.Sprintf("%v", v), 64) if err != nil { return 0, err } return int64(f), nil } // toInt64 takes a `v interface{}` value and if it is a float type, // converts and returns a `float64`. If it's any other type, forces it to a // string and attempts to get a float out using `strconv.ParseFloat`. func toFloat64(v interface{}) (float64, error) { switch i := v.(type) { case float32: return float64(i), nil case float64: return i, nil } // Force it to a string and try to convert. f, err := strconv.ParseFloat(fmt.Sprintf("%v", v), 64) if err != nil { return f, err } return f, nil } // toBool takes an interface value and if it is a bool type, // returns it. If it's any other type, forces it to a string and attempts // to parse it as a bool using strconv.ParseBool. func toBool(v interface{}) (bool, error) { if b, ok := v.(bool); ok { return b, nil } // Force it to a string and try to convert. b, err := strconv.ParseBool(fmt.Sprintf("%v", v)) if err != nil { return b, err } return b, nil } // populateKeyParts iterates a key map and generates all possible // traversal paths. For instance, `parent.child.key` generates // `parent`, and `parent.child`. func populateKeyParts(m KeyMap, delim string) KeyMap { out := make(KeyMap, len(m)) // The size of the result is at very least same to KeyMap for _, parts := range m { // parts is a slice of [parent, child, key] var nk string for i := range parts { if i == 0 { // On first iteration only use first part nk = parts[i] } else { // If nk already contains a part (e.g. `parent`) append delim + `child` nk += delim + parts[i] } if _, ok := out[nk]; ok { continue } out[nk] = make([]string, i+1) copy(out[nk][:], parts[0:i+1]) } } return out }