From 6de20968040a9cf894f9bb5c2dfbb14cea5fb4ba Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 8 Jul 2021 18:04:28 -0300 Subject: [PATCH 1/2] feat: onset hook Signed-off-by: Carlos Alexandro Becker --- README.md | 38 ++++++++++++++++++++++++++++++++++++ env.go | 30 ++++++++++++++++++++++------- env_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fe1f959..44c82f6 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,44 @@ 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) +} +``` + ## Stargazers over time [![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) diff --git a/env.go b/env.go index 792c855..3c39130 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. @@ -102,6 +104,8 @@ type Options struct { // TagName specifies another tagname to use rather than the default env. TagName string + OnSet OnSetFn + // Sets to true if we have already configured once. configured bool } @@ -130,11 +134,18 @@ func configure(opts []Options) []Options { if item.TagName != "" { opt.TagName = item.TagName } + if item.OnSet != nil { + opt.OnSet = item.OnSet + } } 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 @@ -217,10 +228,10 @@ func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Opti func get(field reflect.StructField, opts []Options) (val string, err error) { var required bool var exists bool + var isDefault bool var loadFile bool var unset bool var notEmpty bool - expand := strings.EqualFold(field.Tag.Get("envExpand"), "true") key, tags := parseKeyForOption(field.Tag.Get(getTagName(opts))) @@ -241,8 +252,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) @@ -268,6 +280,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 } @@ -282,16 +298,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 e5b5169..76177a7 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 ExampleParseWithOnSet() { + 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) From a6727061b0c80e62e91058762fc9cd7be15764d2 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 8 Jul 2021 18:19:15 -0300 Subject: [PATCH 2/2] fix: lint Signed-off-by: Carlos Alexandro Becker --- env_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env_test.go b/env_test.go index 76177a7..ab0362e 100644 --- a/env_test.go +++ b/env_test.go @@ -1136,7 +1136,7 @@ func ExampleParse() { // Output: {Home:/tmp/fakehome Port:3000 IsProduction:false Inner:{Foo:foobar}} } -func ExampleParseWithOnSet() { +func ExampleParse_onSet() { type config struct { Home string `env:"HOME,required"` Port int `env:"PORT" envDefault:"3000"`