Skip to content

Commit

Permalink
fix: better handle envDefault, refactor merge options (caarlos0#349)
Browse files Browse the repository at this point in the history
* fix: modify the env tag options expand logic to recursively handle ${var} or $var

* chore: modify the merging logic of opts and defOptions in the customOptions function

* docs: update parse options

* docs: typo

* refactor: simplify the judgment logic of the isSliceOfStructs function

* fix: typo

* fix: typo
  • Loading branch information
astak16 authored Dec 12, 2024
1 parent 52e7186 commit 6f3a5c0
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 51 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ You can see the full documentation and list of examples at [pkg.go.dev](https://
- `Parse`: parse the current environment into a type
- `ParseAs`: parse the current environment into a type using generics
- `ParseWithOptions`: parse the current environment into a type with custom options
- `ParseAsithOptions`: parse the current environment into a type with custom options and using generics
- `ParseAsWithOptions`: parse the current environment into a type with custom options and using generics
- `Must`: can be used to wrap `Parse.*` calls to panic on error
- `GetFieldParams`: get the `env` parsed options for a type
- `GetFieldParamsWithOptions`: get the `env` parsed options for a type with custom options
Expand Down Expand Up @@ -115,6 +115,8 @@ There are a few options available in the functions that end with `WithOptions`:

- `Environment`: keys and values to be used instead of `os.Environ()`
- `TagName`: specifies another tag name to use rather than the default `env`
- `PrefixTagName`: specifies another prefix tag name to use rather than the default `envPrefix`
- `DefaultValueTagName`: specifies another default tag name to use rather than the default `envDefault`
- `RequiredIfNoDef`: set all `env` fields as required if they do not declare `envDefault`
- `OnSet`: allows to hook into the `env` parsing and do something when a value is set
- `Prefix`: prefix to be used in all environment variables
Expand Down
98 changes: 49 additions & 49 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ type Options struct {
func (opts *Options) getRawEnv(s string) string {
val := opts.rawEnvVars[s]
if val == "" {
return opts.Environment[s]
val = opts.Environment[s]
}
return val
return os.Expand(val, opts.getRawEnv)
}

func defaultOptions() Options {
Expand All @@ -189,32 +189,45 @@ func defaultOptions() Options {
}
}

func customOptions(opt Options) Options {
defOpts := defaultOptions()
if opt.TagName == "" {
opt.TagName = defOpts.TagName
}
if opt.PrefixTagName == "" {
opt.PrefixTagName = defOpts.PrefixTagName
}
if opt.DefaultValueTagName == "" {
opt.DefaultValueTagName = defOpts.DefaultValueTagName
}
if opt.Environment == nil {
opt.Environment = defOpts.Environment
}
if opt.FuncMap == nil {
opt.FuncMap = map[reflect.Type]ParserFunc{}
}
if opt.rawEnvVars == nil {
opt.rawEnvVars = defOpts.rawEnvVars
}
for k, v := range defOpts.FuncMap {
if _, exists := opt.FuncMap[k]; !exists {
opt.FuncMap[k] = v
func mergeOptions[T any](target, source *T) {
targetPtr := reflect.ValueOf(target).Elem()
sourcePtr := reflect.ValueOf(source).Elem()

targetType := targetPtr.Type()
for i := 0; i < targetPtr.NumField(); i++ {
targetField := targetPtr.Field(i)
sourceField := sourcePtr.FieldByName(targetType.Field(i).Name)

if targetField.CanSet() && !isZero(sourceField) {
switch targetField.Kind() {
case reflect.Map:
if !sourceField.IsZero() {
iter := sourceField.MapRange()
for iter.Next() {
targetField.SetMapIndex(iter.Key(), iter.Value())
}
}
default:
targetField.Set(sourceField)
}
}
}
return opt
}

func isZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Func, reflect.Map, reflect.Slice:
return v.IsNil()
default:
zero := reflect.Zero(v.Type())
return v.Interface() == zero.Interface()
}
}

func customOptions(opts Options) Options {
defOpts := defaultOptions()
mergeOptions(&defOpts, &opts)
return defOpts
}

func optionsWithSliceEnvPrefix(opts Options, index int) Options {
Expand Down Expand Up @@ -386,43 +399,30 @@ func doParseField(
return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
}

if isSliceOfStructs(refTypeField, opts) {
if isSliceOfStructs(refTypeField) {
return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
}

return nil
}

func isSliceOfStructs(refTypeField reflect.StructField, opts Options) bool {
func isSliceOfStructs(refTypeField reflect.StructField) bool {
field := refTypeField.Type
if field.Kind() == reflect.Ptr {
field = field.Elem()
}

if field.Kind() != reflect.Slice {
return false
}

field = field.Elem()

// *[]struct
if field.Kind() == reflect.Ptr {
field = field.Elem()
if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct {
return true
}
}

_, ignore := defaultBuiltInParsers[field.Kind()]

if !ignore {
_, ignore = opts.FuncMap[field]
}

if !ignore {
_, ignore = reflect.New(field).Interface().(encoding.TextUnmarshaler)
// []struct{}
if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct {
return true
}

if !ignore {
ignore = field.Kind() != reflect.Struct
}
return !ignore
return false
}

func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) error {
Expand Down
4 changes: 3 additions & 1 deletion env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,14 +1019,16 @@ func TestParseExpandWithDefaultOption(t *testing.T) {
CompoundDefault string `env:"HOST_PORT,expand" envDefault:"${HOST}:${PORT}"`
SimpleDefault string `env:"DEFAULT,expand" envDefault:"def1"`
MixedDefault string `env:"MIXED_DEFAULT,expand" envDefault:"$USER@${HOST}:${OTHER_PORT}"`
OverrideDefault string `env:"OVERRIDE_DEFAULT,expand" envDefault:"$THIS_SHOULD_NOT_BE_USED"`
OverrideDefault string `env:"OVERRIDE_DEFAULT,expand"`
DefaultIsExpand string `env:"DEFAULT_IS_EXPAND,expand" envDefault:"$THIS_IS_EXPAND"`
NoDefault string `env:"NO_DEFAULT,expand"`
}

t.Setenv("OTHER_PORT", "5000")
t.Setenv("USER", "jhon")
t.Setenv("THIS_IS_USED", "this is used instead")
t.Setenv("OVERRIDE_DEFAULT", "msg: ${THIS_IS_USED}")
t.Setenv("THIS_IS_EXPAND", "msg: ${THIS_IS_USED}")
t.Setenv("NO_DEFAULT", "$PORT:$OTHER_PORT")

cfg := config{}
Expand Down

0 comments on commit 6f3a5c0

Please sign in to comment.