Skip to content

Commit 6f3a5c0

Browse files
authored
fix: better handle envDefault, refactor merge options (#349)
* 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
1 parent 52e7186 commit 6f3a5c0

File tree

3 files changed

+55
-51
lines changed

3 files changed

+55
-51
lines changed

Diff for: README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ You can see the full documentation and list of examples at [pkg.go.dev](https://
5454
- `Parse`: parse the current environment into a type
5555
- `ParseAs`: parse the current environment into a type using generics
5656
- `ParseWithOptions`: parse the current environment into a type with custom options
57-
- `ParseAsithOptions`: parse the current environment into a type with custom options and using generics
57+
- `ParseAsWithOptions`: parse the current environment into a type with custom options and using generics
5858
- `Must`: can be used to wrap `Parse.*` calls to panic on error
5959
- `GetFieldParams`: get the `env` parsed options for a type
6060
- `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`:
115115

116116
- `Environment`: keys and values to be used instead of `os.Environ()`
117117
- `TagName`: specifies another tag name to use rather than the default `env`
118+
- `PrefixTagName`: specifies another prefix tag name to use rather than the default `envPrefix`
119+
- `DefaultValueTagName`: specifies another default tag name to use rather than the default `envDefault`
118120
- `RequiredIfNoDef`: set all `env` fields as required if they do not declare `envDefault`
119121
- `OnSet`: allows to hook into the `env` parsing and do something when a value is set
120122
- `Prefix`: prefix to be used in all environment variables

Diff for: env.go

+49-49
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ type Options struct {
173173
func (opts *Options) getRawEnv(s string) string {
174174
val := opts.rawEnvVars[s]
175175
if val == "" {
176-
return opts.Environment[s]
176+
val = opts.Environment[s]
177177
}
178-
return val
178+
return os.Expand(val, opts.getRawEnv)
179179
}
180180

181181
func defaultOptions() Options {
@@ -189,32 +189,45 @@ func defaultOptions() Options {
189189
}
190190
}
191191

192-
func customOptions(opt Options) Options {
193-
defOpts := defaultOptions()
194-
if opt.TagName == "" {
195-
opt.TagName = defOpts.TagName
196-
}
197-
if opt.PrefixTagName == "" {
198-
opt.PrefixTagName = defOpts.PrefixTagName
199-
}
200-
if opt.DefaultValueTagName == "" {
201-
opt.DefaultValueTagName = defOpts.DefaultValueTagName
202-
}
203-
if opt.Environment == nil {
204-
opt.Environment = defOpts.Environment
205-
}
206-
if opt.FuncMap == nil {
207-
opt.FuncMap = map[reflect.Type]ParserFunc{}
208-
}
209-
if opt.rawEnvVars == nil {
210-
opt.rawEnvVars = defOpts.rawEnvVars
211-
}
212-
for k, v := range defOpts.FuncMap {
213-
if _, exists := opt.FuncMap[k]; !exists {
214-
opt.FuncMap[k] = v
192+
func mergeOptions[T any](target, source *T) {
193+
targetPtr := reflect.ValueOf(target).Elem()
194+
sourcePtr := reflect.ValueOf(source).Elem()
195+
196+
targetType := targetPtr.Type()
197+
for i := 0; i < targetPtr.NumField(); i++ {
198+
targetField := targetPtr.Field(i)
199+
sourceField := sourcePtr.FieldByName(targetType.Field(i).Name)
200+
201+
if targetField.CanSet() && !isZero(sourceField) {
202+
switch targetField.Kind() {
203+
case reflect.Map:
204+
if !sourceField.IsZero() {
205+
iter := sourceField.MapRange()
206+
for iter.Next() {
207+
targetField.SetMapIndex(iter.Key(), iter.Value())
208+
}
209+
}
210+
default:
211+
targetField.Set(sourceField)
212+
}
215213
}
216214
}
217-
return opt
215+
}
216+
217+
func isZero(v reflect.Value) bool {
218+
switch v.Kind() {
219+
case reflect.Func, reflect.Map, reflect.Slice:
220+
return v.IsNil()
221+
default:
222+
zero := reflect.Zero(v.Type())
223+
return v.Interface() == zero.Interface()
224+
}
225+
}
226+
227+
func customOptions(opts Options) Options {
228+
defOpts := defaultOptions()
229+
mergeOptions(&defOpts, &opts)
230+
return defOpts
218231
}
219232

220233
func optionsWithSliceEnvPrefix(opts Options, index int) Options {
@@ -386,43 +399,30 @@ func doParseField(
386399
return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
387400
}
388401

389-
if isSliceOfStructs(refTypeField, opts) {
402+
if isSliceOfStructs(refTypeField) {
390403
return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
391404
}
392405

393406
return nil
394407
}
395408

396-
func isSliceOfStructs(refTypeField reflect.StructField, opts Options) bool {
409+
func isSliceOfStructs(refTypeField reflect.StructField) bool {
397410
field := refTypeField.Type
398-
if field.Kind() == reflect.Ptr {
399-
field = field.Elem()
400-
}
401-
402-
if field.Kind() != reflect.Slice {
403-
return false
404-
}
405-
406-
field = field.Elem()
407411

412+
// *[]struct
408413
if field.Kind() == reflect.Ptr {
409414
field = field.Elem()
415+
if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct {
416+
return true
417+
}
410418
}
411419

412-
_, ignore := defaultBuiltInParsers[field.Kind()]
413-
414-
if !ignore {
415-
_, ignore = opts.FuncMap[field]
416-
}
417-
418-
if !ignore {
419-
_, ignore = reflect.New(field).Interface().(encoding.TextUnmarshaler)
420+
// []struct{}
421+
if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct {
422+
return true
420423
}
421424

422-
if !ignore {
423-
ignore = field.Kind() != reflect.Struct
424-
}
425-
return !ignore
425+
return false
426426
}
427427

428428
func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) error {

Diff for: env_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -1019,14 +1019,16 @@ func TestParseExpandWithDefaultOption(t *testing.T) {
10191019
CompoundDefault string `env:"HOST_PORT,expand" envDefault:"${HOST}:${PORT}"`
10201020
SimpleDefault string `env:"DEFAULT,expand" envDefault:"def1"`
10211021
MixedDefault string `env:"MIXED_DEFAULT,expand" envDefault:"$USER@${HOST}:${OTHER_PORT}"`
1022-
OverrideDefault string `env:"OVERRIDE_DEFAULT,expand" envDefault:"$THIS_SHOULD_NOT_BE_USED"`
1022+
OverrideDefault string `env:"OVERRIDE_DEFAULT,expand"`
1023+
DefaultIsExpand string `env:"DEFAULT_IS_EXPAND,expand" envDefault:"$THIS_IS_EXPAND"`
10231024
NoDefault string `env:"NO_DEFAULT,expand"`
10241025
}
10251026

10261027
t.Setenv("OTHER_PORT", "5000")
10271028
t.Setenv("USER", "jhon")
10281029
t.Setenv("THIS_IS_USED", "this is used instead")
10291030
t.Setenv("OVERRIDE_DEFAULT", "msg: ${THIS_IS_USED}")
1031+
t.Setenv("THIS_IS_EXPAND", "msg: ${THIS_IS_USED}")
10301032
t.Setenv("NO_DEFAULT", "$PORT:$OTHER_PORT")
10311033

10321034
cfg := config{}

0 commit comments

Comments
 (0)