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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ The following tags are provided:
- `env`: sets the environment variable name and optionally takes the tag options described below
- `envDefault`: sets the default value for the field
- `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it
- `envSuffix`: can be used in a field that is a complex type to set a suffix 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: `:`)

Expand All @@ -116,10 +117,12 @@ 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`
- `SuffixTagName`: specifies another suffix tag name to use rather than the default `envSuffix`
- `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
- `Suffix`: suffix to be used in all environment variables
- `UseFieldNameByDefault`: defines whether or not `env` should use the field name by default if the `env` key is missing
- `FuncMap`: custom parse functions for custom types

Expand Down
23 changes: 17 additions & 6 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ type Options struct {
// PrefixTagName specifies another prefix tag name to use rather than the default 'envPrefix'.
PrefixTagName string

// SuffixTagName specifies another suffix tag name to use rather than the default 'envSuffix'.
SuffixTagName string

// DefaultValueTagName specifies another default tag name to use rather than the default 'envDefault'.
DefaultValueTagName string

Expand All @@ -153,6 +156,9 @@ type Options struct {
// Prefix define a prefix for every key.
Prefix string

// Suffix define a suffix for every key.
Suffix string

// UseFieldNameByDefault defines whether or not `env` should use the field
// name by default if the `env` key is missing.
// Note that the field name will be "converted" to conform with environment
Expand Down Expand Up @@ -188,6 +194,7 @@ func defaultOptions() Options {
return Options{
TagName: "env",
PrefixTagName: "envPrefix",
SuffixTagName: "envSuffix",
DefaultValueTagName: "envDefault",
Environment: toMap(os.Environ()),
FuncMap: defaultTypeParsers(),
Expand Down Expand Up @@ -242,26 +249,30 @@ func optionsWithSliceEnvPrefix(opts Options, index int) Options {
Environment: opts.Environment,
TagName: opts.TagName,
PrefixTagName: opts.PrefixTagName,
SuffixTagName: opts.SuffixTagName,
DefaultValueTagName: opts.DefaultValueTagName,
RequiredIfNoDef: opts.RequiredIfNoDef,
OnSet: opts.OnSet,
Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index),
Suffix: opts.Suffix,
UseFieldNameByDefault: opts.UseFieldNameByDefault,
SetDefaultsForZeroValuesOnly: opts.SetDefaultsForZeroValuesOnly,
FuncMap: opts.FuncMap,
rawEnvVars: opts.rawEnvVars,
}
}

func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options {
func optionsWithEnvPrefixSuffix(field reflect.StructField, opts Options) Options {
return Options{
Environment: opts.Environment,
TagName: opts.TagName,
PrefixTagName: opts.PrefixTagName,
SuffixTagName: opts.SuffixTagName,
DefaultValueTagName: opts.DefaultValueTagName,
RequiredIfNoDef: opts.RequiredIfNoDef,
OnSet: opts.OnSet,
Prefix: opts.Prefix + field.Tag.Get(opts.PrefixTagName),
Suffix: field.Tag.Get(opts.SuffixTagName) + opts.Suffix,
UseFieldNameByDefault: opts.UseFieldNameByDefault,
SetDefaultsForZeroValuesOnly: opts.SetDefaultsForZeroValuesOnly,
FuncMap: opts.FuncMap,
Expand Down Expand Up @@ -380,10 +391,10 @@ func doParseField(
return nil
}
if refField.Kind() == reflect.Ptr && refField.Elem().Kind() == reflect.Struct && !refField.IsNil() {
return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts))
return parseInternal(refField.Interface(), processField, optionsWithEnvPrefixSuffix(refTypeField, opts))
}
if refField.Kind() == reflect.Struct && refField.CanAddr() && refField.Type().Name() == "" {
return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefix(refTypeField, opts))
return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefixSuffix(refTypeField, opts))
}

params, err := parseFieldParams(refTypeField, opts)
Expand All @@ -405,11 +416,11 @@ func doParseField(
}

if refField.Kind() == reflect.Struct {
return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts))
return doParse(refField, processField, optionsWithEnvPrefixSuffix(refTypeField, opts))
}

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

return nil
Expand Down Expand Up @@ -557,7 +568,7 @@ func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, err

result := FieldParams{
OwnKey: ownKey,
Key: opts.Prefix + ownKey,
Key: opts.Prefix + ownKey + opts.Suffix,
Required: opts.RequiredIfNoDef,
DefaultValue: defaultValue,
HasDefaultValue: hasDefaultValue,
Expand Down
215 changes: 215 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,221 @@ func TestComplePrefix(t *testing.T) {
isEqual(t, "blahhh", cfg.Blah)
}

func TestSuffix(t *testing.T) {
type Config struct {
Home string `env:"HOME"`
}
type ComplexConfig struct {
Foo Config `envSuffix:"_FOO"`
Bar Config `envSuffix:"_BAR"`
Clean Config
}
cfg := ComplexConfig{}
isNoErr(t, ParseWithOptions(&cfg, Options{Environment: map[string]string{"HOME_FOO": "/foo", "HOME_BAR": "/bar", "HOME": "/clean"}}))
isEqual(t, "/foo", cfg.Foo.Home)
isEqual(t, "/bar", cfg.Bar.Home)
isEqual(t, "/clean", cfg.Clean.Home)
}

func TestSuffixPointers(t *testing.T) {
type Test struct {
Str string `env:"TEST"`
}
type ComplexConfig struct {
Foo *Test `envSuffix:"_FOO"`
Bar *Test `envSuffix:"_BAR"`
Clean *Test
}

cfg := ComplexConfig{
Foo: &Test{},
Bar: &Test{},
Clean: &Test{},
}
isNoErr(t, ParseWithOptions(&cfg, Options{Environment: map[string]string{"TEST_FOO": "kek", "TEST_BAR": "lel", "TEST": "clean"}}))
isEqual(t, "kek", cfg.Foo.Str)
isEqual(t, "lel", cfg.Bar.Str)
isEqual(t, "clean", cfg.Clean.Str)
}

func TestNestedSuffixPointer(t *testing.T) {
type ComplexConfig struct {
Foo struct {
Str string `env:"STR"`
} `envSuffix:"_FOO"`
}
cfg := ComplexConfig{}
isNoErr(t, ParseWithOptions(&cfg, Options{Environment: map[string]string{"STR_FOO": "str_foo"}}))
isEqual(t, "str_foo", cfg.Foo.Str)

type ComplexConfig2 struct {
Foo struct {
Bar struct {
Str string `env:"STR"`
} `envSuffix:"_BAR"`
Bar2 string `env:"BAR2"`
} `envSuffix:"_FOO"`
}
cfg2 := ComplexConfig2{}
isNoErr(t, ParseWithOptions(&cfg2, Options{Environment: map[string]string{"STR_BAR_FOO": "kek", "BAR2_FOO": "lel"}}))
isEqual(t, "lel", cfg2.Foo.Bar2)
isEqual(t, "kek", cfg2.Foo.Bar.Str)
}

func TestComplexSuffix(t *testing.T) {
type Config struct {
Home string `env:"HOME"`
}
type ComplexConfig struct {
Foo Config `envSuffix:"_FOO"`
Clean Config
Bar Config `envSuffix:"_BAR"`
Blah string `env:"BLAH"`
}
cfg := ComplexConfig{}
isNoErr(t, ParseWithOptions(&cfg, Options{
Suffix: "_T",
Environment: map[string]string{
"HOME_FOO_T": "/foo",
"HOME_BAR_T": "/bar",
"BLAH_T": "blahhh",
"HOME_T": "/clean",
},
}))
isEqual(t, "/foo", cfg.Foo.Home)
isEqual(t, "/bar", cfg.Bar.Home)
isEqual(t, "/clean", cfg.Clean.Home)
isEqual(t, "blahhh", cfg.Blah)
}

func TestGetFieldParamsWithSuffix(t *testing.T) {
var config FieldParamsConfig
params, err := GetFieldParamsWithOptions(&config, Options{Suffix: "_FOO"})
isNoErr(t, err)

expectedParams := []FieldParams{
{OwnKey: "SIMPLE", Key: "SIMPLE_FOO"},
{OwnKey: "WITH_DEFAULT", Key: "WITH_DEFAULT_FOO", DefaultValue: "default", HasDefaultValue: true},
{OwnKey: "REQUIRED", Key: "REQUIRED_FOO", Required: true},
{OwnKey: "FILE", Key: "FILE_FOO", LoadFile: true},
{OwnKey: "UNSET", Key: "UNSET_FOO", Unset: true},
{OwnKey: "NOT_EMPTY", Key: "NOT_EMPTY_FOO", NotEmpty: true},
{OwnKey: "EXPAND", Key: "EXPAND_FOO", Expand: true},
{OwnKey: "SIMPLE", Key: "NESTED_SIMPLE_FOO"},
}
isTrue(t, len(params) == len(expectedParams))
isTrue(t, areEqual(params, expectedParams))
}

func TestParseWithOptionsRenamedSuffix(t *testing.T) {
type Config struct {
Str string `env:"STR"`
}
type ComplexConfig struct {
Foo Config `envSuffix:"_FOO" mySuffix:"_BAR"`
}

t.Setenv("STR_FOO", "101")
t.Setenv("STR_BAR", "202")
t.Setenv("STR_BAR_APP", "303")

cfg := &ComplexConfig{}
isNoErr(t, ParseWithOptions(cfg, Options{SuffixTagName: "mySuffix"}))
isEqual(t, "202", cfg.Foo.Str)

isNoErr(t, ParseWithOptions(cfg, Options{SuffixTagName: "mySuffix", Suffix: "_APP"}))
isEqual(t, "303", cfg.Foo.Str)

isNoErr(t, Parse(cfg))
isEqual(t, "101", cfg.Foo.Str)
}

func TestPrefixAndSuffix(t *testing.T) {
type Config struct {
Home string `env:"HOME"`
}
cfg := Config{}
isNoErr(t, ParseWithOptions(&cfg, Options{
Prefix: "PRE_",
Suffix: "_SUF",
Environment: map[string]string{
"PRE_HOME_SUF": "/home",
},
}))
isEqual(t, "/home", cfg.Home)
}

func TestComplexPrefixAndSuffix(t *testing.T) {
type Config struct {
Home string `env:"HOME"`
}
type ComplexConfig struct {
Foo Config `envPrefix:"FOO_"`
Bar Config `envSuffix:"_BAR"`
FooBar Config `envPrefix:"FOO_" envSuffix:"_BAR"`
Clean Config
Blah string `env:"BLAH"`
}
cfg := ComplexConfig{}
isNoErr(t, ParseWithOptions(&cfg, Options{
Prefix: "T_",
Suffix: "_T",
Environment: map[string]string{
"T_FOO_HOME_T": "/foo",
"T_HOME_BAR_T": "/bar",
"T_FOO_HOME_BAR_T": "/foobar",
"T_HOME_T": "/clean",
"T_BLAH_T": "blahhh",
},
}))
isEqual(t, "/foo", cfg.Foo.Home)
isEqual(t, "/bar", cfg.Bar.Home)
isEqual(t, "/foobar", cfg.FooBar.Home)
isEqual(t, "/clean", cfg.Clean.Home)
isEqual(t, "blahhh", cfg.Blah)
}

func TestNestedPrefixAndSuffix(t *testing.T) {
type ComplexConfig struct {
Foo struct {
Bar struct {
Str string `env:"STR"`
} `envSuffix:"_BAR"`
Str2 string `env:"STR2"`
} `envPrefix:"FOO_"`
}
cfg := ComplexConfig{}
isNoErr(t, ParseWithOptions(&cfg, Options{
Prefix: "T_",
Suffix: "_T",
Environment: map[string]string{
"T_FOO_STR_BAR_T": "nested",
"T_FOO_STR2_T": "str2val",
},
}))
isEqual(t, "nested", cfg.Foo.Bar.Str)
isEqual(t, "str2val", cfg.Foo.Str2)
}

func TestGetFieldParamsWithPrefixAndSuffix(t *testing.T) {
var config FieldParamsConfig
params, err := GetFieldParamsWithOptions(&config, Options{Prefix: "FOO_", Suffix: "_BAR"})
isNoErr(t, err)

expectedParams := []FieldParams{
{OwnKey: "SIMPLE", Key: "FOO_SIMPLE_BAR"},
{OwnKey: "WITH_DEFAULT", Key: "FOO_WITH_DEFAULT_BAR", DefaultValue: "default", HasDefaultValue: true},
{OwnKey: "REQUIRED", Key: "FOO_REQUIRED_BAR", Required: true},
{OwnKey: "FILE", Key: "FOO_FILE_BAR", LoadFile: true},
{OwnKey: "UNSET", Key: "FOO_UNSET_BAR", Unset: true},
{OwnKey: "NOT_EMPTY", Key: "FOO_NOT_EMPTY_BAR", NotEmpty: true},
{OwnKey: "EXPAND", Key: "FOO_EXPAND_BAR", Expand: true},
{OwnKey: "SIMPLE", Key: "FOO_NESTED_SIMPLE_BAR"},
}
isTrue(t, len(params) == len(expectedParams))
isTrue(t, areEqual(params, expectedParams))
}

func TestNoEnvKey(t *testing.T) {
type Config struct {
Foo string
Expand Down
Loading