diff --git a/README.md b/README.md index f7b1352..a89d88f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Define a struct with fields using the `env` tag: ```go type MyConfig struct { - Port int `env:"PORT"` + Port string `env:"PORT"` Username string `env:"USERNAME"` } ``` @@ -59,113 +59,141 @@ type MyConfig struct { } type DatabaseConfig struct { - Port int `env:"PORT"` + Port string `env:"PORT"` Username string `env:"USERNAME"` } ``` ## Configuration -Use the `env` struct tag to define configuration. +Use the `env` struct tag to define configuration. See the [godoc][] for usage +examples. -### Required +- `required` - marks a field as required. If a field is required, decoding + will error if the environment variable is unset. -If a field is required, processing will error if the environment variable is -unset. + ```go + type MyStruct struct { + Port string `env:"PORT, required"` + } + ``` -```go -type MyStruct struct { - Port int `env:"PORT,required"` -} -``` +- `default` - sets the default value for the environment variable is not set. + The environment variable must not be set (e.g. `unset PORT`). If the + environment variable is the empty string, envconfig considers that a "value" + and the default will **not** be used. -It is invalid to have a field as both `required` and `default`. + You can also set the default value to the value from another field or a + value from a different environment variable. -### Default + ```go + type MyStruct struct { + Port string `env:"PORT, default=5555"` + User string `env:"USER, default=$CURRENT_USER"` + } + ``` -If an environment variable is not set, the field will be set to the default -value. Note that the environment variable must not be set (e.g. `unset PORT`). -If the environment variable is the empty string, that counts as a "value" and -the default will not be used. +- `prefix` - sets the prefix to use for looking up environment variable keys + on child structs and fields. This is useful for shared configurations: -```go -type MyStruct struct { - Port int `env:"PORT,default=5555"` -} -``` + ```go + type RedisConfig struct { + Host string `env:"REDIS_HOST"` + User string `env:"REDIS_USER"` + } -You can also set the default value to another field or value from the -environment, for example: + type ServerConfig struct { + // CacheConfig will process values from $CACHE_REDIS_HOST and + // $CACHE_REDIS respectively. + CacheConfig *RedisConfig `env:", prefix=CACHE_"` -```go -type MyStruct struct { - DefaultPort int `env:"DEFAULT_PORT,default=5555"` - Port int `env:"OVERRIDE_PORT,default=$DEFAULT_PORT"` -} -``` + // RateLimitConfig will process values from $RATE_LIMIT_REDIS_HOST and + // $RATE_LIMIT_REDIS respectively. + RateLimitConfig *RedisConfig `env:", prefix=RATE_LIMIT_"` + } + ``` -The value for `Port` will default to the value of `DEFAULT_PORT`. +- `overwrite` - force overwriting existing non-zero struct values if the + environment variable was provided. -It is invalid to have a field as both `required` and `default`. + ```go + type MyStruct struct { + Port string `env:"PORT,overwrite"` + } + ``` -### Prefix + The rules for overwrite + default are: -For shared, embedded structs, you can define a prefix to use when processing -struct values for that embed. + - If the struct field has the zero value and a default is set: -```go -type SharedConfig struct { - Port int `env:"PORT,default=5555"` -} + - If no environment variable is specified, the struct field will be + populated with the default value. -type Server1 struct { - // This processes Port from $FOO_PORT. - *SharedConfig `env:",prefix=FOO_"` -} + - If an environment variable is specified, the struct field will be + populate with the environment variable value. -type Server2 struct { - // This processes Port from $BAR_PORT. - *SharedConfig `env:",prefix=BAR_"` -} -``` + - If the struct field has a non-zero value and a default is set: -It is invalid to specify a prefix on non-struct fields. + - If no environment variable is specified, the struct field's existing + value will be used (the default is ignored). -### Overwrite + - If an environment variable is specified, the struct field's existing + value will be overwritten with the environment variable value. -If overwrite is set, the value will be overwritten if there is an environment -variable match regardless if the value is non-zero. +- `delimiter` - choose a custom character to denote individual slice and map + entries. The default value is the comma (`,`). -```go -type MyStruct struct { - Port int `env:"PORT,overwrite"` -} -``` + ```go + type MyStruct struct { + MyVar []string `env:"MYVAR, delimiter=;"` + ``` -The rules for overwrite + default are: + ```bash + export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"} + ``` -- If the struct field has the zero value and a default is set: +- `separator` - choose a custom character to denote the separation between + keys and values in map entries. The default value is the colon (`:`) Define + a separator with `separator`: - - If no environment variable is specified, the struct field will be - populated with the default value. + ```go + type MyStruct struct { + MyVar map[string]string `env:"MYVAR, separator=|"` + } + ``` - - If an environment variable is specified, the struct field will be - populate with the environment variable value. + ```bash + export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"} + ``` -- If the struct field has a non-zero value and a default is set: +- `noinit` - do not initialize struct fields unless environment variables were + provided. The default behavior is to deeply initialize all fields to their + default (zero) value. - - If no environment variable is specified, the struct field's existing - value will be used (the default is ignored). + ```go + type MyStruct struct { + MyVar *url.URL `env:"MYVAR, noinit"` + } + ``` - - If an environment variable is specified, the struct field's existing - value will be overwritten with the environment variable value. +- `decodeunset` - force envconfig to run decoders even on unset environment + variable values. The default behavior is to skip running decoders on unset + environment variable values. + ```go + type MyStruct struct { + MyVar *url.URL `env:"MYVAR, decodeunset"` + } + ``` -## Complex Types -**Note:** Complex types are only decoded or unmarshalled when the environment -variable is defined or a default is specified. The decoding/unmarshalling -functions are _not_ invoked when a value is not defined. +## Decoding + +> [!NOTE] +> +> Complex types are only decoded or unmarshalled when the environment variable +> is defined or a default value is specified. + ### Durations @@ -182,14 +210,18 @@ type MyStruct struct { export MYVAR="10m" # 10 * time.Minute ``` + ### TextUnmarshaler / BinaryUnmarshaler -Types that implement `TextUnmarshaler` or `BinaryUnmarshaler` are processed as such. +Types that implement `TextUnmarshaler` or `BinaryUnmarshaler` are processed as +such. + ### json.Unmarshaler Types that implement `json.Unmarshaler` are processed as such. + ### gob.Decoder Types that implement `gob.Decoder` are processed as such. @@ -197,7 +229,7 @@ Types that implement `gob.Decoder` are processed as such. ### Slices -Slices are specified as comma-separated values: +Slices are specified as comma-separated values. ```go type MyStruct struct { @@ -209,20 +241,10 @@ type MyStruct struct { export MYVAR="a,b,c,d" # []string{"a", "b", "c", "d"} ``` -Define a custom delimiter with `delimiter`: - -```go -type MyStruct struct { - MyVar []string `env:"MYVAR,delimiter=;"` -``` - -```bash -export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"} -``` - Note that byte slices are special cased and interpreted as strings from the environment. + ### Maps Maps are specified as comma-separated key:value pairs: @@ -237,29 +259,6 @@ type MyStruct struct { export MYVAR="a:b,c:d" # map[string]string{"a":"b", "c":"d"} ``` -Define a custom delimiter with `delimiter`: - -```go -type MyStruct struct { - MyVar map[string]string `env:"MYVAR,delimiter=;"` -``` - -```bash -export MYVAR="a:b;c:d" # map[string]string{"a":"b", "c":"d"} -``` - -Define a separator with `separator`: - -```go -type MyStruct struct { - MyVar map[string]string `env:"MYVAR,separator=|"` -} -``` - -```bash -export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"} -``` - ### Structs @@ -271,211 +270,10 @@ the non-nil value. To change this behavior, see [Initialization](#Initialization). -### Custom - -You can also [define your own decoder](#Extension). - - -## Prefixing - -You can define a custom prefix using the `PrefixLookuper`. This will lookup -values in the environment by prefixing the keys with the provided value: - -```go -type MyStruct struct { - MyVar string `env:"MYVAR"` -} -``` - -```go -// Process variables, but look for the "APP_" prefix. -if err := envconfig.ProcessWith(ctx, &c, &envconfig.Config{ - Lookuper: envconfig.PrefixLookuper("APP_", envconfig.OsLookuper()), -}); err != nil { - panic(err) -} -``` - -```bash -export APP_MYVAR="foo" -``` - -## Initialization - -By default, all pointers, slices, and maps are initialized (allocated) so they -are not `nil`. To disable this behavior, use the tag the field as `noinit`: - -```go -type MyStruct struct { - // Without `noinit`, DeleteUser would be initialized to the default boolean - // value. With `noinit`, if the environment variable is not given, the value - // is kept as uninitialized (nil). - DeleteUser *bool `env:"DELETE_USER, noinit"` -} -``` - -This also applies to nested fields in a struct: - -```go -type ParentConfig struct { - // Without `noinit` tag, `Child` would be set to `&ChildConfig{}` whether - // or not `FIELD` is set in the env var. - // With `noinit`, `Child` would stay nil if `FIELD` is not set in the env var. - Child *ChildConfig `env:",noinit"` -} - -type ChildConfig struct { - Field string `env:"FIELD"` -} -``` - -The `noinit` tag is only applicable for pointer, slice, and map fields. Putting -the tag on a different type will return an error. - - -## Extension - -### Decoders - -All built-in types are supported except `Func` and `Chan`. If you need to define -a custom decoder, implement the `Decoder` interface: - -```go -type MyStruct struct { - field string -} - -func (v *MyStruct) EnvDecode(val string) error { - v.field = fmt.Sprintf("PREFIX-%s", val) - return nil -} -``` - -### Mutators - -If you need to modify environment variable values before processing, you can -specify a custom `Mutator`: - -```go -type Config struct { - Password `env:"PASSWORD"` -} - -func resolveSecretFunc(ctx context.Context, originalKey, resolvedKey, originalValue, resolvedValue string) (newValue string, stop bool, err error) { - if strings.HasPrefix(value, "secret://") { - v, err := secretmanager.Resolve(ctx, value) // example - if err != nil { - return resolvedValue, true, fmt.Errorf("failed to access secret: %w", err) - } - return v, false, nil - } - return resolvedValue, false, nil -} - -var config Config -envconfig.ProcessWith(ctx, &config, &envconfig.Config{ - Lookuper: envconfig.OsLookuper(), - Mutators: []envconfig.Mutator{resolveSecretFunc}, -}) -``` - -Mutators are like middleware, and they have access to the initial and current -state of the stack. Mutators only run when a value has been provided in the -environment. They execute _before_ any complex type processing, so all inputs -and outputs are strings. To create a mutator, implement `EnvMutate` or use -`MutatorFunc`: - -```go -type MyMutator struct {} - -func (m *MyMutator) EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { - // ... -} - -// -// OR -// - -envconfig.MutatorFunc(func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { - // ... -}) -``` - -The parameters (in order) are: - -- `context` is the context provided to `Process`. - -- `originalKey` is the unmodified environment variable name as it was defined - on the struct. - -- `resolvedKey` is the fully-resolved environment variable name, which may - include prefixes or modifications from processing. When there are no - modifications, this will be equivalent to `originalKey`. - -- `originalValue` is the unmodified environment variable's value before any - mutations were run. - -- `currentValue` is the currently-resolved value, which may have been modified - by previous mutators and may be modified by subsequent mutators in the - stack. - -The function returns (in order): - -- The new value to use in both future mutations and final processing. - -- A boolean which indicates whether future mutations in the stack should be - applied. - -- Any errors that occurred. - -> [!TIP] -> -> Users coming from the v0 series can wrap their mutator functions with -> `LegacyMutatorFunc` for an easier transition to this new syntax. - -Consider the following example to illustrate the difference between -`originalKey` and `resolvedKey`: - -```go -type Config struct { - Password `env:"PASSWORD"` -} - -var config Config -mutators := []envconfig.Mutators{mutatorFunc1, mutatorFunc2, mutatorFunc3} -envconfig.ProcessWith(ctx, &config, &envconfig.Config{ - Lookuper: envconfig.PrefixLookuper("REDIS_", envconfig.MapLookuper(map[string]string{ - "PASSWORD": "original", - })), - Mutators: mutators, -}) - -func mutatorFunc1(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (string, bool, error) { - // originalKey is "PASSWORD" - // resolvedKey is "REDIS_PASSWORD" - // originalValue is "original" - // currentValue is "original" - return currentValue+"-modified", false, nil -} - -func mutatorFunc2(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (string, bool, error) { - // originalKey is "PASSWORD" - // resolvedKey is "REDIS_PASSWORD" - // originalValue is "original" - // currentValue is "original-modified" - return currentValue, true, nil -} - -func mutatorFunc3(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (string, bool, error) { - // This mutator will never run because mutatorFunc2 stopped the chain. - return "...", false, nil -} -``` - - -## Advanced Processing +### Custom Decoders -See the [godoc][] for examples. +You can also define your own decoders. See the [godoc][godoc] for more +information. ## Testing diff --git a/envconfig.go b/envconfig.go index fce6968..6c85866 100644 --- a/envconfig.go +++ b/envconfig.go @@ -46,22 +46,7 @@ // // export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"} // -// If you need to modify environment variable values before processing, you can -// specify a custom mutator: -// -// type Config struct { -// Password `env:"PASSWORD_SECRET"` -// } -// -// func resolveSecretFunc(ctx context.Context, key, value string) (string, error) { -// if strings.HasPrefix(value, "secret://") { -// return secretmanager.Resolve(ctx, value) // example -// } -// return value, nil -// } -// -// var config Config -// ProcessWith(&config, OsLookuper(), resolveSecretFunc) +// For more configuration options and examples, see the documentation. package envconfig import ( @@ -92,27 +77,27 @@ const ( optSeparator = "separator=" ) -// Error is a custom error type for errors returned by envconfig. -type Error string +// internalError is a custom error type for errors returned by envconfig. +type internalError string // Error implements error. -func (e Error) Error() string { +func (e internalError) Error() string { return string(e) } const ( - ErrInvalidEnvvarName = Error("invalid environment variable name") - ErrInvalidMapItem = Error("invalid map item") - ErrLookuperNil = Error("lookuper cannot be nil") - ErrMissingKey = Error("missing key") - ErrMissingRequired = Error("missing required value") - ErrNoInitNotPtr = Error("field must be a pointer to have noinit") - ErrNotPtr = Error("input must be a pointer") - ErrNotStruct = Error("input must be a struct") - ErrPrefixNotStruct = Error("prefix is only valid on struct types") - ErrPrivateField = Error("cannot parse private fields") - ErrRequiredAndDefault = Error("field cannot be required and have a default value") - ErrUnknownOption = Error("unknown option") + ErrInvalidEnvvarName = internalError("invalid environment variable name") + ErrInvalidMapItem = internalError("invalid map item") + ErrLookuperNil = internalError("lookuper cannot be nil") + ErrMissingKey = internalError("missing key") + ErrMissingRequired = internalError("missing required value") + ErrNoInitNotPtr = internalError("field must be a pointer to have noinit") + ErrNotPtr = internalError("input must be a pointer") + ErrNotStruct = internalError("input must be a struct") + ErrPrefixNotStruct = internalError("prefix is only valid on struct types") + ErrPrivateField = internalError("cannot parse private fields") + ErrRequiredAndDefault = internalError("field cannot be required and have a default value") + ErrUnknownOption = internalError("unknown option") ) // Lookuper is an interface that provides a lookup for a string-based key. @@ -195,14 +180,14 @@ func (p *prefixLookuper) Key(key string) string { func (p *prefixLookuper) Unwrap() Lookuper { l := p.l - for v, ok := l.(UnwrappableLookuper); ok; { + for v, ok := l.(unwrappableLookuper); ok; { l = v.Unwrap() } return l } -// UnwrappableLookuper is a lookuper that can return the underlying lookuper. -type UnwrappableLookuper interface { +// unwrappableLookuper is a lookuper that can return the underlying lookuper. +type unwrappableLookuper interface { Unwrap() Lookuper } @@ -212,9 +197,9 @@ func MultiLookuper(lookupers ...Lookuper) Lookuper { return &multiLookuper{ls: lookupers} } -// KeyedLookuper is an extension to the [Lookuper] interface that returns the +// keyedLookuper is an extension to the [Lookuper] interface that returns the // underlying key (used by the [PrefixLookuper] or custom implementations). -type KeyedLookuper interface { +type keyedLookuper interface { Key(key string) string } @@ -244,7 +229,8 @@ type options struct { // Config represent inputs to the envconfig decoding. type Config struct { - // Target is the destination structure to decode. This value is required. + // Target is the destination structure to decode. This value is required, and + // it must be a pointer to a struct. Target any // Lookuper is the lookuper implementation to use. If not provided, it @@ -282,8 +268,8 @@ type Config struct { Mutators []Mutator } -// Process processes the struct using the environment. See [ProcessWith] for a -// more customizable version. +// Process decodes the struct using values from environment variables. See +// [ProcessWith] for a more customizable version. func Process(ctx context.Context, i any, mus ...Mutator) error { return ProcessWith(ctx, &Config{ Target: i, @@ -291,14 +277,17 @@ func Process(ctx context.Context, i any, mus ...Mutator) error { }) } -// ProcessWith processes the given interface with the given lookuper. See the -// package-level documentation for specific examples and behaviors. +// ProcessWith executes the decoding process using the provided [Config]. func ProcessWith(ctx context.Context, c *Config) error { if c == nil { c = new(Config) } - // Deep copy the slice and remove any nil functions. + if c.Lookuper == nil { + c.Lookuper = OsLookuper() + } + + // Deep copy the slice and remove any nil mutators. var mus []Mutator for _, m := range c.Mutators { if m != nil { @@ -310,8 +299,7 @@ func ProcessWith(ctx context.Context, c *Config) error { return processWith(ctx, c) } -// processWith is a helper that captures whether the parent wanted -// initialization. +// processWith is a helper that retains configuration from the parent structs. func processWith(ctx context.Context, c *Config) error { i := c.Target @@ -513,7 +501,7 @@ func processWith(ctx context.Context, c *Config) error { if found || usedDefault { originalKey := key resolvedKey := originalKey - if keyer, ok := l.(KeyedLookuper); ok { + if keyer, ok := l.(keyedLookuper); ok { resolvedKey = keyer.Key(resolvedKey) } originalValue := val @@ -620,7 +608,7 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string, val, found := l.Lookup(key) if !found { if required { - if keyer, ok := l.(KeyedLookuper); ok { + if keyer, ok := l.(keyedLookuper); ok { key = keyer.Key(key) } @@ -632,7 +620,7 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string, // a different environment variable. val = os.Expand(defaultValue, func(i string) string { lookuper := l - if v, ok := lookuper.(UnwrappableLookuper); ok { + if v, ok := lookuper.(unwrappableLookuper); ok { lookuper = v.Unwrap() } diff --git a/envconfig_decoder_doc_test.go b/envconfig_decoder_doc_test.go new file mode 100644 index 0000000..c928eb5 --- /dev/null +++ b/envconfig_decoder_doc_test.go @@ -0,0 +1,63 @@ +// Copyright The envconfig Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package envconfig_test + +import ( + "encoding/json" + "fmt" + + "github.com/sethvargo/go-envconfig" +) + +type CustomStruct struct { + Port string `json:"port"` + User string `json:"user"` + Max int `json:"max"` +} + +func (s *CustomStruct) EnvDecode(val string) error { + return json.Unmarshal([]byte(val), s) +} + +func Example_decoder() { + // This example demonstrates defining a custom decoder function. + + type MyStruct struct { + Config CustomStruct `env:"CONFIG"` + } + + var s MyStruct + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &s, + Lookuper: envconfig.MapLookuper(map[string]string{ + "CONFIG": `{ + "port": "8080", + "user": "yoyo", + "max": 51 + }`, + }), + }); err != nil { + panic(err) // TODO: handle error + } + + fmt.Printf("port: %v\n", s.Config.Port) + fmt.Printf("user: %v\n", s.Config.User) + fmt.Printf("max: %v\n", s.Config.Max) + + // Output: + // port: 8080 + // user: yoyo + // max: 51 +} diff --git a/envconfig_doc_test.go b/envconfig_doc_test.go index e897c5d..c1708da 100644 --- a/envconfig_doc_test.go +++ b/envconfig_doc_test.go @@ -17,13 +17,290 @@ package envconfig_test import ( "context" "fmt" + "net/url" + "strconv" + "strings" "github.com/sethvargo/go-envconfig" ) var ctx = context.Background() -func Example_inherited() { +var secretmanager = &testSecretManager{} + +type testSecretManager struct{} + +func (s *testSecretManager) Resolve(ctx context.Context, value string) (string, error) { + return "", nil +} + +func Example_basic() { + // This example demonstrates the basic usage for envconfig. + + type MyStruct struct { + Port int `env:"PORT"` + Username string `env:"USERNAME"` + } + + // Set some environment variables in the process: + // + // export PORT=5555 + // export USERNAME=yoyo + // + + var s MyStruct + if err := envconfig.Process(ctx, &s); err != nil { + panic(err) // TODO: handle error + } + + // c.Port = 5555 + // c.Username = "yoyo" +} + +func Example_mapLookuper() { + // This example demonstrates using a [MapLookuper] to source environment + // variables from a map instead of the environment. The map will always be of + // type string=string, because environment variables are always string types. + + type MyStruct struct { + Port int `env:"PORT"` + Username string `env:"USERNAME"` + } + + var s MyStruct + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &s, + Lookuper: envconfig.MapLookuper(map[string]string{ + "PORT": "5555", + "USERNAME": "yoyo", + }), + }); err != nil { + panic(err) // TODO: handle error + } + + fmt.Printf("port: %d\n", s.Port) + fmt.Printf("username: %q\n", s.Username) + + // Output: + // port: 5555 + // username: "yoyo" +} + +func Example_required() { + // This example demonstrates how to set fields as required. Required fields + // will error if unset. + + type MyStruct struct { + Port int `env:"PORT, required"` + } + + var s MyStruct + if err := envconfig.Process(ctx, &s); err != nil { + fmt.Printf("error: %s\n", err) + } + + // Output: + // error: Port: missing required value: PORT +} + +func Example_defaults() { + // This example demonstrates how to set default values for fields. Fields will + // use their default value if no value is provided for that key in the + // environment. + + type MyStruct struct { + Port int `env:"PORT, default=8080"` + Username string `env:"USERNAME, default=$OTHER_ENV"` + } + + var s MyStruct + if err := envconfig.Process(ctx, &s); err != nil { + panic(err) // TODO: handle error + } + + fmt.Printf("port: %d\n", s.Port) + + // Output: + // port: 8080 +} + +func Example_prefix() { + // This example demonstrates using prefixes to share structures. + + type RedisConfig struct { + Host string `env:"REDIS_HOST"` + User string `env:"REDIS_USER"` + } + + type ServerConfig struct { + // CacheConfig will process values from $CACHE_REDIS_HOST and + // $CACHE_REDIS respectively. + CacheConfig *RedisConfig `env:", prefix=CACHE_"` + + // RateLimitConfig will process values from $RATE_LIMIT_REDIS_HOST and + // $RATE_LIMIT_REDIS respectively. + RateLimitConfig *RedisConfig `env:", prefix=RATE_LIMIT_"` + } + + var s ServerConfig + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &s, + Lookuper: envconfig.MapLookuper(map[string]string{ + "CACHE_REDIS_HOST": "https://cache.host.internal", + "CACHE_REDIS_USER": "cacher", + "RATE_LIMIT_REDIS_HOST": "https://limiter.host.internal", + "RATE_LIMIT_REDIS_USER": "limiter", + }), + }); err != nil { + panic(err) // TODO: handle error + } + + fmt.Printf("cache redis host: %s\n", s.CacheConfig.Host) + fmt.Printf("cache redis user: %s\n", s.CacheConfig.User) + fmt.Printf("rate limit redis host: %s\n", s.RateLimitConfig.Host) + fmt.Printf("rate limit redis user: %s\n", s.RateLimitConfig.User) + + // Output: + // cache redis host: https://cache.host.internal + // cache redis user: cacher + // rate limit redis host: https://limiter.host.internal + // rate limit redis user: limiter +} + +func Example_overwrite() { + // This example demonstrates how to tell envconfig to overwrite existing + // struct values. + + type MyStruct struct { + Port int `env:"PORT, overwrite"` + } + + s := &MyStruct{ + Port: 1234, + } + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: s, + Lookuper: envconfig.MapLookuper(map[string]string{ + "PORT": "8080", + }), + }); err != nil { + panic(err) // TODO: handle error + } + + fmt.Printf("port: %d\n", s.Port) + + // Output: + // port: 8080 +} + +func Example_prefixLookuper() { + // This example demonstrates using a [PrefixLookuper] to programatically alter + // environment variable keys to include the given prefix. + + type MyStruct struct { + Port int `env:"PORT"` + } + + var s MyStruct + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &s, + Lookuper: envconfig.PrefixLookuper("APP_", envconfig.MapLookuper(map[string]string{ + "APP_PORT": "1234", + })), + }); err != nil { + panic(err) + } + + fmt.Printf("port: %d\n", s.Port) + + // Output: + // port: 1234 +} + +func Example_noinit() { + // This example demonstrates setting the "noinit" tag to bypass + // initialization. + + type MyStruct struct { + SecureA *bool `env:"SECURE_A"` + SecureB *bool `env:"SECURE_B, noinit"` + } + + var s MyStruct + if err := envconfig.Process(ctx, &s); err != nil { + panic(err) + } + + printVal := func(v *bool) string { + if v != nil { + return strconv.FormatBool(*v) + } + return "" + } + + fmt.Printf("secureA: %s\n", printVal(s.SecureA)) + fmt.Printf("secureB: %s\n", printVal(s.SecureB)) + + // Output: + // secureA: false + // secureB: +} + +func Example_decodeunset() { + // This example demonstrates forcing envconfig to run decoders, even on unset + // environment variables. + + type MyStruct struct { + UrlA *url.URL `env:"URL_A"` + UrlB *url.URL `env:"URL_B, decodeunset"` + } + + var s MyStruct + if err := envconfig.Process(ctx, &s); err != nil { + panic(err) + } + + fmt.Printf("urlA: %s\n", s.UrlA) + fmt.Printf("urlB: %s\n", s.UrlB) + + // Output: + // urlA: //@ + // urlB: +} + +func Example_mutatorFunc() { + // This example demonstrates authoring mutator functions to modify environment + // variable values before processing. + + type MyStruct struct { + Password string `env:"PASSWORD"` + } + + resolveSecretFunc := envconfig.MutatorFunc(func(ctx context.Context, originalKey, resolvedKey, originalValue, resolvedValue string) (newValue string, stop bool, err error) { + if strings.HasPrefix(resolvedValue, "secret://") { + v, err := secretmanager.Resolve(ctx, resolvedValue) + if err != nil { + return resolvedValue, true, fmt.Errorf("failed to access secret: %w", err) + } + return v, false, nil + } + return resolvedValue, false, nil + }) + + var s MyStruct + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &s, + Lookuper: envconfig.OsLookuper(), + Mutators: []envconfig.Mutator{resolveSecretFunc}, + }); err != nil { + panic(err) // TODO: handle error + } +} + +func Example_inheritedConfiguration() { + // This example demonstrates how struct-level configuration options are + // propagated to child fields and structs. + type Credentials struct { Username string `env:"USERNAME"` Password string `env:"PASSWORD"` @@ -77,7 +354,10 @@ func Example_inherited() { // margins: map[bottom:1.5 top:0.5] } -func Example_globalConfigurations() { +func Example_customConfiguration() { + // This example demonstrates how to set global configuration options that + // apply to all decoding (unless overridden at the field level). + type HTTPConfig struct { AllowedHeaders map[string]string `env:"ALLOWED_HEADERS"` RejectedHeaders map[string]string `env:"REJECTED_HEADERS, delimiter=|"` diff --git a/envconfig_mutator_doc_test.go b/envconfig_mutator_doc_test.go new file mode 100644 index 0000000..8e3706c --- /dev/null +++ b/envconfig_mutator_doc_test.go @@ -0,0 +1,69 @@ +// Copyright The envconfig Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package envconfig_test + +import ( + "context" + "fmt" + + "github.com/sethvargo/go-envconfig" +) + +// MyMutator is a mutator that keeps a count of all mutated values, appending +// the current count to each environment variable that is processed. +type MyMutator struct { + counter int +} + +func (m *MyMutator) EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { + m.counter++ + return fmt.Sprintf("%s-%d", currentValue, m.counter), false, nil +} + +func Example_mutator() { + // This exmaple demonstrates authoring a complex mutator that modifies + // environment variable values before processing. + + type MyStruct struct { + FieldA string `env:"FIELD_A"` + FieldB string `env:"FIELD_B"` + FieldC string `env:"FIELD_C"` + FieldD string `env:"FIELD_D"` + } + + var s MyStruct + if err := envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &s, + Lookuper: envconfig.MapLookuper(map[string]string{ + "FIELD_A": "a", + "FIELD_B": "b", + "FIELD_C": "c", + }), + Mutators: []envconfig.Mutator{&MyMutator{}}, + }); err != nil { + panic(err) // TODO: handle error + } + + fmt.Printf("field a: %q\n", s.FieldA) + fmt.Printf("field b: %q\n", s.FieldB) + fmt.Printf("field c: %q\n", s.FieldC) + fmt.Printf("field d: %q\n", s.FieldD) + + // Output: + // field a: "a-1" + // field b: "b-2" + // field c: "c-3" + // field d: "" +} diff --git a/mutator.go b/mutator.go index c838387..ccfadba 100644 --- a/mutator.go +++ b/mutator.go @@ -16,32 +16,48 @@ package envconfig import "context" +// Mutator is the interface for a mutator function. Mutators act like middleware +// and alter values for subsequent processing. This is useful if you want to +// mutate the environment variable value before it's converted to the proper +// type. +// +// Mutators are only called on defined values (or when decodeunset is true). type Mutator interface { + // EnvMutate is called to alter the environment variable value. + // + // - `originalKey` is the unmodified environment variable name as it was defined + // on the struct. + // + // - `resolvedKey` is the fully-resolved environment variable name, which may + // include prefixes or modifications from processing. When there are + // no modifications, this will be equivalent to `originalKey`. + // + // - `originalValue` is the unmodified environment variable's value before any + // mutations were run. + // + // - `currentValue` is the currently-resolved value, which may have been + // modified by previous mutators and may be modified in the future by + // subsequent mutators in the stack. + // + // The function returns (in order): + // + // - The new value to use in both future mutations and final processing. + // + // - A boolean which indicates whether future mutations in the stack should be + // applied. + // + // - Any errors that occurred. + // EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) } -// MutatorFunc is a function that mutates a given value before it is passed -// along for processing. This is useful if you want to mutate the environment -// variable value before it's converted to the proper type. -// -// - `originalKey` is the unmodified environment variable name as it was defined -// on the struct. -// -// - `resolvedKey` is the fully-resolved environment variable name, which may -// include prefixes or modifications from processing. When there are -// no modifications, this will be equivalent to `originalKey`. -// -// - `originalValue` is the unmodified environment variable's value before any -// mutations were run. -// -// - `currentValue` is the currently-resolved value, which may have been -// modified by previous mutators and may be modified in the future by -// subsequent mutators in the stack. -// -// It returns the new value, a boolean which indicates whether future mutations -// in the stack should be applied, and any errors that occurred. +var _ Mutator = (MutatorFunc)(nil) + +// MutatorFunc implements the [Mutator] and provides a quick way to create an +// anonymous function. type MutatorFunc func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) +// EnvMutate implements [Mutator]. func (m MutatorFunc) EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { return m(ctx, originalKey, resolvedKey, originalValue, currentValue) } @@ -51,7 +67,7 @@ func (m MutatorFunc) EnvMutate(ctx context.Context, originalKey, resolvedKey, or // returns a new one. Since the former mutator function had less data, this is // inherently lossy. // -// DEPRECATED: Change type signatures to [MutatorFunc] instead. +// Deprecated: Use [MutatorFunc] instead. func LegacyMutatorFunc(fn func(ctx context.Context, key, value string) (string, error)) MutatorFunc { return func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { v, err := fn(ctx, originalKey, currentValue)