Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of nested struct in map store by pointer #109

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions cleanenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,40 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
separator string
)

// process structs in map (except of supported ones)
if fld := s.Field(idx); fld.Kind() == reflect.Map && validStructs[fld.Type().Elem()] == nil {
mapElem := fld.Type().Elem()

if mapElem.Kind() == reflect.Struct {
// I don't see a way to support structures in maps at the moment.
// Perhaps this requires a major change in library logic.
return nil, fmt.Errorf("struct in map value is not supported, use pointer to struct instead")
}

isPointerToStruct := mapElem.Kind() == reflect.Ptr && mapElem.Elem().Kind() == reflect.Struct
if isPointerToStruct {
// Map values are not addressable, and we can't pass them to cfgNode.
// So we need to create a new map with new struct values.
newMap := reflect.MakeMap(fld.Type())
for _, key := range fld.MapKeys() {
value := fld.MapIndex(key)
// unwrap pointer
if value.Kind() == reflect.Ptr {
value = value.Elem()
}

newStruct := reflect.New(value.Type()).Elem()
newStruct.Set(value)
newMap.SetMapIndex(key, newStruct.Addr())

prefix, _ := fType.Tag.Lookup(TagEnvPrefix)
cfgStack = append(cfgStack, cfgNode{newStruct.Addr().Interface(), sPrefix + prefix})
}
// replace old map with new one
fld.Set(newMap)
}
}

// process nested structure (except of supported ones)
if fld := s.Field(idx); fld.Kind() == reflect.Struct {
//skip unexported
Expand Down
60 changes: 58 additions & 2 deletions cleanenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1191,8 +1191,8 @@ func TestTimeLocation(t *testing.T) {
func TestSkipUnexportedField(t *testing.T) {
conf := struct {
Database struct {
Host string `yaml:"host" env:"DB_HOST" env-description:"Database host"`
Port string `yaml:"port" env:"DB_PORT" env-description:"Database port"`
Host string `yaml:"host" env:"DB_HOST" env-description:"Database host"`
Port string `yaml:"port" env:"DB_PORT" env-description:"Database port"`
} `yaml:"database"`
server struct {
Host string `yaml:"host" env:"SRV_HOST,HOST" env-description:"Server host" env-default:"localhost"`
Expand All @@ -1210,3 +1210,59 @@ func TestSkipUnexportedField(t *testing.T) {
t.Fatal("expect value on exported fields")
}
}

func TestReadConfig_NestedStructInMap(t *testing.T) {
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yaml")
if err != nil {
t.Fatal("create temp file:", err)
}

text := []byte(`
children:
first:
number: 1`)
if _, err = tmpFile.Write(text); err != nil {
t.Fatal("write to temp file:", err)
}

t.Run("nested pointer to struct", func(t *testing.T) {
type (
Child struct {
Number int64 `yaml:"number"`
DefaultNumber int64 `yaml:"default_number" env-default:"10"`
}
Parent struct {
Children map[string]*Child `yaml:"children"`
}
)
expectedConfig := Parent{
Children: map[string]*Child{"first": {Number: 1, DefaultNumber: 10}},
}

var p Parent
if err = ReadConfig(tmpFile.Name(), &p); err != nil {
t.Fatal("read config:", err)
}
if err == nil && !reflect.DeepEqual(p, expectedConfig) {
t.Errorf("wrong data: %+v, want: %+v", p, expectedConfig)
}

})

t.Run("nested struct", func(t *testing.T) {
type (
Child struct {
Number int64 `yaml:"number"`
DefaultNumber int64 `yaml:"default_number" env-default:"10"`
}
Parent struct {
Children map[string]Child `yaml:"children"`
}
)

var p Parent
if err = ReadConfig(tmpFile.Name(), &p); err == nil {
t.Error("expected error, got nil")
}
})
}