Skip to content

Commit

Permalink
Merge values for provider group in the constructor (#48)
Browse files Browse the repository at this point in the history
ProviderGroup can and should merge values on construction, instead of on value retrieval.
To do that we need need apply expand to all nested YAML nodes, instead of only top level ones.
  • Loading branch information
Alex authored Jul 31, 2017
1 parent 7d6a5c6 commit e1caf0b
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 174 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
other cast libraries.
- **[Breaking]** Removed `Value.IsDefault` method.
- **[Breaking]** Removed Load* functions.
- **[Breaking]** Removed NewYAMLProviderFromReader* functions.
- **[Breaking]** Unexport NewYAMLProviderFromReader* functions.
- **[Breaking]** `NewProviderGroup` returns an error.

## v1.0.0-rc1 (26 Jun 2017)

Expand Down
122 changes: 0 additions & 122 deletions config.go

This file was deleted.

13 changes: 6 additions & 7 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestDirectAccess(t *testing.T) {
p, err := NewYAMLProviderFromBytes(nestedYaml)
require.NoError(t, err, "Can't create a YAML provider")

provider := NewProviderGroup("test", p)
provider, err := NewProviderGroup("test", p)
require.NoError(t, err)

v, err := provider.Get("n1.id1").WithDefault("xxx")
Expand All @@ -119,8 +119,7 @@ func TestScopedAccess(t *testing.T) {

p, err := NewYAMLProviderFromBytes(nestedYaml)
require.NoError(t, err, "Can't create a YAML provider")

provider := NewProviderGroup("test", p)
provider, err := NewProviderGroup("test", p)
require.NoError(t, err)

p1 := provider.Get("n1")
Expand All @@ -141,7 +140,7 @@ func TestSimpleConfigValues(t *testing.T) {
p, err := NewYAMLProviderFromBytes(yamlConfig3)
require.NoError(t, err, "Can't create a YAML provider")

provider := NewProviderGroup("test", p)
provider, err := NewProviderGroup("test", p)
require.NoError(t, err)

assert.Equal(t, 123, provider.Get("int").Value())
Expand All @@ -165,7 +164,7 @@ func TestNestedStructs(t *testing.T) {
p, err := NewYAMLProviderFromBytes(nestedYaml)
require.NoError(t, err, "Can't create a YAML provider")

provider := NewProviderGroup("test", p)
provider, err := NewProviderGroup("test", p)
require.NoError(t, err)

str := &root{}
Expand All @@ -189,7 +188,7 @@ func TestArrayOfStructs(t *testing.T) {
p, err := NewYAMLProviderFromBytes(structArrayYaml)
require.NoError(t, err, "Can't create a YAML provider")

provider := NewProviderGroup("test", p)
provider, err := NewProviderGroup("test", p)
require.NoError(t, err)

target := &arrayOfStructs{}
Expand All @@ -208,7 +207,7 @@ func TestDefault(t *testing.T) {
p, err := NewYAMLProviderFromBytes(nest1)
require.NoError(t, err, "Can't create a YAML provider")

provider := NewProviderGroup("test", p)
provider, err := NewProviderGroup("test", p)
require.NoError(t, err)

target := &nested{}
Expand Down
5 changes: 4 additions & 1 deletion example_merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ config:

// Provider is going to keep name from the base provider,
// but ports and pool values will be overridden by prod.
p := config.NewProviderGroup("merge", base, prod)
p, err := config.NewProviderGroup("merge", base, prod)
if err != nil {
log.Fatal(err)
}

var c struct {
Name string
Expand Down
24 changes: 9 additions & 15 deletions provider_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,29 @@
package config

type providerGroup struct {
name string
providers []Provider
name string
}

// NewProviderGroup creates a configuration provider from a group of providers.
// The highest priority provider is the last.
func NewProviderGroup(name string, providers ...Provider) Provider {
return providerGroup{
name: name,
providers: providers,
}
}

// Get iterates through the providers and return the value merged from
// underlying of providers.
//
// The merge strategy for two objects
// from the first provider(A) and the last provider(B) is following:
// * if A and B are maps, A and B will form a new map with keys from
// If A and B are maps, A and B will form a new map with keys from
// A and B and values from B will overwrite values of A. e.g.
// A: B: merge(A, B):
// keep:A new:B keep:A
// update:fromA update:fromB update:fromB
// new:B
//
// * if A is a map and B is not, this function will panic,
// e.g. key:value and -slice
// If A is a map and B is not, this function will return a Value with
// an error inside.
//
// * in all the remaining cases B will overwrite A.
// In all the remaining cases B will overwrite A.
func NewProviderGroup(name string, providers ...Provider) (Provider, error) {
return providerGroup{providers: providers, name: name}, nil
}

func (p providerGroup) Get(key string) Value {
var res interface{}
found := false
Expand Down
29 changes: 11 additions & 18 deletions provider_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ func TestProviderGroup(t *testing.T) {
p, err := NewYAMLProviderFromBytes([]byte(`id: test`))
require.NoError(t, err, "Can't create a YAML provider")

pg := NewProviderGroup("test-group", p)
pg, err := NewProviderGroup("test-group", p)
require.NoError(t, err)

assert.Equal(t, "test-group", pg.Name())
assert.Equal(t, "test", pg.Get("id").String())
}
Expand All @@ -44,7 +46,9 @@ func TestProviderGroupScope(t *testing.T) {
p, err := NewStaticProvider(map[string]interface{}{"hello": map[string]int{"world": 42}})
require.NoError(t, err, "Can't create a static provider")

pg := NewProviderGroup("test-group", p)
pg, err := NewProviderGroup("test-group", p)
require.NoError(t, err)

assert.Equal(t, 42, pg.Get("hello").Get("world").Value())
}

Expand Down Expand Up @@ -87,7 +91,9 @@ logging:
f, err := NewYAMLProviderFromBytes(fst)
require.NoError(t, err, "Can't create a YAML provider")

pg := NewProviderGroup("group", s, f)
pg, err := NewProviderGroup("group", s, f)
require.NoError(t, err)

assert.True(t, pg.Get("logging").Get("enabled").Value().(bool))
}

Expand All @@ -100,23 +106,10 @@ func TestProviderGroup_GetChecksAllProviders(t *testing.T) {
s, err := NewStaticProvider(map[string]string{"owner": "[email protected]", "name": "fx"})
require.NoError(t, err, "Can't create the second provider")

pg := NewProviderGroup("test-group", f, s)
require.NotNil(t, pg)
pg, err := NewProviderGroup("test-group", f, s)
require.NoError(t, err)

var svc map[string]string
require.NoError(t, pg.Get(Root).Populate(&svc))
assert.Equal(t, map[string]string{"name": "fx", "owner": "[email protected]", "desc": "test"}, svc)
}

func TestProviderGroupMergeFail(t *testing.T) {
t.Parallel()

m, err := NewStaticProvider(map[string]interface{}{"a": map[string]string{"b": "c"}})
require.NoError(t, err)
s, err := NewStaticProvider(map[string]interface{}{"a": []string{"b"}})
require.NoError(t, err)

g := NewProviderGroup("group", s, m)
v := g.Get("a")
assert.False(t, v.HasValue())
}
6 changes: 5 additions & 1 deletion value.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ func (cv Value) WithDefault(value interface{}) (Value, error) {
return cv, err
}

g := NewProviderGroup("withDefault", p, cv.provider)
g, err := NewProviderGroup("withDefault", p, cv.provider)
if err != nil {
return cv, err
}

return g.Get(cv.key), nil
}

Expand Down
43 changes: 35 additions & 8 deletions yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,23 @@ func newYAMLProviderCore(files ...io.ReadCloser) (*yamlConfigProvider, error) {
}, nil
}

// We need to have a custom merge map because yamlV2 doesn't unmarshal `map[interface{}]map[interface{}]interface{}`
// as we expect: it will replace second level maps with new maps on each unmarshal call, instead of merging them.
// We need to have a custom merge map because yamlV2 doesn't unmarshal
// `map[interface{}]map[interface{}]interface{}` as we expect: it will
// replace second level maps with new maps on each unmarshal call,
// instead of merging them.
//
// The merge strategy for two objects A and B is following:
// * if A and B are maps, A and B will form a new map with keys from A and B and values from B will overwrite values of A. e.g.
// If A and B are maps, A and B will form a new map with keys from A and B and
// values from B will overwrite values of A. e.g.:
// A: B: merge(A, B):
// keep:A new:B keep:A
// update:fromA update:fromB update:fromB
// new:B
//
// * if A is a map and B is not, this function will panic, e.g. key:value and -slice
// If A is a map and B is not, this function will return an error,
// e.g. key:value and -slice.
//
// * in all the remaining cases B will overwrite A.
// In all the remaining cases B will overwrite A.
func mergeMaps(dst interface{}, src interface{}) (interface{}, error) {
if dst == nil {
return src, nil
Expand Down Expand Up @@ -321,6 +326,30 @@ func (n *yamlNode) Children() []*yamlNode {
return nodes
}

// Apply expand to all nested elements of a node.
// There is no need to use reflection, because YAML unmarshaler is using
// map[interface{}]interface{} to store objects and []interface{}
// to store collections.
func recursiveApply(node interface{}, expand func(string) string) interface{} {
if node == nil {
return nil
}
switch t := node.(type) {
case map[interface{}]interface{}:
for k := range t {
t[k] = recursiveApply(t[k], expand)
}
return t
case []interface{}:
for i := range t {
t[i] = recursiveApply(t[i], expand)
}
return t
}

return os.Expand(fmt.Sprint(node), expand)
}

func (n *yamlNode) applyOnAllNodes(expand func(string) string) (err error) {

defer func() {
Expand All @@ -333,9 +362,7 @@ func (n *yamlNode) applyOnAllNodes(expand func(string) string) (err error) {
}
}()

if n.nodeType == valueNode && n.value != nil {
n.value = os.Expand(fmt.Sprint(n.value), expand)
}
n.value = recursiveApply(n.value, expand)

for _, c := range n.Children() {
if err := c.applyOnAllNodes(expand); err != nil {
Expand Down
4 changes: 3 additions & 1 deletion yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ func withYamlBytes(yamlBytes []byte, f func(Provider), t *testing.T) {
y, err := NewYAMLProviderFromBytes(yamlBytes)
require.NoError(t, err, "Can't create a YAML provider")

provider := NewProviderGroup("global", y)
provider, err := NewProviderGroup("global", y)
require.NoError(t, err)

f(provider)
}

Expand Down

0 comments on commit e1caf0b

Please sign in to comment.