forked from ebhomengo/niki
514 lines
14 KiB
Go
514 lines
14 KiB
Go
|
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
|
||
|
}
|