Skip to content

Commit

Permalink
add support for embedding struct configuration with special "_" tag
Browse files Browse the repository at this point in the history
Issue #22
  • Loading branch information
Steve van Loben Sels committed May 9, 2018
1 parent ee865b9 commit c40d9c2
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 2 deletions.
60 changes: 60 additions & 0 deletions load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,3 +483,63 @@ func TestMakeEnvVars(t *testing.T) {
t.Error(envVars)
}
}

func TestEmbeddedStruct(t *testing.T) {

type Child struct {
ChildField1 string
ChildField2 string
}

type Branch struct {
Child `conf:"_"`
BranchField string
}

type Container struct {
Branch `conf:"_"`
OtherBranch Branch
}

testVal := Container{
Branch: Branch{
Child: Child{
ChildField1: "embedded-child-1",
ChildField2: "embedded-child-2",
},
BranchField: "embedded-branch",
},
OtherBranch: Branch{
Child: Child{
ChildField1: "no-embedded-child-1",
ChildField2: "no-embedded-child-2",
},
BranchField: "no-embedded-branch",
},
}

ld := Loader{
Name: "test",
Args: []string{
"-ChildField1", "embedded-child-1",
"-ChildField2", "embedded-child-2",
"-BranchField", "embedded-branch",
"-OtherBranch.ChildField1", "no-embedded-child-1",
"-OtherBranch.ChildField2", "no-embedded-child-2",
"-OtherBranch.BranchField", "no-embedded-branch",
},
}

val := reflect.New(reflect.TypeOf(testVal))

if _, _, err := ld.Load(val.Interface()); err != nil {
t.Error(err)
t.Log("<<<", testVal)
t.Log(">>>", val.Elem().Interface())
return
}

if v := val.Elem().Interface(); !reflect.DeepEqual(testVal, v) {
t.Errorf("bad value:\n<<< %#v\n>>> %#v", testVal, v)
}
}
28 changes: 26 additions & 2 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,25 @@ func makeNodeStruct(v reflect.Value, t reflect.Type) (m Map) {
m.value = v
m.items = newMapItems()

populateNodeStruct(t, t.Name(), v, t, m)

// if using the "_" notation to embed structs, it's possible that names are no longer unique.
props := make(map[string]struct{})
for _, item := range m.Items() {
if _, ok := props[item.Name]; ok {
panic("duplicate name '" + item.Name + "' found after collapsing embedded structs in configuration: " + t.String())
}
props[item.Name] = struct{}{}
}

return
}

// populateNodeStruct is the mutually recursive helper of makeNodeStruct to create the node struct with potentially
// embedded types. It will populate m with the struct fields from v. The original type and path of the current field
// are passed in order to create decent panic strings if an invalid configuration is detected.
func populateNodeStruct(originalT reflect.Type, path string, v reflect.Value, t reflect.Type, m Map) {

for i, n := 0, v.NumField(); i != n; i++ {
fv := v.Field(i)
ft := t.Field(i)
Expand All @@ -190,6 +209,13 @@ func makeNodeStruct(v reflect.Value, t reflect.Type) (m Map) {
switch name {
case "-":
continue
case "_":
path = path + "." + ft.Name
if ft.Type.Kind() != reflect.Struct && !ft.Anonymous {
panic("found \"_\" on invalid type at path " + path + " in configuration: " + originalT.Name())
}
populateNodeStruct(originalT, path, fv, ft.Type, m)
continue
case "":
name = ft.Name
}
Expand All @@ -200,8 +226,6 @@ func makeNodeStruct(v reflect.Value, t reflect.Type) (m Map) {
Value: makeNode(fv),
})
}

return
}

func makeNodeMap(v reflect.Value, t reflect.Type) (m Map) {
Expand Down
95 changes: 95 additions & 0 deletions node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package conf
import (
"fmt"
"reflect"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -457,3 +458,97 @@ func TestNodeJSON(t *testing.T) {
}
})
}

func Test_FlattenedEmbeddedStructs(t *testing.T) {

type Smallest struct {
SmallestOne string
}

type Small struct {
Smallest `conf:"_"`
SmallOne string
}

type Medium struct {
Small `conf:"_"`
MediumOne string
}

type Matroska struct {
Medium `conf:"_"`
LargeOne string
}

m := Matroska{}
node := makeNodeStruct(reflect.ValueOf(m), reflect.TypeOf(m))
if len(node.Items()) != 4 {
t.Errorf("expected to find four flattened fields...got %d", len(node.Items()))
}

for _, name := range []string{"SmallestOne", "SmallOne", "MediumOne", "LargeOne"} {
f := node.Item(name)
if f == nil {
t.Errorf("flattened field %s is missing", name)
}
if f.Kind() != ScalarNode {
t.Errorf("flattened field %s should have been scalar but was %d", name, f.Kind())
}
}
}

func Test_InvalidFlattenedEmbeddedStructs(t *testing.T) {

type Thing1 struct {
Stuff string
}

type Thing2 struct {
Stuff string
}

type ConflictingName struct {
Thing1 `conf:"_"`
Thing2 `conf:"_"`
}

type EmbedPrimitive struct {
Str string `conf:"_"`
}

tests := []struct {
val interface{}
errFragments []string
} {
{
val: ConflictingName{},
errFragments: []string{"'Stuff'", "duplicate"},
},
{
val: EmbedPrimitive{},
errFragments: []string{"\"_\"", "at path EmbedPrimitive.Str"},
},
}

for _, tt := range tests {
t.Run(reflect.TypeOf(tt.val).Name(), func(t *testing.T) {
defer func() {
recovered := recover()
msg, ok := recovered.(string)
if !ok {
t.Errorf("expected a string to be recovered...got %v", recovered)
}

// NOTE : ensure that the type name is included in the message!
for _, frag := range append(tt.errFragments, reflect.TypeOf(tt.val).Name()) {
if !strings.Contains(msg, frag) {
t.Errorf("message should have contained fragment \"%s\": %s", frag, msg)
}
}
}()

makeNodeStruct(reflect.ValueOf(tt.val), reflect.TypeOf(tt.val))
t.Error("test should have paniced")
})
}
}

0 comments on commit c40d9c2

Please sign in to comment.