diff --git a/README.md b/README.md index 9199fe9..a3f27b3 100644 --- a/README.md +++ b/README.md @@ -476,6 +476,61 @@ func main() { } ``` +### Complex objects inside array (slice) + +You can set sub-struct field values inside a slice by naming the environment variables with sequential numbers starting from 0 (without omitting numbers in between) and an underscore. +It is possible to use prefix tag too. + +Here's an example with and without prefix tag: + +```go +package main + +import ( + "fmt" + "log" + + "github.com/caarlos0/env/v11" +) + +type Test struct { + Str string `env:"STR"` + Num int `env:"NUM"` +} +type ComplexConfig struct { + Baz []Test `env:",init"` + Bar []Test `envPrefix:"BAR"` + Foo *[]Test `envPrefix:"FOO_"` +} + +func main() { + cfg := &ComplexConfig{} + opts := env.Options{ + Environment: map[string]string{ + "0_STR": "bt", + "1_NUM": "10", + + "FOO_0_STR": "b0t", + "FOO_1_STR": "b1t", + "FOO_1_NUM": "212", + + "BAR_0_STR": "f0t", + "BAR_0_NUM": "101", + "BAR_1_STR": "f1t", + "BAR_1_NUM": "111", + }, + } + + // Load env vars. + if err := env.ParseWithOptions(cfg, opts); err != nil { + log.Fatal(err) + } + + // Print the loaded data. + fmt.Printf("%+v\n", cfg) +} +``` + ### On set hooks You might want to listen to value sets and, for example, log something or do diff --git a/env.go b/env.go index 8b8b724..9585321 100644 --- a/env.go +++ b/env.go @@ -168,6 +168,19 @@ func customOptions(opt Options) Options { return opt } +func optionsWithSliceEnvPrefix(opts Options, index int) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { return Options{ Environment: opts.Environment, @@ -313,6 +326,104 @@ func doParseField(refField reflect.Value, refTypeField reflect.StructField, proc return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) } + if isSliceOfStructs(refTypeField, opts) { + return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + return nil +} + +func isSliceOfStructs(refTypeField reflect.StructField, opts Options) bool { + field := refTypeField.Type + if reflect.Ptr == field.Kind() { + field = field.Elem() + } + + if reflect.Slice != field.Kind() { + return false + } + + field = field.Elem() + + if reflect.Ptr == field.Kind() { + field = field.Elem() + } + + _, ignore := defaultBuiltInParsers[field.Kind()] + + if !ignore { + _, ignore = opts.FuncMap[field] + } + + if !ignore { + _, ignore = reflect.New(field).Interface().(encoding.TextUnmarshaler) + } + + if !ignore { + ignore = reflect.Struct != field.Kind() + } + return !ignore +} + +func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) 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) + } + } + + if len(environments) > 0 { + counter := 0 + for finished := false; !finished; { + finished = true + prefix := fmt.Sprintf("%s%d%c", opts.Prefix, counter, underscore) + for _, variable := range environments { + if strings.HasPrefix(variable, prefix) { + counter++ + finished = false + break + } + } + } + + sliceType := ref.Type() + var initialized int + if reflect.Ptr == ref.Kind() { + sliceType = sliceType.Elem() + // Due to the rest of code the pre-initialized slice has no chance for this situation + initialized = 0 + } else { + initialized = ref.Len() + } + + var capacity int + if capacity = initialized; counter > initialized { + capacity = counter + } + result := reflect.MakeSlice(sliceType, capacity, capacity) + for i := 0; i < capacity; i++ { + item := result.Index(i) + if i < initialized { + item.Set(ref.Index(i)) + } + if err := doParse(item, processField, optionsWithSliceEnvPrefix(opts, i)); err != nil { + return err + } + } + + if reflect.Ptr == ref.Kind() { + resultPtr := reflect.New(sliceType) + resultPtr.Elem().Set(result) + result = resultPtr + } + ref.Set(result) + } + return nil } diff --git a/env_test.go b/env_test.go index 2891fac..a24e2e1 100644 --- a/env_test.go +++ b/env_test.go @@ -2151,3 +2151,67 @@ func TestMultipleTagOptions(t *testing.T) { isEqual(t, "", os.Getenv("URL")) }) } + +func TestIssue298(t *testing.T) { + type Test struct { + Str string `env:"STR"` + Num int `env:"NUM"` + } + type ComplexConfig struct { + Foo *[]Test `envPrefix:"FOO_"` + Bar []Test `envPrefix:"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", "202") // Not overridden + t.Setenv("BAR_1_STR", "b1t") + t.Setenv("BAR_1_NUM", "212") + + t.Setenv("0_STR", "bt") + t.Setenv("1_NUM", "10") + + sample := make([]Test, 1) + sample[0].Str = "overridden text" + sample[0].Num = 99999999 + cfg := ComplexConfig{Bar: sample} + + isNoErr(t, Parse(&cfg)) + + isEqual(t, "f0t", (*cfg.Foo)[0].Str) + isEqual(t, 101, (*cfg.Foo)[0].Num) + isEqual(t, "f1t", (*cfg.Foo)[1].Str) + isEqual(t, 111, (*cfg.Foo)[1].Num) + + isEqual(t, "b0t", cfg.Bar[0].Str) + isEqual(t, 99999999, cfg.Bar[0].Num) + isEqual(t, "b1t", cfg.Bar[1].Str) + isEqual(t, 212, cfg.Bar[1].Num) + + isEqual(t, "bt", cfg.Baz[0].Str) + isEqual(t, 0, cfg.Baz[0].Num) + isEqual(t, "", cfg.Baz[1].Str) + isEqual(t, 10, cfg.Baz[1].Num) +} + +func TestIssue298ErrorNestedFieldRequiredNotSet(t *testing.T) { + type Test struct { + Str string `env:"STR,required"` + Num int `env:"NUM"` + } + type ComplexConfig struct { + Foo *[]Test `envPrefix:"FOO"` + } + + t.Setenv("FOO_0_NUM", "101") + + cfg := ComplexConfig{} + err := Parse(&cfg) + isErrorWithMessage(t, err, `env: required environment variable "FOO_0_STR" is not set`) + isTrue(t, errors.Is(err, EnvVarIsNotSetError{})) +}