From 222531b04f97407d6ec130af1bb04c9f6502f67e Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 30 Aug 2021 10:12:35 -0300 Subject: [PATCH] feat: onset hook (#185) * feat: onset hook Signed-off-by: Carlos Alexandro Becker * fix: lint Signed-off-by: Carlos Alexandro Becker --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++-- env.go | 30 +++++++++++++++++++++++------ env_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1f1eda2..c01de93 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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() { diff --git a/env.go b/env.go index 93fd56b..1908598 100644 --- a/env.go +++ b/env.go @@ -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. @@ -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 } @@ -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 @@ -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") @@ -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) @@ -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 } @@ -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 { diff --git a/env_test.go b/env_test.go index fd94992..b2402ba 100644 --- a/env_test.go +++ b/env_test.go @@ -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) @@ -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)