Skip to content

Commit

Permalink
Make mutators a full-fledged interface (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
sethvargo committed Dec 18, 2023
1 parent b632c02 commit 053a0d2
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 72 deletions.
31 changes: 26 additions & 5 deletions README.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
57 changes: 11 additions & 46 deletions envconfig.go
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
Expand Down
65 changes: 44 additions & 21 deletions envconfig_test.go
Expand Up @@ -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"`
Expand Down Expand Up @@ -235,7 +241,7 @@ func TestProcessWith(t *testing.T) {
input any
exp any
lookuper Lookuper
mutators []MutatorFunc
mutators []Mutator
err error
errMsg string
}{
Expand Down Expand Up @@ -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",
Expand All @@ -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
},
}),
},
},
{
Expand All @@ -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
},
}),
},
},
{
Expand All @@ -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
},
}),
},
},
{
Expand All @@ -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",
},
Expand Down Expand Up @@ -1976,7 +1999,7 @@ func TestProcessWith(t *testing.T) {
"BREAD_NAME": "rye",
"MEAT_TYPE": "pep",
}),
mutators: []MutatorFunc{valueMutatorFunc},
mutators: []Mutator{valueMutatorFunc},
},

// Overwriting
Expand Down
60 changes: 60 additions & 0 deletions 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
}
}

0 comments on commit 053a0d2

Please sign in to comment.