Skip to content

Commit 68793c0

Browse files
RancbarHamid Reza Ranjbar
and
Hamid Reza Ranjbar
authoredJul 19, 2024··
feat: mapping slice of complex struct (#312)
* feat: mapping slice of complex struct #298 * feat: support predefined values and pre initialized structs * refactor: some improvement * refactor: some improvement * test: support normal features for nested fields * test: trying to fix `gofumpt` lint issues * chore: add sample for complex struct in readme --------- Co-authored-by: Hamid Reza Ranjbar <hamidreza.ranjbar@snapp.cab>
1 parent 0de9383 commit 68793c0

File tree

3 files changed

+230
-0
lines changed

3 files changed

+230
-0
lines changed
 

‎README.md

+55
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,61 @@ func main() {
476476
}
477477
```
478478

479+
### Complex objects inside array (slice)
480+
481+
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.
482+
It is possible to use prefix tag too.
483+
484+
Here's an example with and without prefix tag:
485+
486+
```go
487+
package main
488+
489+
import (
490+
"fmt"
491+
"log"
492+
493+
"github.com/caarlos0/env/v11"
494+
)
495+
496+
type Test struct {
497+
Str string `env:"STR"`
498+
Num int `env:"NUM"`
499+
}
500+
type ComplexConfig struct {
501+
Baz []Test `env:",init"`
502+
Bar []Test `envPrefix:"BAR"`
503+
Foo *[]Test `envPrefix:"FOO_"`
504+
}
505+
506+
func main() {
507+
cfg := &ComplexConfig{}
508+
opts := env.Options{
509+
Environment: map[string]string{
510+
"0_STR": "bt",
511+
"1_NUM": "10",
512+
513+
"FOO_0_STR": "b0t",
514+
"FOO_1_STR": "b1t",
515+
"FOO_1_NUM": "212",
516+
517+
"BAR_0_STR": "f0t",
518+
"BAR_0_NUM": "101",
519+
"BAR_1_STR": "f1t",
520+
"BAR_1_NUM": "111",
521+
},
522+
}
523+
524+
// Load env vars.
525+
if err := env.ParseWithOptions(cfg, opts); err != nil {
526+
log.Fatal(err)
527+
}
528+
529+
// Print the loaded data.
530+
fmt.Printf("%+v\n", cfg)
531+
}
532+
```
533+
479534
### On set hooks
480535

481536
You might want to listen to value sets and, for example, log something or do

‎env.go

+111
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ func customOptions(opt Options) Options {
168168
return opt
169169
}
170170

171+
func optionsWithSliceEnvPrefix(opts Options, index int) Options {
172+
return Options{
173+
Environment: opts.Environment,
174+
TagName: opts.TagName,
175+
RequiredIfNoDef: opts.RequiredIfNoDef,
176+
OnSet: opts.OnSet,
177+
Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index),
178+
UseFieldNameByDefault: opts.UseFieldNameByDefault,
179+
FuncMap: opts.FuncMap,
180+
rawEnvVars: opts.rawEnvVars,
181+
}
182+
}
183+
171184
func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options {
172185
return Options{
173186
Environment: opts.Environment,
@@ -313,6 +326,104 @@ func doParseField(refField reflect.Value, refTypeField reflect.StructField, proc
313326
return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
314327
}
315328

329+
if isSliceOfStructs(refTypeField, opts) {
330+
return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
331+
}
332+
333+
return nil
334+
}
335+
336+
func isSliceOfStructs(refTypeField reflect.StructField, opts Options) bool {
337+
field := refTypeField.Type
338+
if reflect.Ptr == field.Kind() {
339+
field = field.Elem()
340+
}
341+
342+
if reflect.Slice != field.Kind() {
343+
return false
344+
}
345+
346+
field = field.Elem()
347+
348+
if reflect.Ptr == field.Kind() {
349+
field = field.Elem()
350+
}
351+
352+
_, ignore := defaultBuiltInParsers[field.Kind()]
353+
354+
if !ignore {
355+
_, ignore = opts.FuncMap[field]
356+
}
357+
358+
if !ignore {
359+
_, ignore = reflect.New(field).Interface().(encoding.TextUnmarshaler)
360+
}
361+
362+
if !ignore {
363+
ignore = reflect.Struct != field.Kind()
364+
}
365+
return !ignore
366+
}
367+
368+
func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) error {
369+
if opts.Prefix != "" && !strings.HasSuffix(opts.Prefix, string(underscore)) {
370+
opts.Prefix += string(underscore)
371+
}
372+
373+
var environments []string
374+
for environment := range opts.Environment {
375+
if strings.HasPrefix(environment, opts.Prefix) {
376+
environments = append(environments, environment)
377+
}
378+
}
379+
380+
if len(environments) > 0 {
381+
counter := 0
382+
for finished := false; !finished; {
383+
finished = true
384+
prefix := fmt.Sprintf("%s%d%c", opts.Prefix, counter, underscore)
385+
for _, variable := range environments {
386+
if strings.HasPrefix(variable, prefix) {
387+
counter++
388+
finished = false
389+
break
390+
}
391+
}
392+
}
393+
394+
sliceType := ref.Type()
395+
var initialized int
396+
if reflect.Ptr == ref.Kind() {
397+
sliceType = sliceType.Elem()
398+
// Due to the rest of code the pre-initialized slice has no chance for this situation
399+
initialized = 0
400+
} else {
401+
initialized = ref.Len()
402+
}
403+
404+
var capacity int
405+
if capacity = initialized; counter > initialized {
406+
capacity = counter
407+
}
408+
result := reflect.MakeSlice(sliceType, capacity, capacity)
409+
for i := 0; i < capacity; i++ {
410+
item := result.Index(i)
411+
if i < initialized {
412+
item.Set(ref.Index(i))
413+
}
414+
if err := doParse(item, processField, optionsWithSliceEnvPrefix(opts, i)); err != nil {
415+
return err
416+
}
417+
}
418+
419+
if reflect.Ptr == ref.Kind() {
420+
resultPtr := reflect.New(sliceType)
421+
resultPtr.Elem().Set(result)
422+
result = resultPtr
423+
}
424+
ref.Set(result)
425+
}
426+
316427
return nil
317428
}
318429

‎env_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -2151,3 +2151,67 @@ func TestMultipleTagOptions(t *testing.T) {
21512151
isEqual(t, "", os.Getenv("URL"))
21522152
})
21532153
}
2154+
2155+
func TestIssue298(t *testing.T) {
2156+
type Test struct {
2157+
Str string `env:"STR"`
2158+
Num int `env:"NUM"`
2159+
}
2160+
type ComplexConfig struct {
2161+
Foo *[]Test `envPrefix:"FOO_"`
2162+
Bar []Test `envPrefix:"BAR"`
2163+
Baz []Test `env:",init"`
2164+
}
2165+
2166+
t.Setenv("FOO_0_STR", "f0t")
2167+
t.Setenv("FOO_0_NUM", "101")
2168+
t.Setenv("FOO_1_STR", "f1t")
2169+
t.Setenv("FOO_1_NUM", "111")
2170+
2171+
t.Setenv("BAR_0_STR", "b0t")
2172+
// t.Setenv("BAR_0_NUM", "202") // Not overridden
2173+
t.Setenv("BAR_1_STR", "b1t")
2174+
t.Setenv("BAR_1_NUM", "212")
2175+
2176+
t.Setenv("0_STR", "bt")
2177+
t.Setenv("1_NUM", "10")
2178+
2179+
sample := make([]Test, 1)
2180+
sample[0].Str = "overridden text"
2181+
sample[0].Num = 99999999
2182+
cfg := ComplexConfig{Bar: sample}
2183+
2184+
isNoErr(t, Parse(&cfg))
2185+
2186+
isEqual(t, "f0t", (*cfg.Foo)[0].Str)
2187+
isEqual(t, 101, (*cfg.Foo)[0].Num)
2188+
isEqual(t, "f1t", (*cfg.Foo)[1].Str)
2189+
isEqual(t, 111, (*cfg.Foo)[1].Num)
2190+
2191+
isEqual(t, "b0t", cfg.Bar[0].Str)
2192+
isEqual(t, 99999999, cfg.Bar[0].Num)
2193+
isEqual(t, "b1t", cfg.Bar[1].Str)
2194+
isEqual(t, 212, cfg.Bar[1].Num)
2195+
2196+
isEqual(t, "bt", cfg.Baz[0].Str)
2197+
isEqual(t, 0, cfg.Baz[0].Num)
2198+
isEqual(t, "", cfg.Baz[1].Str)
2199+
isEqual(t, 10, cfg.Baz[1].Num)
2200+
}
2201+
2202+
func TestIssue298ErrorNestedFieldRequiredNotSet(t *testing.T) {
2203+
type Test struct {
2204+
Str string `env:"STR,required"`
2205+
Num int `env:"NUM"`
2206+
}
2207+
type ComplexConfig struct {
2208+
Foo *[]Test `envPrefix:"FOO"`
2209+
}
2210+
2211+
t.Setenv("FOO_0_NUM", "101")
2212+
2213+
cfg := ComplexConfig{}
2214+
err := Parse(&cfg)
2215+
isErrorWithMessage(t, err, `env: required environment variable "FOO_0_STR" is not set`)
2216+
isTrue(t, errors.Is(err, EnvVarIsNotSetError{}))
2217+
}

0 commit comments

Comments
 (0)
Please sign in to comment.