Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ bin
card.png
dist
codecov*
.idea
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ The following tags are provided:
- `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it
- `envSeparator`: sets the character to be used to separate items in slices and maps (default: `,`)
- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (default: `:`)
- `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.

### `env` tag options

Expand Down
37 changes: 31 additions & 6 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ type FieldParams struct {
Expand bool
Init bool
Ignored bool
// EnabledByEnv holds the name of a boolean env var (from the `envEnableIf`
// struct tag). When that variable resolves to "true", this field is treated
// as both required and notEmpty regardless of its other tag options.
EnabledByEnv string
}

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

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

// isEnvTrue reports whether the environment variable named key resolves to a
// truthy value ("1", "t", "T", "TRUE", "true", "True") using the same rules as
// strconv.ParseBool.
func isEnvTrue(key string, envs map[string]string) bool {
v := strings.ToLower(strings.TrimSpace(envs[key]))
return v == "true" || v == "1" || v == "t"
}

func get(fieldParams FieldParams, opts Options) (val string, err error) {
var exists, isDefault bool

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

if fieldParams.Required && !exists && fieldParams.OwnKey != "" {
return "", newVarIsNotSetError(fieldParams.Key)
}

if fieldParams.NotEmpty && val == "" {
return "", newEmptyVarError(fieldParams.Key)
// envEnableIf: when the named controlling env var is truthy, this field
// is treated as both required and notEmpty.
if fieldParams.EnabledByEnv != "" && isEnvTrue(fieldParams.EnabledByEnv, opts.Environment) {
if !exists && fieldParams.OwnKey != "" {
return "", newVarIsNotSetError(fieldParams.Key)
}
if val == "" {
return "", newEmptyVarError(fieldParams.Key)
}
// Fall through so the value is used normally.
} else {
// Standard required / notEmpty checks (no EnabledByEnv override).
if fieldParams.Required && !exists && fieldParams.OwnKey != "" {
return "", newVarIsNotSetError(fieldParams.Key)
}
if fieldParams.NotEmpty && val == "" {
return "", newEmptyVarError(fieldParams.Key)
}
}

if fieldParams.LoadFile && val != "" {
Expand Down
75 changes: 75 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2410,3 +2410,78 @@ func TestEnvBleed(t *testing.T) {
isEqual(t, "", cfg.Foo)
})
}

func TestEnvEnableIf(t *testing.T) {
t.Run("disabled and env missing should not error", func(t *testing.T) {
t.Setenv("EXTERNAL_URL_ENABLED", "false")

type external struct {
ExternalURLEnabled bool `env:"EXTERNAL_URL_ENABLED"`
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"EXTERNAL_URL_ENABLED"`
}

cfg := external{}
isNoErr(t, Parse(&cfg))
isEqual(t, cfg.ExternalURL, "")
})

t.Run("enabled but env missing should error", func(t *testing.T) {
t.Setenv("EXTERNAL_URL_ENABLED", "true")

type external struct {
ExternalURLEnabled bool `env:"EXTERNAL_URL_ENABLED"`
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"EXTERNAL_URL_ENABLED"`
}

cfg := external{}
err := Parse(&cfg)

if err == nil {
t.Fatalf("expected error when EXTERNAL_URL is missing but enabled")
}
})

t.Run("enabled and env present should parse", func(t *testing.T) {
t.Setenv("EXTERNAL_URL_ENABLED", "true")
t.Setenv("EXTERNAL_URL", "https://foo.bar")

type external struct {
ExternalURLEnabled bool `env:"EXTERNAL_URL_ENABLED"`
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"EXTERNAL_URL_ENABLED"`
}

cfg := external{}
isNoErr(t, Parse(&cfg))
isEqual(t, cfg.ExternalURL, "https://foo.bar")
})

t.Run("invalid envEnableIf key should not apply condition", func(t *testing.T) {
t.Setenv("EXTERNAL_URL", "https://foo.bar")

type external struct {
ExternalURL string `env:"EXTERNAL_URL" envEnableIf:"INVALID_KEY"`
}

cfg := external{}
isNoErr(t, Parse(&cfg))
isEqual(t, cfg.ExternalURL, "https://foo.bar")
})

t.Run("multiple fields using same envEnableIf", func(t *testing.T) {
t.Setenv("FEATURE_ENABLED", "true")
t.Setenv("URL_ONE", "https://one.bar")
t.Setenv("URL_TWO", "https://two.bar")

type external struct {
FeatureEnabled bool `env:"FEATURE_ENABLED"`
URL1 string `env:"URL_ONE" envEnableIf:"FEATURE_ENABLED"`
URL2 string `env:"URL_TWO" envEnableIf:"FEATURE_ENABLED"`
}

cfg := external{}
isNoErr(t, Parse(&cfg))

isEqual(t, cfg.URL1, "https://one.bar")
isEqual(t, cfg.URL2, "https://two.bar")
})
}
2 changes: 1 addition & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func (e NoParserError) Error() string {

// NoSupportedTagOptionError occurs when the given tag is not supported.
// Built-in supported tags: "", "file", "required", "unset", "notEmpty",
// "expand", "envDefault", and "envSeparator".
// "expand", "envDefault", "envSeparator", and "envEnableIf".
type NoSupportedTagOptionError struct {
Tag string
}
Expand Down