Skip to content

Commit 9f8125c

Browse files
feat: add envEnableIf tag support
1 parent cf4a968 commit 9f8125c

File tree

5 files changed

+110
-7
lines changed

5 files changed

+110
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ bin
33
card.png
44
dist
55
codecov*
6+
.idea

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ The following tags are provided:
9797
- `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it
9898
- `envSeparator`: sets the character to be used to separate items in slices and maps (default: `,`)
9999
- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (default: `:`)
100+
- `envEnableIf`: enables parsing of a field only if the specified environment variable is true. If it is false, the field is ignored. If it is true and the field’s environment variable is missing, an error is returned.
100101

101102
### `env` tag options
102103

env.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,10 @@ type FieldParams struct {
545545
Expand bool
546546
Init bool
547547
Ignored bool
548+
// EnabledByEnv holds the name of a boolean env var (from the `envEnableIf`
549+
// struct tag). When that variable resolves to "true", this field is treated
550+
// as both required and notEmpty regardless of its other tag options.
551+
EnabledByEnv string
548552
}
549553

550554
func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) {
@@ -562,6 +566,7 @@ func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, err
562566
DefaultValue: defaultValue,
563567
HasDefaultValue: hasDefaultValue,
564568
Ignored: ownKey == "-",
569+
EnabledByEnv: field.Tag.Get("envEnableIf"),
565570
}
566571

567572
for _, tag := range tags {
@@ -590,6 +595,14 @@ func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, err
590595
return result, nil
591596
}
592597

598+
// isEnvTrue reports whether the environment variable named key resolves to a
599+
// truthy value ("1", "t", "T", "TRUE", "true", "True") using the same rules as
600+
// strconv.ParseBool.
601+
func isEnvTrue(key string, envs map[string]string) bool {
602+
v := strings.ToLower(strings.TrimSpace(envs[key]))
603+
return v == "true" || v == "1" || v == "t"
604+
}
605+
593606
func get(fieldParams FieldParams, opts Options) (val string, err error) {
594607
var exists, isDefault bool
595608

@@ -610,12 +623,24 @@ func get(fieldParams FieldParams, opts Options) (val string, err error) {
610623
defer os.Unsetenv(fieldParams.Key)
611624
}
612625

613-
if fieldParams.Required && !exists && fieldParams.OwnKey != "" {
614-
return "", newVarIsNotSetError(fieldParams.Key)
615-
}
616-
617-
if fieldParams.NotEmpty && val == "" {
618-
return "", newEmptyVarError(fieldParams.Key)
626+
// envEnableIf: when the named controlling env var is truthy, this field
627+
// is treated as both required and notEmpty.
628+
if fieldParams.EnabledByEnv != "" && isEnvTrue(fieldParams.EnabledByEnv, opts.Environment) {
629+
if !exists && fieldParams.OwnKey != "" {
630+
return "", newVarIsNotSetError(fieldParams.Key)
631+
}
632+
if val == "" {
633+
return "", newEmptyVarError(fieldParams.Key)
634+
}
635+
// Fall through so the value is used normally.
636+
} else {
637+
// Standard required / notEmpty checks (no EnabledByEnv override).
638+
if fieldParams.Required && !exists && fieldParams.OwnKey != "" {
639+
return "", newVarIsNotSetError(fieldParams.Key)
640+
}
641+
if fieldParams.NotEmpty && val == "" {
642+
return "", newEmptyVarError(fieldParams.Key)
643+
}
619644
}
620645

621646
if fieldParams.LoadFile && val != "" {

env_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2410,3 +2410,79 @@ func TestEnvBleed(t *testing.T) {
24102410
isEqual(t, "", cfg.Foo)
24112411
})
24122412
}
2413+
2414+
func TestEnvEnableIf(t *testing.T) {
2415+
2416+
t.Run("disabled and env missing should not error", func(t *testing.T) {
2417+
t.Setenv("EXTERNAL_URL_ENABLED", "false")
2418+
2419+
type external struct {
2420+
ExternalURLEnabled bool `env:"EXTERNAL_URL_ENABLED"`
2421+
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"EXTERNAL_URL_ENABLED"`
2422+
}
2423+
2424+
cfg := external{}
2425+
isNoErr(t, Parse(&cfg))
2426+
isEqual(t, cfg.ExternalURL, "")
2427+
})
2428+
2429+
t.Run("enabled but env missing should error", func(t *testing.T) {
2430+
t.Setenv("EXTERNAL_URL_ENABLED", "true")
2431+
2432+
type external struct {
2433+
ExternalURLEnabled bool `env:"EXTERNAL_URL_ENABLED"`
2434+
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"EXTERNAL_URL_ENABLED"`
2435+
}
2436+
2437+
cfg := external{}
2438+
err := Parse(&cfg)
2439+
2440+
if err == nil {
2441+
t.Fatalf("expected error when EXTERNAL_URL is missing but enabled")
2442+
}
2443+
})
2444+
2445+
t.Run("enabled and env present should parse", func(t *testing.T) {
2446+
t.Setenv("EXTERNAL_URL_ENABLED", "true")
2447+
t.Setenv("EXTERNAL_URL", "https://foo.bar")
2448+
2449+
type external struct {
2450+
ExternalURLEnabled bool `env:"EXTERNAL_URL_ENABLED"`
2451+
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"EXTERNAL_URL_ENABLED"`
2452+
}
2453+
2454+
cfg := external{}
2455+
isNoErr(t, Parse(&cfg))
2456+
isEqual(t, cfg.ExternalURL, "https://foo.bar")
2457+
})
2458+
2459+
t.Run("invalid envEnableIf key should not apply condition", func(t *testing.T) {
2460+
t.Setenv("EXTERNAL_URL", "https://foo.bar")
2461+
2462+
type external struct {
2463+
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"INVALID_KEY"`
2464+
}
2465+
2466+
cfg := external{}
2467+
isNoErr(t, Parse(&cfg))
2468+
isEqual(t, cfg.ExternalURL, "https://foo.bar")
2469+
})
2470+
2471+
t.Run("multiple fields using same envEnableIf", func(t *testing.T) {
2472+
t.Setenv("FEATURE_ENABLED", "true")
2473+
t.Setenv("URL_ONE", "https://one.bar")
2474+
t.Setenv("URL_TWO", "https://two.bar")
2475+
2476+
type external struct {
2477+
FeatureEnabled bool `env:"FEATURE_ENABLED"`
2478+
URL1 string `env:"URL_ONE" envEnableIf:"FEATURE_ENABLED"`
2479+
URL2 string `env:"URL_TWO" envEnableIf:"FEATURE_ENABLED"`
2480+
}
2481+
2482+
cfg := external{}
2483+
isNoErr(t, Parse(&cfg))
2484+
2485+
isEqual(t, cfg.URL1, "https://one.bar")
2486+
isEqual(t, cfg.URL2, "https://two.bar")
2487+
})
2488+
}

error.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func (e NoParserError) Error() string {
9494

9595
// NoSupportedTagOptionError occurs when the given tag is not supported.
9696
// Built-in supported tags: "", "file", "required", "unset", "notEmpty",
97-
// "expand", "envDefault", and "envSeparator".
97+
// "expand", "envDefault", "envSeparator", and "envEnableIf".
9898
type NoSupportedTagOptionError struct {
9999
Tag string
100100
}

0 commit comments

Comments
 (0)