663 lines
17 KiB
Go
663 lines
17 KiB
Go
package koanf
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/go-viper/mapstructure/v2"
|
|
"github.com/knadh/koanf/maps"
|
|
"github.com/mitchellh/copystructure"
|
|
)
|
|
|
|
// Koanf is the configuration apparatus.
|
|
type Koanf struct {
|
|
confMap map[string]any
|
|
confMapFlat map[string]any
|
|
keyMap KeyMap
|
|
conf Conf
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// 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]any),
|
|
confMapFlat: make(map[string]any),
|
|
keyMap: make(KeyMap),
|
|
conf: conf,
|
|
}
|
|
}
|
|
|
|
// Load takes a Provider that either provides a parsed config map[string]any
|
|
// 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]any
|
|
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 {
|
|
ko.mu.RLock()
|
|
out := make([]string, 0, len(ko.confMapFlat))
|
|
for k := range ko.confMapFlat {
|
|
out = append(out, k)
|
|
}
|
|
ko.mu.RUnlock()
|
|
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 {
|
|
ko.mu.RLock()
|
|
out := make(KeyMap, len(ko.keyMap))
|
|
for key, parts := range ko.keyMap {
|
|
out[key] = make([]string, len(parts))
|
|
copy(out[key], parts)
|
|
}
|
|
ko.mu.RUnlock()
|
|
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]any {
|
|
ko.mu.RLock()
|
|
defer ko.mu.RUnlock()
|
|
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]any {
|
|
ko.mu.RLock()
|
|
defer ko.mu.RUnlock()
|
|
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{}
|
|
keys := ko.Keys()
|
|
ko.mu.RLock()
|
|
for _, k := range keys {
|
|
v := ko.confMapFlat[k]
|
|
b.WriteString(fmt.Sprintf("%s -> %v\n", k, v))
|
|
}
|
|
ko.mu.RUnlock()
|
|
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]any)
|
|
|
|
// Cut only makes sense if the requested key path is a map.
|
|
if v, ok := ko.Get(path).(map[string]any); 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]any{
|
|
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 any) error {
|
|
// Unflatten the config map with the given key path.
|
|
n := maps.Unflatten(map[string]any{
|
|
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 any) 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 any, c UnmarshalConf) error {
|
|
if c.DecoderConfig == nil {
|
|
c.DecoderConfig = &mapstructure.DecoderConfig{
|
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
mapstructure.StringToTimeDurationHookFunc(),
|
|
textUnmarshalerHookFunc()),
|
|
Metadata: nil,
|
|
WeaklyTypedInput: true,
|
|
}
|
|
}
|
|
|
|
c.DecoderConfig.Result = o
|
|
|
|
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]any); 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) {
|
|
ko.mu.Lock()
|
|
defer ko.mu.Unlock()
|
|
|
|
// No path. Erase the entire map.
|
|
if path == "" {
|
|
ko.confMap = make(map[string]any)
|
|
ko.confMapFlat = make(map[string]any)
|
|
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 any 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) any {
|
|
// No path. Return the whole conf map.
|
|
if path == "" {
|
|
return ko.Raw()
|
|
}
|
|
|
|
// Does the path exist?
|
|
ko.mu.RLock()
|
|
defer ko.mu.RUnlock()
|
|
|
|
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]any:
|
|
return maps.Copy(v)
|
|
case nil:
|
|
return nil
|
|
}
|
|
|
|
// Skip nil pointers before copying.
|
|
if rv := reflect.ValueOf(res); rv.Kind() == reflect.Ptr && rv.IsNil() {
|
|
return res
|
|
}
|
|
|
|
out, _ := copystructure.Copy(&res)
|
|
if ptrOut, ok := out.(*any); ok {
|
|
return *ptrOut
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Slices returns a list of Koanf instances constructed from the value
|
|
// at path. It accepts []map[string]any or []any containing map[string]any
|
|
// elements. Non-slice elements are skipped.
|
|
func (ko *Koanf) Slices(path string) []*Koanf {
|
|
out := []*Koanf{}
|
|
if path == "" {
|
|
return out
|
|
}
|
|
|
|
switch v := ko.Get(path).(type) {
|
|
case []map[string]any:
|
|
for _, mp := range v {
|
|
out = ko.appendMap(mp, out)
|
|
}
|
|
case []any:
|
|
for _, item := range v {
|
|
if mp, ok := item.(map[string]any); ok {
|
|
out = ko.appendMap(mp, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// Exists returns true if the given key path exists in the conf map.
|
|
func (ko *Koanf) Exists(path string) bool {
|
|
ko.mu.RLock()
|
|
_, ok := ko.keyMap[path]
|
|
ko.mu.RUnlock()
|
|
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]any)
|
|
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]any, opts *options) error {
|
|
ko.mu.Lock()
|
|
|
|
maps.IntfaceKeysToStrings(c)
|
|
if opts.merge != nil {
|
|
// Deep-copy confMap so the custom merge function can safely call
|
|
// ko.Get*() methods (which acquire a read lock) without deadlocking.
|
|
dest := maps.Copy(ko.confMap)
|
|
|
|
ko.mu.Unlock()
|
|
err := opts.merge(c, dest)
|
|
ko.mu.Lock()
|
|
|
|
if err != nil {
|
|
ko.mu.Unlock()
|
|
return err
|
|
}
|
|
ko.confMap = dest
|
|
} else if ko.conf.StrictMerge {
|
|
if err := maps.MergeStrict(c, ko.confMap); err != nil {
|
|
ko.mu.Unlock()
|
|
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)
|
|
|
|
ko.mu.Unlock()
|
|
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 do a strconv.Atoi
|
|
// to get an integer out.
|
|
func toInt64(v any) (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
|
|
case uint8:
|
|
return int64(i), nil
|
|
case uint16:
|
|
return int64(i), nil
|
|
case uint32:
|
|
return int64(i), nil
|
|
case uint:
|
|
return uintToInt64(uint64(i))
|
|
case uint64:
|
|
return uintToInt64(i)
|
|
}
|
|
|
|
// Force it to a string and try to convert.
|
|
s := fmt.Sprintf("%v", v)
|
|
|
|
// Try parsing as int64 first, and on failure, attempt float64 parsing.
|
|
// Parsing directly as float64, when the number is beyond its upper limit (2^53)
|
|
// causes unnecessary precision loss when the value is actually an int.
|
|
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
|
|
return i, nil
|
|
}
|
|
|
|
f, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// int64(f) is undefined for numbers that are out of range and returns different
|
|
// results on different architectures. float64 cannot exactly represent MaxInt64,
|
|
// so check against the ACTUAL upper limit of 2^63.
|
|
// If neither match, return an error instead of the undefined result.
|
|
if math.IsNaN(f) || f < math.MinInt64 || f >= 1<<63 {
|
|
return 0, fmt.Errorf("value %v overflows int64", v)
|
|
}
|
|
|
|
return int64(f), nil
|
|
}
|
|
|
|
// uintToInt64 converts an unsigned integer to int64 and throw an error on overflow.
|
|
func uintToInt64(v uint64) (int64, error) {
|
|
if v > math.MaxInt64 {
|
|
return 0, fmt.Errorf("value %d overflows int64", v)
|
|
}
|
|
|
|
return int64(v), nil
|
|
}
|
|
|
|
// toInt64 takes a `v any` 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 any) (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 any) (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
|
|
}
|
|
|
|
// textUnmarshalerHookFunc is a fixed version of mapstructure.TextUnmarshallerHookFunc.
|
|
// This hook allows to additionally unmarshal text into custom string types that implement the encoding.Text(Un)Marshaler interface(s).
|
|
func textUnmarshalerHookFunc() mapstructure.DecodeHookFuncType {
|
|
return func(
|
|
f reflect.Type,
|
|
t reflect.Type,
|
|
data any,
|
|
) (any, error) {
|
|
if f.Kind() != reflect.String {
|
|
return data, nil
|
|
}
|
|
result := reflect.New(t).Interface()
|
|
unmarshaller, ok := result.(encoding.TextUnmarshaler)
|
|
if !ok {
|
|
return data, nil
|
|
}
|
|
|
|
// default text representation is the actual value of the `from` string
|
|
var (
|
|
dataVal = reflect.ValueOf(data)
|
|
text = []byte(dataVal.String())
|
|
)
|
|
if f.Kind() == t.Kind() {
|
|
// source and target are of underlying type string
|
|
var (
|
|
err error
|
|
ptrVal = reflect.New(dataVal.Type())
|
|
)
|
|
if !ptrVal.Elem().CanSet() {
|
|
// cannot set, skip, this should not happen
|
|
if err := unmarshaller.UnmarshalText(text); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
ptrVal.Elem().Set(dataVal)
|
|
|
|
// We need to assert that both, the value type and the pointer type
|
|
// do (not) implement the TextMarshaller interface before proceeding and simply
|
|
// using the string value of the string type.
|
|
// it might be the case that the internal string representation differs from
|
|
// the (un)marshalled string.
|
|
|
|
for _, v := range []reflect.Value{dataVal, ptrVal} {
|
|
if marshaller, ok := v.Interface().(encoding.TextMarshaler); ok {
|
|
text, err = marshaller.MarshalText()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// text is either the source string's value or the source string type's marshaled value
|
|
// which may differ from its internal string value.
|
|
if err := unmarshaller.UnmarshalText(text); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
// appendMap creates new Koanf instances from a map returns a slice of Koanf instances.
|
|
func (ko *Koanf) appendMap(mp map[string]any, out []*Koanf) []*Koanf {
|
|
k := New(ko.conf.Delim)
|
|
_ = k.merge(mp, new(options))
|
|
return append(out, k)
|
|
}
|