aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/config
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2017-06-02 14:25:44 +0200
committerDmitry Vyukov <dvyukov@google.com>2017-06-03 10:41:09 +0200
commit96b8d4e99c7812f91633ea6cd1aee5867965e742 (patch)
treeaec8a76b057b1b245856f7bcb3ad00a9b9f47bc8 /pkg/config
parent46c6ed89bf1a7de94496b853608ecd6f80776b58 (diff)
pkg/config: support nested structs
Diffstat (limited to 'pkg/config')
-rw-r--r--pkg/config/config.go52
-rw-r--r--pkg/config/config_test.go131
2 files changed, 174 insertions, 9 deletions
diff --git a/pkg/config/config.go b/pkg/config/config.go
index feb031c71..461ebdabc 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -33,29 +33,67 @@ func load(data []byte, cfg interface{}) error {
}
func checkUnknownFields(data []byte, typ reflect.Type) error {
- if typ.Kind() != reflect.Ptr {
+ if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return fmt.Errorf("config type is not pointer to struct")
}
- typ = typ.Elem()
+ return checkUnknownFieldsRec(data, "", typ)
+}
+
+func checkUnknownFieldsRec(data []byte, prefix string, typ reflect.Type) error {
+ if typ.Kind() == reflect.Ptr {
+ typ = typ.Elem()
+ }
if typ.Kind() != reflect.Struct {
return fmt.Errorf("config type is not pointer to struct")
}
- fields := make(map[string]bool)
+ fields := make(map[string]reflect.Type)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Tag.Get("json") == "-" {
continue
}
- fields[strings.ToLower(field.Name)] = true
+ fields[strings.ToLower(field.Name)] = field.Type
}
f := make(map[string]interface{})
if err := json.Unmarshal(data, &f); err != nil {
return fmt.Errorf("failed to parse config file: %v", err)
}
- for k := range f {
- if !fields[strings.ToLower(k)] {
- return fmt.Errorf("unknown field '%v' in config", k)
+ for k, v := range f {
+ field, ok := fields[strings.ToLower(k)]
+ if !ok {
+ return fmt.Errorf("unknown field '%v%v' in config", prefix, k)
+ }
+ if field.Kind() == reflect.Slice &&
+ (field.PkgPath() != "encoding/json" || field.Name() != "RawMessage") {
+ vv := reflect.ValueOf(v)
+ if vv.Type().Kind() != reflect.Slice {
+ return fmt.Errorf("bad json array type '%v%v'", prefix, k)
+ }
+ for i := 0; i < vv.Len(); i++ {
+ e := vv.Index(i).Interface()
+ prefix1 := fmt.Sprintf("%v%v[%v].", prefix, k, i)
+ if err := checkUnknownFieldsStruct(e, prefix1, field.Elem()); err != nil {
+ return err
+ }
+ }
+ }
+ if err := checkUnknownFieldsStruct(v, prefix+k+".", field); err != nil {
+ return err
}
}
return nil
}
+
+func checkUnknownFieldsStruct(val interface{}, prefix string, typ reflect.Type) error {
+ if typ.Kind() == reflect.Ptr {
+ typ = typ.Elem()
+ }
+ if typ.Kind() != reflect.Struct {
+ return nil
+ }
+ inner, err := json.Marshal(val)
+ if err != nil {
+ return fmt.Errorf("failed to marshal inner struct '%v%v':", prefix, err)
+ }
+ return checkUnknownFieldsRec(inner, prefix, typ)
+}
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 73ad3502c..e36dd9ee9 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -4,17 +4,33 @@
package config
import (
+ "encoding/json"
"fmt"
"reflect"
"testing"
)
-func TestUnknown(t *testing.T) {
+func TestLoad(t *testing.T) {
+ type NestedNested struct {
+ Ccc int
+ Ddd string
+ }
+ type Nested struct {
+ Aaa int
+ Bbb string
+ More NestedNested
+ }
type Config struct {
Foo int
Bar string
Baz string `json:"-"`
+ Raw json.RawMessage
+ Qux []string
+ Box Nested
+ Boq *Nested
+ Arr []Nested
}
+
tests := []struct {
input string
output Config
@@ -45,6 +61,102 @@ func TestUnknown(t *testing.T) {
Config{},
"unknown field 'baz' in config",
},
+ {
+ `{"foo": 1, "box": {"aaa": 12, "bbb": "bbb"}}`,
+ Config{
+ Foo: 1,
+ Box: Nested{
+ Aaa: 12,
+ Bbb: "bbb",
+ },
+ },
+ "",
+ },
+ {
+ `{"qux": ["aaa", "bbb"]}`,
+ Config{
+ Qux: []string{"aaa", "bbb"},
+ },
+ "",
+ },
+ {
+ `{"box": {"aaa": 12, "ccc": "bbb"}}`,
+ Config{},
+ "unknown field 'box.ccc' in config",
+ },
+ {
+ `{"foo": 1, "boq": {"aaa": 12, "bbb": "bbb"}}`,
+ Config{
+ Foo: 1,
+ Boq: &Nested{
+ Aaa: 12,
+ Bbb: "bbb",
+ },
+ },
+ "",
+ },
+ {
+ `{"boq": {"aaa": 12, "ccc": "bbb"}}`,
+ Config{},
+ "unknown field 'boq.ccc' in config",
+ },
+
+ {
+ `{"foo": 1, "arr": []}`,
+ Config{
+ Foo: 1,
+ Arr: []Nested{},
+ },
+ "",
+ },
+ {
+ `{"foo": 1, "arr": [{"aaa": 12, "bbb": "bbb"}, {"aaa": 13, "bbb": "ccc"}]}`,
+ Config{
+ Foo: 1,
+ Arr: []Nested{
+ Nested{
+ Aaa: 12,
+ Bbb: "bbb",
+ },
+ Nested{
+ Aaa: 13,
+ Bbb: "ccc",
+ },
+ },
+ },
+ "",
+ },
+ {
+ `{"arr": [{"aaa": 12, "ccc": "bbb"}]}`,
+ Config{},
+ "unknown field 'arr[0].ccc' in config",
+ },
+ {
+ `{"foo": 1, "boq": {"aaa": 12, "more": {"ccc": 13, "ddd": "ddd"}}}`,
+ Config{
+ Foo: 1,
+ Boq: &Nested{
+ Aaa: 12,
+ More: NestedNested{
+ Ccc: 13,
+ Ddd: "ddd",
+ },
+ },
+ },
+ "",
+ },
+ {
+ `{"foo": 1, "boq": {"aaa": 12, "more": {"ccc": 13, "eee": "eee"}}}`,
+ Config{},
+ "unknown field 'boq.more.eee' in config",
+ },
+ {
+ `{"raw": {"zux": 11}}`,
+ Config{
+ Raw: []byte(`{"zux": 11}`),
+ },
+ "",
+ },
}
for i, test := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
@@ -58,8 +170,23 @@ func TestUnknown(t *testing.T) {
t.Fatalf("bad err: want '%v', got '%v'", test.err, errStr)
}
if !reflect.DeepEqual(test.output, cfg) {
- t.Fatalf("bad output: want '%#v', got '%#v'", test.output, cfg)
+ t.Fatalf("bad output: want:\n%#v\n, got:\n%#v", test.output, cfg)
}
})
}
}
+
+func TestLoadBadType(t *testing.T) {
+ want := "config type is not pointer to struct"
+ if err := load([]byte("{}"), 1); err == nil || err.Error() != want {
+ t.Fatalf("got '%v', want '%v'", err, want)
+ }
+ i := 0
+ if err := load([]byte("{}"), &i); err == nil || err.Error() != want {
+ t.Fatalf("got '%v', want '%v'", err, want)
+ }
+ s := struct{}{}
+ if err := load([]byte("{}"), s); err == nil || err.Error() != want {
+ t.Fatalf("got '%v', want '%v'", err, want)
+ }
+}