diff --git a/README.md b/README.md index 352a8c9..de6f824 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/env.go b/env.go index 9be6522..eaf15cd 100644 --- a/env.go +++ b/env.go @@ -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 { @@ -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 { @@ -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 { diff --git a/env_test.go b/env_test.go index 1f717a8..3607aaa 100644 --- a/env_test.go +++ b/env_test.go @@ -1019,7 +1019,8 @@ 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"` } @@ -1027,6 +1028,7 @@ func TestParseExpandWithDefaultOption(t *testing.T) { 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{}