From 2fca466d36651308b5375288b12c88beab0943f3 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 18 Dec 2023 16:15:39 -0500 Subject: [PATCH] Make mutators a full-fledged interface --- README.md | 31 ++++++++++++++++++---- envconfig.go | 57 ++++++++--------------------------------- envconfig_test.go | 65 ++++++++++++++++++++++++++++++++--------------- mutator.go | 60 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 72 deletions(-) create mode 100644 mutator.go diff --git a/README.md b/README.md index 3d335c1..d4dbfdf 100644 --- a/README.md +++ b/README.md @@ -375,10 +375,31 @@ var config Config envconfig.ProcessWith(ctx, &config, envconfig.OsLookuper(), resolveSecretFunc) ``` -Mutator functions 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. The parameters (in order) are: +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. @@ -420,7 +441,7 @@ var config Config lookuper := envconfig.PrefixLookuper("REDIS_", envconfig.MapLookuper(map[string]string{ "PASSWORD": "original", })) -mutators := []envconfig.MutatorFunc{mutatorFunc1, mutatorFunc2, mutatorFunc3} +mutators := []envconfig.Mutators{mutatorFunc1, mutatorFunc2, mutatorFunc3} envconfig.ProcessWith(ctx, &config, lookuper, mutators...) func mutatorFunc1(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (string, bool, error) { diff --git a/envconfig.go b/envconfig.go index b62819b..dede29c 100644 --- a/envconfig.go +++ b/envconfig.go @@ -219,41 +219,6 @@ type Decoder interface { EnvDecode(val string) 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. -type MutatorFunc func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) - -// LegacyMutatorFunc is a helper that eases the transition from the previous -// MutatorFunc signature. It wraps the previous-style mutator function and -// returns a new one. Since the former mutator function had less data, this is -// inherently lossy. -// -// DEPRECATED: Change type signatures to [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) - return v, true, err - } -} - // options are internal options for decoding. type options struct { Default string @@ -267,14 +232,14 @@ type options struct { // Process processes the struct using the environment. See [ProcessWith] for a // more customizable version. -func Process(ctx context.Context, i any) error { - return ProcessWith(ctx, i, OsLookuper()) +func Process(ctx context.Context, i any, mus ...Mutator) error { + return ProcessWith(ctx, i, OsLookuper(), mus...) } // ProcessWith processes the given interface with the given lookuper. See the // package-level documentation for specific examples and behaviors. -func ProcessWith(ctx context.Context, i any, l Lookuper, fns ...MutatorFunc) error { - return processWith(ctx, i, l, false, fns...) +func ProcessWith(ctx context.Context, i any, l Lookuper, mus ...Mutator) error { + return processWith(ctx, i, l, false, mus...) } // ExtractDefaults is a helper that returns a fully-populated struct with the @@ -295,13 +260,13 @@ func ProcessWith(ctx context.Context, i any, l Lookuper, fns ...MutatorFunc) err // // This is effectively the same as calling [ProcessWith] with an empty // [MapLookuper]. -func ExtractDefaults(ctx context.Context, i any, fns ...MutatorFunc) error { - return processWith(ctx, i, MapLookuper(nil), false, fns...) +func ExtractDefaults(ctx context.Context, i any, mus ...Mutator) error { + return processWith(ctx, i, MapLookuper(nil), false, mus...) } // processWith is a helper that captures whether the parent wanted // initialization. -func processWith(ctx context.Context, i any, l Lookuper, parentNoInit bool, fns ...MutatorFunc) error { +func processWith(ctx context.Context, i any, l Lookuper, parentNoInit bool, mus ...Mutator) error { if l == nil { return ErrLookuperNil } @@ -413,7 +378,7 @@ func processWith(ctx context.Context, i any, l Lookuper, parentNoInit bool, fns plu = PrefixLookuper(opts.Prefix, l) } - if err := processWith(ctx, ef.Interface(), plu, shouldNotInit, fns...); err != nil { + if err := processWith(ctx, ef.Interface(), plu, shouldNotInit, mus...); err != nil { return fmt.Errorf("%s: %w", tf.Name, err) } @@ -462,12 +427,12 @@ func processWith(ctx context.Context, i any, l Lookuper, parentNoInit bool, fns originalValue := val stop := false - for _, fn := range fns { - if fn == nil { + for _, mu := range mus { + if mu == nil { continue } - val, stop, err = fn(ctx, originalKey, resolvedKey, originalValue, val) + val, stop, err = mu.EnvMutate(ctx, originalKey, resolvedKey, originalValue, val) if err != nil { return fmt.Errorf("%s: %w", tf.Name, err) } diff --git a/envconfig_test.go b/envconfig_test.go index b689f25..27131fa 100644 --- a/envconfig_test.go +++ b/envconfig_test.go @@ -162,6 +162,12 @@ var valueMutatorFunc MutatorFunc = func(ctx context.Context, oKey, rKey, oVal, r return fmt.Sprintf("MUTATED_%s", rVal), false, nil } +type CustomMutator struct{} + +func (m *CustomMutator) EnvMutate(ctx context.Context, oKey, rKey, oVal, rVal string) (string, bool, error) { + return fmt.Sprintf("CUSTOM_MUTATED_%s", rVal), false, nil +} + // Electron > Lepton > Quark type Electron struct { Name string `env:"ELECTRON_NAME"` @@ -235,7 +241,7 @@ func TestProcessWith(t *testing.T) { input any exp any lookuper Lookuper - mutators []MutatorFunc + mutators []Mutator err error errMsg string }{ @@ -1835,7 +1841,24 @@ func TestProcessWith(t *testing.T) { lookuper: MapLookuper(map[string]string{ "FIELD": "value", }), - mutators: []MutatorFunc{valueMutatorFunc}, + mutators: []Mutator{valueMutatorFunc}, + }, + { + name: "mutate/custom", + input: &struct { + Field string `env:"FIELD"` + }{}, + exp: &struct { + Field string `env:"FIELD"` + }{ + Field: "CUSTOM_MUTATED_value", + }, + lookuper: MapLookuper(map[string]string{ + "FIELD": "value", + }), + mutators: []Mutator{ + &CustomMutator{}, + }, }, { name: "mutate/stops", @@ -1850,13 +1873,13 @@ func TestProcessWith(t *testing.T) { lookuper: MapLookuper(map[string]string{ "FIELD": "", }), - mutators: []MutatorFunc{ - func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { + mutators: []Mutator{ + MutatorFunc(func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { return "value-1", true, nil - }, - func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { + }), + MutatorFunc(func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { return "value-2", true, nil - }, + }), }, }, { @@ -1872,10 +1895,10 @@ func TestProcessWith(t *testing.T) { lookuper: PrefixLookuper("KEY_", MapLookuper(map[string]string{ "KEY_FIELD": "", })), - mutators: []MutatorFunc{ - func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { + mutators: []Mutator{ + MutatorFunc(func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { return fmt.Sprintf("oKey:%s, rKey:%s", oKey, rKey), false, nil - }, + }), }, }, { @@ -1891,13 +1914,13 @@ func TestProcessWith(t *testing.T) { lookuper: PrefixLookuper("KEY_", MapLookuper(map[string]string{ "KEY_FIELD": "old-value", })), - mutators: []MutatorFunc{ - func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { + mutators: []Mutator{ + MutatorFunc(func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { return "new-value", false, nil - }, - func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { + }), + MutatorFunc(func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { return fmt.Sprintf("oVal:%s, cVal:%s", oVal, cVal), false, nil - }, + }), }, }, { @@ -1911,13 +1934,13 @@ func TestProcessWith(t *testing.T) { lookuper: MapLookuper(map[string]string{ "FIELD": "", }), - mutators: []MutatorFunc{ - func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { + mutators: []Mutator{ + MutatorFunc(func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { return "", false, fmt.Errorf("error 1") - }, - func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { + }), + MutatorFunc(func(_ context.Context, oKey, rKey, oVal, cVal string) (string, bool, error) { return "", false, fmt.Errorf("error 2") - }, + }), }, errMsg: "error 1", }, @@ -1976,7 +1999,7 @@ func TestProcessWith(t *testing.T) { "BREAD_NAME": "rye", "MEAT_TYPE": "pep", }), - mutators: []MutatorFunc{valueMutatorFunc}, + mutators: []Mutator{valueMutatorFunc}, }, // Overwriting diff --git a/mutator.go b/mutator.go new file mode 100644 index 0000000..c838387 --- /dev/null +++ b/mutator.go @@ -0,0 +1,60 @@ +// 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 + +import "context" + +type Mutator interface { + 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. +type MutatorFunc func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) + +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) +} + +// LegacyMutatorFunc is a helper that eases the transition from the previous +// MutatorFunc signature. It wraps the previous-style mutator function and +// returns a new one. Since the former mutator function had less data, this is +// inherently lossy. +// +// DEPRECATED: Change type signatures to [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) + return v, true, err + } +}