diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2017-06-02 14:25:44 +0200 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2017-06-03 10:41:09 +0200 |
| commit | 96b8d4e99c7812f91633ea6cd1aee5867965e742 (patch) | |
| tree | aec8a76b057b1b245856f7bcb3ad00a9b9f47bc8 /pkg/config | |
| parent | 46c6ed89bf1a7de94496b853608ecd6f80776b58 (diff) | |
pkg/config: support nested structs
Diffstat (limited to 'pkg/config')
| -rw-r--r-- | pkg/config/config.go | 52 | ||||
| -rw-r--r-- | pkg/config/config_test.go | 131 |
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) + } +} |
