diff --git a/README.md b/README.md index de6f8249..731b6034 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,13 @@ Pointers, slices and slices of pointers, and maps of those types are also suppor You may also add custom parsers for your types. +Additionally, the following are also supported +- Slices of Structs +- Map of Structs + +> [!IMPORTANT] +> For nested maps (i.e. map of structs inside map of structs), be careful with key naming to avoid conflicts. + ### Tags The following tags are provided: @@ -95,8 +102,8 @@ The following tags are provided: - `env`: sets the environment variable name and optionally takes the tag options described below - `envDefault`: sets the default value for the field - `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it -- `envSeparator`: sets the character to be used to separate items in slices and maps (default: `,`) -- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (default: `:`) +- `envSeparator`: sets the character to be used to separate items in slices and maps (which do not have structs as the value type) (default: `,`) +- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (which do not have structs as the value type) (default: `:`) ### `env` tag options diff --git a/env.go b/env.go index c928b66d..bc6322d0 100644 --- a/env.go +++ b/env.go @@ -21,6 +21,7 @@ import ( "net/url" "os" "reflect" + "slices" "strconv" "strings" "time" @@ -240,7 +241,7 @@ func customOptions(opts Options) Options { return defOpts } -func optionsWithSliceEnvPrefix(opts Options, index int) Options { +func optionsWithPrefix(opts Options, prefix string) Options { return Options{ Environment: opts.Environment, TagName: opts.TagName, @@ -248,7 +249,7 @@ func optionsWithSliceEnvPrefix(opts Options, index int) Options { DefaultValueTagName: opts.DefaultValueTagName, RequiredIfNoDef: opts.RequiredIfNoDef, OnSet: opts.OnSet, - Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index), + Prefix: prefix, UseFieldNameByDefault: opts.UseFieldNameByDefault, SetDefaultsForZeroValuesOnly: opts.SetDefaultsForZeroValuesOnly, FuncMap: opts.FuncMap, @@ -256,20 +257,16 @@ func optionsWithSliceEnvPrefix(opts Options, index int) Options { } } +func optionsWithSliceEnvPrefix(opts Options, index int) Options { + return optionsWithPrefix(opts, fmt.Sprintf("%s%d_", opts.Prefix, index)) +} + +func optionsWithMapEnvPrefix(opts Options, mapKey string) Options { + return optionsWithPrefix(opts, fmt.Sprintf("%s%s_", opts.Prefix, mapKey)) +} + func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { - return Options{ - Environment: opts.Environment, - TagName: opts.TagName, - PrefixTagName: opts.PrefixTagName, - DefaultValueTagName: opts.DefaultValueTagName, - RequiredIfNoDef: opts.RequiredIfNoDef, - OnSet: opts.OnSet, - Prefix: opts.Prefix + field.Tag.Get(opts.PrefixTagName), - UseFieldNameByDefault: opts.UseFieldNameByDefault, - SetDefaultsForZeroValuesOnly: opts.SetDefaultsForZeroValuesOnly, - FuncMap: opts.FuncMap, - rawEnvVars: opts.rawEnvVars, - } + return optionsWithPrefix(opts, opts.Prefix+field.Tag.Get(opts.PrefixTagName)) } // Parse parses a struct containing `env` tags and loads its values from @@ -415,6 +412,13 @@ func doParseField( return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) } + isValidMapOfStructs, err := isMapOfStructs(refTypeField, opts) + if err != nil { + return err + } else if isValidMapOfStructs { + return doParseMap(refField, processField, optionsWithEnvPrefix(refTypeField, opts), refTypeField) + } + return nil } @@ -854,3 +858,235 @@ func ToMap(env []string) map[string]string { func isInvalidPtr(v reflect.Value) bool { return reflect.Ptr == v.Kind() && v.Elem().Kind() == reflect.Invalid } + +func isMapOfStructs(refTypeField reflect.StructField, opts Options) (bool, error) { + field := refTypeField.Type + + if field.Kind() == reflect.Ptr { + field = field.Elem() + } + + if field.Kind() == reflect.Map { + kind := field.Elem().Kind() + if kind == reflect.Ptr { + ptrField := field.Elem() + kind = ptrField.Elem().Kind() + } + + if kind == reflect.Struct { + val, _ := parseKeyForOption(refTypeField.Tag.Get(opts.TagName)) + if val != "" { + return false, newParseError(refTypeField, fmt.Errorf(`env key unsupported for struct map %q`, refTypeField.Name)) + } + // Only process if the env prefix tag is present + // This avoids the lib trying to set keys for a map without any prefix given + if refTypeField.Tag.Get(opts.PrefixTagName) != "" { + return true, nil + } + } + } + + return false, nil +} + +func doParseMap(ref reflect.Value, processField processFieldFn, opts Options, sf reflect.StructField) error { + if opts.Prefix != "" && !strings.HasSuffix(opts.Prefix, string(underscore)) { + opts.Prefix += string(underscore) + } + + var environments []string + for environment := range opts.Environment { + if strings.HasPrefix(environment, opts.Prefix) { + environments = append(environments, environment) + } + } + + // There are no map keys that match + if len(environments) == 0 { + return nil + } + + // Create a new map if it's nil + if ref.IsNil() { + ref.Set(reflect.MakeMap(ref.Type())) + } + + // Get the key and value types + keyType := ref.Type().Key() + valueType := ref.Type().Elem() + + keyGroups := make(map[string]bool) + + structInnerSubEnvVars := getPossibleEnvVars(valueType, opts) + + for _, env := range environments { + currKey := "" + key := strings.TrimPrefix(env, opts.Prefix) + + // A user can have multiple environment variables which match to multiple keys + // for example BAR_KEY_STR and BAR_KEY_NEW_STR are valid envars + // If the struct has both "STR" and "NEW_STR" this would mean that + // "STR" matches to both as a suffix and would result in two map keys + // KEY_NEW and KEY, thus we match the suffix that would give the smallest key + // since the smallest suffix that gives the largest key may have its own + // different environment variable + for _, innerEnvVar := range structInnerSubEnvVars { + // If we are using a map of a map (we don't use the absolute path value, instead we use the prefix value) + suffix := string(underscore) + innerEnvVar.value + if innerEnvVar.useContains { + idx := strings.LastIndex(key, suffix) + if idx != -1 { + newKey := key[:idx] + // We had a better match which trimmed the key further + if newKey != "" && (currKey == "" || len(currKey) > len(newKey)) { + currKey = newKey + } + } + } else if strings.HasSuffix(key, innerEnvVar.value) { + if key == innerEnvVar.value { + // If the key is exactly the innerEnvVar, this means that the env var was malformed + return newParseError(sf, fmt.Errorf(`malformed complex map struct for %q`, key)) + } + newKey := strings.TrimSuffix(key, suffix) + // We had a better match which trimmed the key further + if newKey != "" && (currKey == "" || len(currKey) > len(newKey)) { + currKey = newKey + } + } + } + + // If a key match has been found + if currKey != "" { + keyGroups[currKey] = true + } + } + + // Process each key group + for mapKey := range keyGroups { + value := reflect.New(valueType).Elem() + keyOpts := optionsWithMapEnvPrefix(opts, mapKey) + + initialKind := value.Kind() + if initialKind == reflect.Ptr { + if value.IsNil() { + value.Set(reflect.New(valueType.Elem())) + } + value = value.Elem() + } + + err := doParse(value, processField, keyOpts) + if err != nil { + return err + } + + parserFunc, ok := opts.FuncMap[keyType] + if !ok { + kind := keyType.Kind() + if parserFunc, ok = defaultBuiltInParsers[kind]; !ok { + return newNoParserError(sf) + } + } + + parsedKey, err := parserFunc(mapKey) + if err != nil { + return newParseError(sf, fmt.Errorf("failed to parse map key %q: %w", mapKey, err)) + } + + keyValue := reflect.ValueOf(parsedKey).Convert(keyType) + + if initialKind == reflect.Ptr { + valuePtr := reflect.New(valueType.Elem()) + valuePtr.Elem().Set(value) + value = valuePtr + } + + ref.SetMapIndex(keyValue, value) + } + + return nil +} + +type SuffixType struct { + useContains bool + value string +} + +// getPossibleEnvVars returns all possible environment variables that could be set for a given struct type. +func getPossibleEnvVars(v reflect.Type, opts Options) []SuffixType { + envVars := make(map[string]bool) + + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // The lib does not allow recursive structs technically + // Recursive structs need to have the parent reference type as Pointer, + // which means since pointer struct types do not get initialized by the parser by default, + // and only with the `env:,init` tag. However, when the `init` attribute is set + // the lib goes into an infinite loop because it does not support recursive structs + // Thus we do not handle recursive structs here + traverseStruct(v, "", opts, envVars) + + // Convert map keys to slice and sort for deterministic order + result := make([]SuffixType, 0, len(envVars)) + for k, val := range envVars { + entry := SuffixType{ + value: k, + useContains: val, + } + result = append(result, entry) + } + + slices.SortFunc(result, func(i, j SuffixType) int { + if i.useContains != j.useContains { + if i.useContains { + return 1 + } + return -1 + } + return strings.Compare(i.value, j.value) + }) + + return result +} + +// traverseStruct recursively traverses a struct type and collects all possible environment variables. +func traverseStruct(t reflect.Type, prefix string, opts Options, envVars map[string]bool) { + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + + // Get field prefix if exists + fieldPrefix := field.Tag.Get(opts.PrefixTagName) + if fieldPrefix != "" { + prefix = prefix + fieldPrefix + } + + ownKey, _ := parseKeyForOption(field.Tag.Get(opts.TagName)) + + // Get env tag if exists + key := prefix + ownKey + if ownKey != "" { + envVars[key] = false + } + + // Handle nested structs and maps of structs + fieldType := field.Type + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + + if fieldType.Kind() == reflect.Struct { + traverseStruct(fieldType, prefix, opts, envVars) + } + + if fieldType.Kind() == reflect.Map { + elemType := fieldType.Elem() + if elemType.Kind() == reflect.Ptr { + elemType = elemType.Elem() + } + if elemType.Kind() == reflect.Struct { + envVars[key] = true + } + } + } +} diff --git a/env_test.go b/env_test.go index 9a348f83..7db027be 100644 --- a/env_test.go +++ b/env_test.go @@ -2163,6 +2163,36 @@ func TestIssue298ErrorNestedFieldRequiredNotSet(t *testing.T) { isTrue(t, errors.Is(err, VarIsNotSetError{})) } +func TestIssue298VerifyPrefixUsingEnvTagIsNotSupported(t *testing.T) { + type Test struct { + Str string `env:"STR"` + Num int `env:"NUM"` + } + type ComplexConfig struct { + Foo *[]Test `env:"FOO_"` + Bar []Test `env:"BAR"` + Baz []Test `env:",init"` + } + + t.Setenv("FOO_0_STR", "f0t") + t.Setenv("FOO_0_NUM", "101") + t.Setenv("FOO_1_STR", "f1t") + t.Setenv("FOO_1_NUM", "111") + + t.Setenv("BAR_0_STR", "b0t") + t.Setenv("BAR_0_NUM", "127") + t.Setenv("BAR_1_STR", "b1t") + t.Setenv("BAR_1_NUM", "212") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, nil, cfg.Foo) + isEqual(t, nil, cfg.Baz) + isEqual(t, nil, cfg.Bar) +} + func TestIssue320(t *testing.T) { type Test struct { Str string `env:"STR"` @@ -2411,3 +2441,394 @@ func TestEnvBleed(t *testing.T) { isEqual(t, "", cfg.Foo) }) } + +func TestComplexConfigWithMap(t *testing.T) { + t.Run("Should not parse struct map with empty key", func(t *testing.T) { + type Test struct { + Str string `env:"DAT_STR"` + } + + type ComplexConfig struct { + Bar map[string]Test `envPrefix:"BAR_"` + } + + t.Setenv("BAR_DAT_STR", "b1t") + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isErrorWithMessage(t, err, "env: parse error on field \"Bar\" of type \"map[string]env.Test\": malformed complex map struct for \"DAT_STR\"") + }) + + t.Run("Should parse map with struct without any matching env", func(t *testing.T) { + type Test struct { + Str string + } + + type ComplexConfig struct { + Bar map[string]Test `envPrefix:"BAR_"` + } + + t.Setenv("BAR_E_TEST_DAT_STR", "b1t") + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isNoErr(t, err) + }) + + t.Run("Should parse non pointer struct map with", func(t *testing.T) { + type SubTest struct { + Str string `env:"NEW_STR"` + } + + type Test struct { + Str string `env:"DAT_STR"` + Num int `env:"DAT_NUM"` + Sub SubTest `envPrefix:"SUB_"` + } + + type ComplexConfig struct { + Bar map[string]Test `envPrefix:"BAR_"` + } + + t.Run("valid values", func(t *testing.T) { + t.Setenv("BAR_KEY1_TEST_DAT_STR", "b1t") + t.Setenv("BAR_KEY1_DAT_NUM", "201") + t.Setenv("BAR_KEY1_SUB_NEW_STR", "sub_b1t") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, "b1t", cfg.Bar["KEY1_TEST"].Str) + isEqual(t, 201, cfg.Bar["KEY1"].Num) + isEqual(t, "sub_b1t", cfg.Bar["KEY1"].Sub.Str) + }) + + t.Run("no values", func(t *testing.T) { + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, 0, len(cfg.Bar)) + isEqual(t, "", cfg.Bar["KEY1_TEST"].Str) + isEqual(t, 0, cfg.Bar["KEY1"].Num) + isEqual(t, "", cfg.Bar["KEY1"].Sub.Str) + }) + }) + + t.Run("Should skip processing map when no tags are present on the map", func(t *testing.T) { + type SubTest struct { + Str string `env:"NEW_STR"` + } + + type Test struct { + Str string `env:"DAT_STR"` + Num int `env:"DAT_NUM"` + Sub SubTest `envPrefix:"SUB_"` + } + + type ComplexConfig struct { + Bar map[string]Test + } + + t.Setenv("KEY1_TEST_DAT_STR", "b1t") + t.Setenv("KEY1_DAT_NUM", "201") + t.Setenv("KEY1_SUB_NEW_STR", "sub_b1t") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, nil, cfg.Bar) + }) + + t.Run("Should not allow an env tag with a value to be set on the map", func(t *testing.T) { + type Test struct { + Str string `env:"DAT_STR"` + } + + type ComplexConfig struct { + Bar map[string]Test `env:"VALUE,init"` + } + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isEqual(t, nil, cfg.Bar) + isErrorWithMessage(t, err, "env: parse error on field \"Bar\" of type \"map[string]env.Test\": env key unsupported for struct map \"Bar\"") + }) + + t.Run("Should allow an env tag without a value to be set on the map", func(t *testing.T) { + type Test struct { + Str string `env:"DAT_STR"` + } + + type ComplexConfig struct { + Bar map[string]Test `env:",init"` + } + + t.Setenv("KEY1_DAT_STR", "b1t") + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isNoErr(t, err) + isEqual(t, nil, cfg.Bar) + }) + + t.Run("Should parse pointer struct map with", func(t *testing.T) { + type SubTest struct { + Str string `env:"NEW_STR"` + } + + type Test struct { + Str string `env:"DAT_STR"` + Num int `env:"DAT_NUM"` + Sub SubTest `envPrefix:"SUB_"` + } + + type ComplexConfig struct { + Bar map[string]*Test `envPrefix:"BAR_"` + } + + t.Run("valid values", func(t *testing.T) { + t.Setenv("BAR_KEY1_TEST_DAT_STR", "b1t") + t.Setenv("BAR_KEY1_DAT_NUM", "201") + t.Setenv("BAR_KEY1_SUB_NEW_STR", "sub_b1t") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, "b1t", cfg.Bar["KEY1_TEST"].Str) + isEqual(t, 201, cfg.Bar["KEY1"].Num) + isEqual(t, "sub_b1t", cfg.Bar["KEY1"].Sub.Str) + }) + + t.Run("no values", func(t *testing.T) { + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, nil, cfg.Bar) + }) + }) + + t.Run("Parse struct map nested in struct maps without any duplicate field names", func(t *testing.T) { + type SubTest struct { + Str string `env:"NEW_STR"` + } + + type Test struct { + Foo map[string]SubTest `envPrefix:"FOO_"` + } + + type ComplexConfig struct { + Bar map[string]Test `envPrefix:"BAR_"` + } + + t.Run("Should succeed with valid values", func(t *testing.T) { + t.Setenv("BAR_KEY1_FOO_KEY2_NEW_STR", "b1t") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + isEqual(t, "b1t", cfg.Bar["KEY1"].Foo["KEY2"].Str) + }) + + t.Run("Should succeed with envPrefix as key but nested map having unique key", func(t *testing.T) { + map1Key := "BAR_FOO_FOO_FOO_FOO_FOO_FOO" + map2Key := "KEY1" + t.Setenv(`BAR_`+map1Key+`_FOO_`+map2Key+`_NEW_STR`, "b1t") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + isEqual(t, "b1t", cfg.Bar[map1Key].Foo[map2Key].Str) + }) + + t.Run("Should fail where nested map prefix is key for for parent map and nested map keys", func(t *testing.T) { + map1Key := "FOO_FOO_FOO_FOO_FOO_FOO" + map2Key := "FOO" + t.Setenv(`BAR_`+map1Key+`_FOO_`+map2Key+`_NEW_STR`, "b1t") + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isErrorWithMessage(t, err, "env: parse error on field \"Foo\" of type \"map[string]env.SubTest\": malformed complex map struct for \"NEW_STR\"") + }) + }) + + t.Run("Verify matching of similar names", func(t *testing.T) { + type SubTest struct { + Str1 string `env:"STR"` + } + + type Test struct { + Str1 string `env:"NEW_STR"` + Str2 string `env:"STR"` + Str3 string `env:"NEW_STRING"` + Str4 string `env:"STR_NEWER"` + Str5 string `env:"NEW_STR_STR"` + + SubStr map[string]SubTest `envPrefix:"SUBBER_"` + } + + type ComplexConfig struct { + Bar map[string]Test `envPrefix:"BAR_"` + } + + t.Run("partial environment variables", func(t *testing.T) { + cfg := ComplexConfig{} + + t.Setenv("BAR_FOO_NEW_STR", "a1") + t.Setenv("BAR_FOO_NEW_STRING", "a3") + + isNoErr(t, Parse(&cfg)) + isEqual(t, "a1", cfg.Bar["FOO"].Str1) + isEqual(t, "", cfg.Bar["FOO"].Str2) + isEqual(t, "a3", cfg.Bar["FOO"].Str3) + isEqual(t, "", cfg.Bar["FOO"].Str4) + isEqual(t, "", cfg.Bar["FOO"].Str5) + isEqual(t, 0, len(cfg.Bar["FOO"].SubStr)) + }) + + t.Run("full environment variables", func(t *testing.T) { + cfg := ComplexConfig{} + + t.Setenv("BAR_FOO_NEW_STR", "a1") + t.Setenv("BAR_FOO_STR", "a2") + t.Setenv("BAR_FOO_NEW_STRING", "a3") + t.Setenv("BAR_FOO_STR_NEWER", "a4") + t.Setenv("BAR_FOO_NEW_STR_STR", "a5") + + isNoErr(t, Parse(&cfg)) + isEqual(t, "a1", cfg.Bar["FOO"].Str1) + isEqual(t, "a2", cfg.Bar["FOO"].Str2) + isEqual(t, "a3", cfg.Bar["FOO"].Str3) + isEqual(t, "a4", cfg.Bar["FOO"].Str4) + isEqual(t, "a5", cfg.Bar["FOO"].Str5) + isEqual(t, 0, len(cfg.Bar["FOO"].SubStr)) + }) + + t.Run("the map", func(t *testing.T) { + cfg := ComplexConfig{} + + t.Setenv("BAR_FOO_SUBBER_NEWER_STR", "a1") + + isNoErr(t, Parse(&cfg)) + isEqual(t, "a1", cfg.Bar["FOO"].SubStr["NEWER"].Str1) + }) + }) + + t.Run("Parse struct map nested in struct maps with duplicate field names", func(t *testing.T) { + type SubTest struct { + Str string `env:"FOO_FOO"` + } + + type Test struct { + Foo map[string]SubTest `envPrefix:"FOO_"` + } + + type ComplexConfig struct { + Bar map[string]Test `envPrefix:"BAR_"` + } + + map1Key := "FOO_FOO_FOO_FOO_FOO_FOO" + map2Key := "FOO" + t.Setenv(`BAR_`+map1Key+`_FOO_`+map2Key+`_FOO_FOO`, "b1t") + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isNoErr(t, err) + + invalidAssumedKey := map1Key + "_FOO_FOO" + result, ok := cfg.Bar[invalidAssumedKey] + isTrue(t, ok) + // The map would be initialized but no values set + isEqual(t, 0, len(result.Foo)) + }) + + t.Run("Should parse struct map with float key", func(t *testing.T) { + type Test struct { + Str string `env:"STR"` + Num int `env:"NUM"` + } + type ComplexConfig struct { + Bar map[float64]Test `envPrefix:"BAR_"` + } + + t.Setenv("BAR_10.17_STR", "b1t") + t.Setenv("BAR_7.9_NUM", "201") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, "b1t", cfg.Bar[10.17].Str) + }) + + t.Run("Should parse struct map with required key", func(t *testing.T) { + type Test struct { + Str string `env:"STR,required"` + Num int `env:"NUM"` + } + type ComplexConfig struct { + Bar map[int64]Test `envPrefix:"BAR_"` + } + + t.Run("Where required is present", func(t *testing.T) { + t.Setenv("BAR_7_STR", "b1t") + t.Setenv("BAR_7_NUM", "201") + t.Setenv("BAR_9_STR", "b1t") + + cfg := ComplexConfig{} + + isNoErr(t, Parse(&cfg)) + isEqual(t, "b1t", cfg.Bar[7].Str) + isEqual(t, 201, cfg.Bar[7].Num) + isEqual(t, "b1t", cfg.Bar[9].Str) + }) + + t.Run("Where required is missing", func(t *testing.T) { + t.Setenv("BAR_7_STR", "b1t") + t.Setenv("BAR_7_NUM", "201") + t.Setenv("BAR_9_NUM", "7") + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isErrorWithMessage(t, err, "env: required environment variable \"BAR_9_STR\" is not set") + }) + }) + + t.Run("Should parse struct map which contains fields with init and envDefault", func(t *testing.T) { + type SubStruct struct { + Str string `env:"STR"` + } + + type Test struct { + Str string `env:"STR,required"` + Num int `envDefault:"17" env:"NUM"` + Sub *SubStruct `env:",init" envPrefix:"SUB_"` + } + + type ComplexConfig struct { + Bar map[int64]Test `envPrefix:"BAR_"` + } + + t.Setenv("BAR_7_STR", "b1t") + + cfg := ComplexConfig{} + + err := Parse(&cfg) + isNoErr(t, err) + + isFalse(t, cfg.Bar[7].Sub == nil) + isEqual(t, 17, cfg.Bar[7].Num) + }) + +} diff --git a/example_test.go b/example_test.go index 2562a836..94ed4300 100644 --- a/example_test.go +++ b/example_test.go @@ -480,3 +480,39 @@ func Example_setDefaultsForZeroValuesOnly() { // Without SetDefaultsForZeroValuesOnly, the username would have been 'admin'. // Output: {Username:root Password:qwerty} } + +func Example_useComplexStructMaps() { + type Service struct { + Username string `env:"USERNAME"` + } + + type Host struct { + Address string `env:"ADDRESS"` + Port int `env:"PORT"` + // For nested maps, be careful with key naming to avoid conflicts. + // The library uses the rightmost `envPrefix` match as the key. For example: + // This means that won't work - USERNAME becomes a key instead of a field + // export HOST_aws_server_SERVICE_SERVICE_USERNAME=Jakinson + // Thus you must make sure to name the keys appropriately correctly + Services map[string]Service `envPrefix:"SERVICE_"` + } + + type Config struct { + // In the case of struct of slices, you can use it without a prefix + // however if a prefix is not specified processing this map will be skipped + Hosts map[string]Host `envPrefix:"HOST_"` + } + + os.Setenv("HOST_aws_server_ADDRESS", "127.0.0.1") + os.Setenv("HOST_aws_server_PORT", "79") + os.Setenv("HOST_aws_server_SERVICE_Ec2_USERNAME", "Jakinson") + os.Setenv("HOST_aws_server_SERVICE_cloudfront_USERNAME", "Arnold") + + cfg := Config{} + + if err := Parse(&cfg); err != nil { + fmt.Println(err) + } + + fmt.Println(cfg) +}