Skip to content

Commit

Permalink
feat: onset hook (caarlos0#185)
Browse files Browse the repository at this point in the history
* feat: onset hook

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* fix: lint

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>
  • Loading branch information
nexoscp committed Aug 30, 2021
1 parent 15cccf7 commit 222531b
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 8 deletions.
44 changes: 42 additions & 2 deletions README.md
Expand Up @@ -296,11 +296,51 @@ func main() {
}
```


### On set hooks

You might want to listen to value sets and, for example, log something or do some other kind of logic.
You can do this by passing a `OnSet` option:

```go
package main

import (
"fmt"
"log"

"github.com/caarlos0/env/v6"
)

type Config struct {
Username string `env:"USERNAME" envDefault:"admin"`
Password string `env:"PASSWORD"`
}

func main() {
cfg := &Config{}
opts := &env.Options{
OnSet: func(tag string, value interface{}, isDefault bool) {
fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault)
},
}

// Load env vars.
if err := env.Parse(cfg, opts); err != nil {
log.Fatal(err)
}

// Print the loaded data.
fmt.Printf("%+v\n", cfg.envData)
}
```

## Making all fields to required

You can make all fields that don't have a default value be required by setting the `RequiredIfNoDef: true` in the `Options`.

For example

```go
package main

Expand All @@ -312,8 +352,8 @@ import (
)

type Config struct {
Username string `env:"USERNAME" envDefault:"admin"` // not required
Password string `env:"PASSWORD"` // required
Username string `env:"USERNAME" envDefault:"admin"`
Password string `env:"PASSWORD"`
}

func main() {
Expand Down
30 changes: 24 additions & 6 deletions env.go
Expand Up @@ -95,6 +95,8 @@ var (
// ParserFunc defines the signature of a function that can be used within `CustomParsers`.
type ParserFunc func(v string) (interface{}, error)

type OnSetFn func(tag string, value interface{}, isDefault bool)

// Options for the parser.
type Options struct {
// Environment keys and values that will be accessible for the service.
Expand All @@ -104,6 +106,8 @@ type Options struct {
// RequiredIfNoDef automatically sets all env as required if they do not declare 'envDefault'
RequiredIfNoDef bool

OnSet OnSetFn

// Sets to true if we have already configured once.
configured bool
}
Expand Down Expand Up @@ -132,12 +136,19 @@ func configure(opts []Options) []Options {
if item.TagName != "" {
opt.TagName = item.TagName
}
if item.OnSet != nil {
opt.OnSet = item.OnSet
}
opt.RequiredIfNoDef = item.RequiredIfNoDef
}

return []Options{opt}
}

func getOnSetFn(opts []Options) OnSetFn {
return opts[0].OnSet
}

// getTagName returns the tag name.
func getTagName(opts []Options) string {
return opts[0].TagName
Expand Down Expand Up @@ -219,9 +230,11 @@ func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Opti

func get(field reflect.StructField, opts []Options) (val string, err error) {
var exists bool
var isDefault bool
var loadFile bool
var unset bool
var notEmpty bool

required := opts[0].RequiredIfNoDef
expand := strings.EqualFold(field.Tag.Get("envExpand"), "true")

Expand All @@ -244,8 +257,9 @@ func get(field reflect.StructField, opts []Options) (val string, err error) {
}
}

expand := strings.EqualFold(field.Tag.Get("envExpand"), "true")
defaultValue, defExists := field.Tag.Lookup("envDefault")
val, exists = getOr(key, defaultValue, defExists, getEnvironment(opts))
val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts))

if expand {
val = os.ExpandEnv(val)
Expand All @@ -271,6 +285,10 @@ func get(field reflect.StructField, opts []Options) (val string, err error) {
}
}

onSetFn := getOnSetFn(opts)
if onSetFn != nil {
onSetFn(key, val, isDefault)
}
return val, err
}

Expand All @@ -285,16 +303,16 @@ func getFromFile(filename string) (value string, err error) {
return string(b), err
}

func getOr(key, defaultValue string, defExists bool, envs map[string]string) (value string, exists bool) {
value, exists = envs[key]
func getOr(key, defaultValue string, defExists bool, envs map[string]string) (string, bool, bool) {
value, exists := envs[key]
switch {
case (!exists || key == "") && defExists:
return defaultValue, true
return defaultValue, true, true
case !exists:
return "", false
return "", false, false
}

return value, true
return value, true, false
}

func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error {
Expand Down
55 changes: 55 additions & 0 deletions env_test.go
Expand Up @@ -662,6 +662,39 @@ func TestNoErrorRequiredSet(t *testing.T) {
is.Equal("", cfg.IsRequired)
}

func TestHook(t *testing.T) {
is := is.New(t)

type config struct {
Something string `env:"SOMETHING" envDefault:"important"`
Another string `env:"ANOTHER"`
}

cfg := &config{}

os.Setenv("ANOTHER", "1")
defer os.Clearenv()

type onSetArgs struct {
tag string
key interface{}
isDefault bool
}

var onSetCalled []onSetArgs

is.NoErr(Parse(cfg, Options{
OnSet: func(tag string, value interface{}, isDefault bool) {
onSetCalled = append(onSetCalled, onSetArgs{tag, value, isDefault})
},
}))
is.Equal("important", cfg.Something)
is.Equal("1", cfg.Another)
is.Equal(2, len(onSetCalled))
is.Equal(onSetArgs{"SOMETHING", "important", true}, onSetCalled[0])
is.Equal(onSetArgs{"ANOTHER", "1", false}, onSetCalled[1])
}

func TestErrorRequiredWithDefault(t *testing.T) {
is := is.New(t)

Expand Down Expand Up @@ -1103,6 +1136,28 @@ func ExampleParse() {
// Output: {Home:/tmp/fakehome Port:3000 IsProduction:false Inner:{Foo:foobar}}
}

func ExampleParse_onSet() {
type config struct {
Home string `env:"HOME,required"`
Port int `env:"PORT" envDefault:"3000"`
IsProduction bool `env:"PRODUCTION"`
}
os.Setenv("HOME", "/tmp/fakehome")
var cfg config
if err := Parse(&cfg, Options{
OnSet: func(tag string, value interface{}, isDefault bool) {
fmt.Printf("Set %s to %v (default? %v)\n", tag, value, isDefault)
},
}); err != nil {
fmt.Println("failed:", err)
}
fmt.Printf("%+v", cfg)
// Output: Set HOME to /tmp/fakehome (default? false)
// Set PORT to 3000 (default? true)
// Set PRODUCTION to (default? false)
// {Home:/tmp/fakehome Port:3000 IsProduction:false}
}

func TestIgnoresUnexported(t *testing.T) {
is := is.New(t)

Expand Down

0 comments on commit 222531b

Please sign in to comment.