From 5dd031c4435ecadbc4360b188805d58d8b69b0d6 Mon Sep 17 00:00:00 2001 From: Alexander Demin Date: Mon, 13 Jan 2020 12:49:58 +0300 Subject: [PATCH] Provide ability to have nested config structs (#37) Signed-off-by: Alex Demin --- config/config.go | 94 ++++++++----------------------------- config/config_test.go | 43 +++++++++++++++-- config/parser.go | 105 ++++++++++++++++++++++++++++++++++++++++++ harvester_test.go | 16 +++++-- 4 files changed, 173 insertions(+), 85 deletions(-) create mode 100644 config/parser.go diff --git a/config/config.go b/config/config.go index 4a20e187..9fac702e 100644 --- a/config/config.go +++ b/config/config.go @@ -23,6 +23,8 @@ const ( SourceFlag Source = "flag" ) +var sourceTags = [...]Source{SourceSeed, SourceEnv, SourceConsul, SourceFlag} + // Field definition of a config value that can change. type Field struct { name string @@ -33,36 +35,25 @@ type Field struct { sources map[Source]string } -// NewField constructor. -func NewField(fld *reflect.StructField, val *reflect.Value) (*Field, error) { - if !isTypeSupported(fld.Type) { - return nil, fmt.Errorf("field %s is not supported (only types from the sync package of harvester)", fld.Name) - } +// newField constructor. +func newField(prefix string, fld reflect.StructField, val reflect.Value) *Field { f := &Field{ - name: fld.Name, + name: prefix + fld.Name, tp: fld.Type.Name(), version: 0, - setter: val.FieldByName(fld.Name).Addr().MethodByName("Set"), - printer: val.FieldByName(fld.Name).Addr().MethodByName("String"), + setter: val.Addr().MethodByName("Set"), + printer: val.Addr().MethodByName("String"), sources: make(map[Source]string), } - value, ok := fld.Tag.Lookup(string(SourceSeed)) - if ok { - f.sources[SourceSeed] = value - } - value, ok = fld.Tag.Lookup(string(SourceEnv)) - if ok { - f.sources[SourceEnv] = value - } - value, ok = fld.Tag.Lookup(string(SourceConsul)) - if ok { - f.sources[SourceConsul] = value - } - value, ok = fld.Tag.Lookup(string(SourceFlag)) - if ok { - f.sources[SourceFlag] = value + + for _, tag := range sourceTags { + value, ok := fld.Tag.Lookup(string(tag)) + if ok { + f.sources[tag] = value + } } - return f, nil + + return f } // Name getter. @@ -137,60 +128,11 @@ func New(cfg interface{}) (*Config, error) { if cfg == nil { return nil, errors.New("configuration is nil") } - tp := reflect.TypeOf(cfg) - if tp.Kind() != reflect.Ptr { - return nil, errors.New("configuration should be a pointer type") - } - val := reflect.ValueOf(cfg).Elem() - ff, err := getFields(tp.Elem(), &val) + + ff, err := newParser().ParseCfg(cfg) if err != nil { return nil, err } - return &Config{Fields: ff}, nil -} - -func getFields(tp reflect.Type, val *reflect.Value) ([]*Field, error) { - dup := make(map[Source]string) - var ff []*Field - for i := 0; i < tp.NumField(); i++ { - f := tp.Field(i) - fld, err := NewField(&f, val) - if err != nil { - return nil, err - } - value, ok := fld.Sources()[SourceConsul] - if ok { - if isKeyValueDuplicate(dup, SourceConsul, value) { - return nil, fmt.Errorf("duplicate value %v for source %s", fld, SourceConsul) - } - } - ff = append(ff, fld) - } - return ff, nil -} - -func isTypeSupported(t reflect.Type) bool { - if t.Kind() != reflect.Struct { - return false - } - if t.PkgPath() != "github.com/beatlabs/harvester/sync" { - return false - } - switch t.Name() { - case "Bool", "Int64", "Float64", "String", "Secret": - return true - default: - return false - } -} -func isKeyValueDuplicate(dup map[Source]string, src Source, value string) bool { - v, ok := dup[src] - if ok { - if value == v { - return true - } - } - dup[src] = value - return false + return &Config{Fields: ff}, nil } diff --git a/config/config_test.go b/config/config_test.go index 76b77dbb..c33c32ed 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -58,6 +58,8 @@ func TestNew(t *testing.T) { {name: "cfg is not pointer", args: args{cfg: testConfig{}}, wantErr: true}, {name: "cfg field not supported", args: args{cfg: &testInvalidTypeConfig{}}, wantErr: true}, {name: "cfg duplicate consul key", args: args{cfg: &testDuplicateConfig{}}, wantErr: true}, + {name: "cfg tagged struct not supported", args: args{cfg: &testInvalidNestedStructWithTags{}}, wantErr: true}, + {name: "cfg nested duplicate consul key", args: args{cfg: &testDuplicateNestedConsulConfig{}}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -68,7 +70,7 @@ func TestNew(t *testing.T) { } else { assert.NoError(t, err) assert.NotNil(t, got) - assert.Len(t, got.Fields, 4) + assert.Len(t, got.Fields, 6) assertField(t, got.Fields[0], "Name", "String", map[Source]string{SourceSeed: "John Doe", SourceEnv: "ENV_NAME"}) assertField(t, got.Fields[1], "Age", "Int64", @@ -77,6 +79,10 @@ func TestNew(t *testing.T) { map[Source]string{SourceSeed: "99.9", SourceEnv: "ENV_BALANCE", SourceConsul: "/config/balance"}) assertField(t, got.Fields[3], "HasJob", "Bool", map[Source]string{SourceSeed: "true", SourceEnv: "ENV_HAS_JOB", SourceConsul: "/config/has-job"}) + assertField(t, got.Fields[4], "PositionSalary", "Int64", + map[Source]string{SourceSeed: "2000", SourceEnv: "ENV_SALARY"}) + assertField(t, got.Fields[5], "LevelOneLevelTwoDeepField", "String", + map[Source]string{SourceSeed: "foobar"}) } }) } @@ -101,23 +107,50 @@ func TestConfig_Set(t *testing.T) { assert.NoError(t, err) err = cfg.Fields[3].Set("true", 1) assert.NoError(t, err) + err = cfg.Fields[4].Set("6000", 1) + assert.NoError(t, err) + err = cfg.Fields[5].Set("baz", 1) + assert.NoError(t, err) assert.Equal(t, "John Doe", c.Name.Get()) assert.Equal(t, int64(18), c.Age.Get()) assert.Equal(t, 99.9, c.Balance.Get()) assert.Equal(t, true, c.HasJob.Get()) + assert.Equal(t, int64(6000), c.Position.Salary.Get()) + assert.Equal(t, "baz", c.LevelOne.LevelTwo.DeepField.Get()) +} + +type testNestedConfig struct { + Salary sync.Int64 `seed:"2000" env:"ENV_SALARY"` } type testConfig struct { - Name sync.String `seed:"John Doe" env:"ENV_NAME"` - Age sync.Int64 `env:"ENV_AGE" consul:"/config/age"` - Balance sync.Float64 `seed:"99.9" env:"ENV_BALANCE" consul:"/config/balance"` - HasJob sync.Bool `seed:"true" env:"ENV_HAS_JOB" consul:"/config/has-job"` + Name sync.String `seed:"John Doe" env:"ENV_NAME"` + Age sync.Int64 `env:"ENV_AGE" consul:"/config/age"` + Balance sync.Float64 `seed:"99.9" env:"ENV_BALANCE" consul:"/config/balance"` + HasJob sync.Bool `seed:"true" env:"ENV_HAS_JOB" consul:"/config/has-job"` + Position testNestedConfig + LevelOne struct { + LevelTwo struct { + DeepField sync.String `seed:"foobar"` + } + } +} + +type testDuplicateNestedConsulConfig struct { + Age1 sync.Int64 `env:"ENV_AGE" consul:"/config/age"` + Nested struct { + Age2 sync.Int64 `env:"ENV_AGE" consul:"/config/age"` + } } type testInvalidTypeConfig struct { Balance float32 `seed:"99.9" env:"ENV_BALANCE" consul:"/config/balance"` } +type testInvalidNestedStructWithTags struct { + Nested testNestedConfig `seed:"foo"` +} + type testDuplicateConfig struct { Name sync.String `seed:"John Doe" env:"ENV_NAME"` Age1 sync.Int64 `env:"ENV_AGE" consul:"/config/age"` diff --git a/config/parser.go b/config/parser.go new file mode 100644 index 00000000..2275d90b --- /dev/null +++ b/config/parser.go @@ -0,0 +1,105 @@ +package config + +import ( + "errors" + "fmt" + "reflect" +) + +type structFieldType uint + +const ( + typeInvalid structFieldType = iota + typeField + typeStruct +) + +type parser struct { + dups map[Source]string +} + +func newParser() *parser { + return &parser{} +} + +func (p *parser) ParseCfg(cfg interface{}) ([]*Field, error) { + p.dups = make(map[Source]string) + + tp := reflect.TypeOf(cfg) + if tp.Kind() != reflect.Ptr { + return nil, errors.New("configuration should be a pointer type") + } + + return p.getFields("", tp.Elem(), reflect.ValueOf(cfg).Elem()) +} + +func (p *parser) getFields(prefix string, tp reflect.Type, val reflect.Value) ([]*Field, error) { + var ff []*Field + + for i := 0; i < tp.NumField(); i++ { + f := tp.Field(i) + + typ, err := p.getStructFieldType(f) + if err != nil { + return nil, err + } + + switch typ { + case typeField: + fld, err := p.createField(prefix, f, val.Field(i)) + if err != nil { + return nil, err + } + ff = append(ff, fld) + case typeStruct: + nested, err := p.getFields(prefix+f.Name, f.Type, val.Field(i)) + if err != nil { + return nil, err + } + ff = append(ff, nested...) + } + } + return ff, nil +} + +func (p *parser) createField(prefix string, f reflect.StructField, val reflect.Value) (*Field, error) { + fld := newField(prefix, f, val) + + value, ok := fld.Sources()[SourceConsul] + if ok { + if p.isKeyValueDuplicate(SourceConsul, value) { + return nil, fmt.Errorf("duplicate value %v for source %s", fld, SourceConsul) + } + } + + return fld, nil +} + +func (p *parser) isKeyValueDuplicate(src Source, value string) bool { + v, ok := p.dups[src] + if ok { + if value == v { + return true + } + } + p.dups[src] = value + return false +} + +func (p *parser) getStructFieldType(f reflect.StructField) (structFieldType, error) { + t := f.Type + if t.Kind() != reflect.Struct { + return typeInvalid, fmt.Errorf("only struct type supported for %s", f.Name) + } + + for _, tag := range sourceTags { + if _, ok := f.Tag.Lookup(string(tag)); ok { + if t.PkgPath() != "github.com/beatlabs/harvester/sync" { + return typeInvalid, fmt.Errorf("field %s is not supported (only types from the sync package of harvester)", f.Name) + } + return typeField, nil + } + } + + return typeStruct, nil +} diff --git a/harvester_test.go b/harvester_test.go index f36e21c9..b7f1f34f 100644 --- a/harvester_test.go +++ b/harvester_test.go @@ -60,6 +60,8 @@ func TestCreate_NoConsul(t *testing.T) { assert.Equal(t, int64(18), cfg.Age.Get()) assert.Equal(t, 99.9, cfg.Balance.Get()) assert.Equal(t, true, cfg.HasJob.Get()) + assert.Equal(t, int64(8000), cfg.Position.Salary.Get()) + assert.Equal(t, int64(24), cfg.Position.Place.RoomNumber.Get()) } func TestCreate_SeedError(t *testing.T) { @@ -81,10 +83,16 @@ type testConfig struct { } type testConfigNoConsul struct { - Name sync.String `seed:"John Doe"` - Age sync.Int64 `seed:"18"` - Balance sync.Float64 `seed:"99.9"` - HasJob sync.Bool `seed:"true"` + Name sync.String `seed:"John Doe"` + Age sync.Int64 `seed:"18"` + Balance sync.Float64 `seed:"99.9"` + HasJob sync.Bool `seed:"true"` + Position struct { + Salary sync.Int64 `seed:"8000"` + Place struct { + RoomNumber sync.Int64 `seed:"24"` + } + } } type testConfigSeedError struct {