Skip to content

Commit

Permalink
Provide ability to have nested config structs (#37)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Demin <[email protected]>
  • Loading branch information
Oberonus authored and Sotirios Mantziaris committed Jan 13, 2020
1 parent 4fedcb6 commit 5dd031c
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 85 deletions.
94 changes: 18 additions & 76 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
43 changes: 38 additions & 5 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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",
Expand All @@ -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"})
}
})
}
Expand All @@ -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"`
Expand Down
105 changes: 105 additions & 0 deletions config/parser.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 12 additions & 4 deletions harvester_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down

0 comments on commit 5dd031c

Please sign in to comment.