Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: onset hook #185

Merged
merged 4 commits into from Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 42 additions & 2 deletions README.md
Expand Up @@ -290,11 +290,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 @@ -306,8 +346,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