-
Notifications
You must be signed in to change notification settings - Fork 158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: control merging behavior #66
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
.env | ||
.env | ||
|
||
# IDE | ||
.idea |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,21 @@ type Koanf struct { | |
confMap map[string]interface{} | ||
confMapFlat map[string]interface{} | ||
keyMap KeyMap | ||
delim string | ||
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 | ||
|
@@ -55,10 +69,23 @@ type UnmarshalConf struct { | |
// or a / for `parent/child/key`. | ||
func New(delim string) *Koanf { | ||
return &Koanf{ | ||
delim: delim, | ||
confMap: make(map[string]interface{}), | ||
confMapFlat: make(map[string]interface{}), | ||
keyMap: make(KeyMap), | ||
conf: 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, | ||
} | ||
} | ||
|
||
|
@@ -94,8 +121,7 @@ func (ko *Koanf) Load(p Provider, pa Parser) error { | |
} | ||
} | ||
|
||
ko.merge(mp) | ||
return nil | ||
return ko.merge(mp) | ||
} | ||
|
||
// Keys returns the slice of all flattened keys in the loaded configuration | ||
|
@@ -164,8 +190,8 @@ func (ko *Koanf) Cut(path string) *Koanf { | |
out = v | ||
} | ||
|
||
n := New(ko.delim) | ||
n.merge(out) | ||
n := New(ko.conf.Delim) | ||
_ = n.merge(out) | ||
return n | ||
} | ||
|
||
|
@@ -176,27 +202,26 @@ func (ko *Koanf) Copy() *Koanf { | |
|
||
// Merge merges the config map of a given Koanf instance into | ||
// the current instance. | ||
func (ko *Koanf) Merge(in *Koanf) { | ||
ko.merge(in.Raw()) | ||
func (ko *Koanf) Merge(in *Koanf) error { | ||
return ko.merge(in.Raw()) | ||
} | ||
|
||
// 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) { | ||
func (ko *Koanf) MergeAt(in *Koanf, path string) error { | ||
// No path. Merge the two config maps. | ||
if path == "" { | ||
ko.Merge(in) | ||
return | ||
return ko.Merge(in) | ||
} | ||
|
||
// Unflatten the config map with the given key path. | ||
n := maps.Unflatten(map[string]interface{}{ | ||
path: in.Raw(), | ||
}, ko.delim) | ||
}, ko.conf.Delim) | ||
|
||
ko.merge(n) | ||
return ko.merge(n) | ||
} | ||
|
||
// Marshal takes a Parser implementation and marshals the config map into bytes, | ||
|
@@ -242,7 +267,7 @@ func (ko *Koanf) UnmarshalWithConf(path string, o interface{}, c UnmarshalConf) | |
mp := ko.Get(path) | ||
if c.FlatPaths { | ||
if f, ok := mp.(map[string]interface{}); ok { | ||
fmp, _ := maps.Flatten(f, nil, ko.delim) | ||
fmp, _ := maps.Flatten(f, nil, ko.conf.Delim) | ||
mp = fmp | ||
} | ||
} | ||
|
@@ -270,8 +295,8 @@ func (ko *Koanf) Delete(path string) { | |
maps.Delete(ko.confMap, p) | ||
|
||
// Update the flattened version as well. | ||
ko.confMapFlat, ko.keyMap = maps.Flatten(ko.confMap, nil, ko.delim) | ||
ko.keyMap = populateKeyParts(ko.keyMap, ko.delim) | ||
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 | ||
|
@@ -327,7 +352,7 @@ func (ko *Koanf) Slices(path string) []*Koanf { | |
continue | ||
} | ||
|
||
k := New(ko.delim) | ||
k := New(ko.conf.Delim) | ||
k.Load(confmap.Provider(v, ""), nil) | ||
out = append(out, k) | ||
} | ||
|
@@ -365,13 +390,22 @@ func (ko *Koanf) MapKeys(path string) []string { | |
return out | ||
} | ||
|
||
func (ko *Koanf) merge(c map[string]interface{}) { | ||
func (ko *Koanf) merge(c map[string]interface{}) error{ | ||
maps.IntfaceKeysToStrings(c) | ||
maps.Merge(c, ko.confMap) | ||
if ko.conf.StrictMerge { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
err := maps.MergeStrict(c, ko.confMap) | ||
if 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.delim) | ||
ko.keyMap = populateKeyParts(ko.keyMap, ko.delim) | ||
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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,9 +6,22 @@ package maps | |
import ( | ||
"fmt" | ||
"github.com/mitchellh/copystructure" | ||
"reflect" | ||
"strings" | ||
) | ||
|
||
type MergeStrictError struct { | ||
Errors []error | ||
} | ||
|
||
func (m *MergeStrictError) Error() string { | ||
var msg string | ||
for _, err := range m.Errors { | ||
msg += fmt.Sprintf("%v\n", err.Error()) | ||
} | ||
return msg | ||
} | ||
|
||
// Flatten takes a map[string]interface{} and traverses it and flattens | ||
// nested children into keys delimited by delim. | ||
// | ||
|
@@ -132,6 +145,51 @@ func Merge(a, b map[string]interface{}) { | |
} | ||
} | ||
|
||
func MergeStrict(a, b map[string]interface{}) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing comment. |
||
mergeError := MergeStrictError{Errors: []error{}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if skipping failures and collecting all errors is beneficial. We can just keep this idiomatic and return on the first error. That'll also combine There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming I have 10 errors, that would require 10 runs to figure out all the errors. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That could be said of any function that does multiple things, but collecting errors like that is an uncommon pattern. For instance, a JSON payload passed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
mergeStrict(a, b, &mergeError, "") | ||
if len(mergeError.Errors) > 0 { | ||
return &mergeError | ||
} | ||
return nil | ||
} | ||
|
||
func mergeStrict(a, b map[string]interface{}, mergeError *MergeStrictError, fullKey string) { | ||
for key, val := range a { | ||
// Does the key exist in the target map? | ||
// If no, add it and move on. | ||
bVal, ok := b[key] | ||
if !ok { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we keep There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still need the |
||
b[key] = val | ||
continue | ||
} | ||
|
||
newFullKey := key | ||
if fullKey != "" { | ||
newFullKey = fmt.Sprintf("%v.%v", fullKey, key) | ||
} | ||
|
||
// If the incoming val is not a map, do a direct merge between the same types. | ||
if _, ok := val.(map[string]interface{}); !ok { | ||
if reflect.TypeOf(b[key]) == reflect.TypeOf(val) { | ||
b[key] = val | ||
} else { | ||
err := fmt.Errorf("incorrect types at key %v, type %T != %T", fullKey, b[key], val) | ||
mergeError.Errors = append(mergeError.Errors, err) | ||
} | ||
continue | ||
} | ||
|
||
// The source key and target keys are both maps. Merge them. | ||
switch v := bVal.(type) { | ||
case map[string]interface{}: | ||
mergeStrict(val.(map[string]interface{}), v, mergeError, newFullKey) | ||
default: | ||
b[key] = val | ||
} | ||
} | ||
} | ||
|
||
// Delete removes the entry present at a given path, from the map. The path | ||
// is the key map slice, for eg:, parent.child.key -> [parent child key]. | ||
// Any empty, nested map on the path, is recursively deleted. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can just return this: