diff --git a/cleanenv.go b/cleanenv.go index ce0d0d2..e2556d8 100644 --- a/cleanenv.go +++ b/cleanenv.go @@ -95,22 +95,17 @@ type Updater interface { // ... // } func ReadConfig(path string, cfg interface{}) error { - err := parseFile(path, cfg) - if err != nil { - return err - } - - return readEnvVars(cfg, false) + return parseFile(path, cfg) } // ReadEnv reads environment variables into the structure. func ReadEnv(cfg interface{}) error { - return readEnvVars(cfg, false) + return readEnvVars(cfg, parseOsEnvs(cfg), false) } // UpdateEnv rereads (updates) environment variables in the structure. func UpdateEnv(cfg interface{}) error { - return readEnvVars(cfg, true) + return readEnvVars(cfg, parseOsEnvs(cfg), true) } // parseFile parses configuration file according to it's extension @@ -157,41 +152,50 @@ func parseFile(path string, cfg interface{}) error { // ParseYAML parses YAML from reader to data structure func ParseYAML(r io.Reader, str interface{}) error { - return yaml.NewDecoder(r).Decode(str) + err := yaml.NewDecoder(r).Decode(str) + if err != nil { + return err + } + return setDefaults(str) } // ParseJSON parses JSON from reader to data structure func ParseJSON(r io.Reader, str interface{}) error { - return json.NewDecoder(r).Decode(str) + err := json.NewDecoder(r).Decode(str) + if err != nil { + return err + } + return setDefaults(str) } // ParseTOML parses TOML from reader to data structure func ParseTOML(r io.Reader, str interface{}) error { _, err := toml.NewDecoder(r).Decode(str) - return err + if err != nil { + return err + } + return setDefaults(str) } // parseEDN parses EDN from reader to data structure func parseEDN(r io.Reader, str interface{}) error { - return edn.NewDecoder(r).Decode(str) + err := edn.NewDecoder(r).Decode(str) + if err != nil { + return err + } + return setDefaults(str) } // parseENV, in fact, doesn't fill the structure with environment variable values. // It just parses ENV file and sets all variables to the environment. // Thus, the structure should be filled at the next steps. -func parseENV(r io.Reader, _ interface{}) error { +func parseENV(r io.Reader, cfg interface{}) error { vars, err := godotenv.Parse(r) if err != nil { return err } - for env, val := range vars { - if err = os.Setenv(env, val); err != nil { - return fmt.Errorf("set environment: %w", err) - } - } - - return nil + return readEnvVars(cfg, vars, false) } // parseSlice parses value into a slice of given type @@ -400,8 +404,58 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) { return metas, nil } +// parseOsEnvs parses environment variables into map +func parseOsEnvs(cfg interface{}) (envs map[string]string) { + envs = make(map[string]string) + metaInfo, err := readStructMetadata(cfg) + if err != nil { + return + } + + for _, meta := range metaInfo { + for _, env := range meta.envList { + if value, ok := os.LookupEnv(env); ok { + envs[env] = value + break + } + } + } + + return +} + +func setDefaults(cfg interface{}) error { + metaInfo, err := readStructMetadata(cfg) + if err != nil { + return err + } + + if updater, ok := cfg.(Updater); ok { + if err := updater.Update(); err != nil { + return err + } + } + + for _, meta := range metaInfo { + if meta.required && meta.isFieldValueZero() { + return fmt.Errorf( + "field %q is required but the value is not provided", + meta.fieldName, + ) + } + + if meta.isFieldValueZero() && meta.defValue != nil { + if err := parseValue(meta.fieldValue, *meta.defValue, meta.separator, meta.layout); err != nil { + return fmt.Errorf("parsing default value for field %v: %v", meta.fieldName, err) + } + } + } + + return nil +} + // readEnvVars reads environment variables to the provided configuration structure -func readEnvVars(cfg interface{}, update bool) error { +func readEnvVars(cfg interface{}, envs map[string]string, update bool) error { metaInfo, err := readStructMetadata(cfg) if err != nil { return err @@ -422,7 +476,7 @@ func readEnvVars(cfg interface{}, update bool) error { var rawValue *string for _, env := range meta.envList { - if value, ok := os.LookupEnv(env); ok { + if value, ok := envs[env]; ok { rawValue = &value break } diff --git a/cleanenv_test.go b/cleanenv_test.go index 0a13fcb..3283182 100644 --- a/cleanenv_test.go +++ b/cleanenv_test.go @@ -313,7 +313,7 @@ func TestReadEnvVars(t *testing.T) { } defer os.Clearenv() - if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr { + if err := readEnvVars(tt.cfg, parseOsEnvs(tt.cfg), false); (err != nil) != tt.wantErr { t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr) } if !reflect.DeepEqual(tt.cfg, tt.want) { @@ -374,7 +374,7 @@ func TestReadEnvVarsURL(t *testing.T) { } defer os.Clearenv() - if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr { + if err := readEnvVars(tt.cfg, parseOsEnvs(tt.cfg), false); (err != nil) != tt.wantErr { t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr) } if !reflect.DeepEqual(tt.cfg, tt.want) { @@ -425,7 +425,7 @@ func TestReadEnvVarsTime(t *testing.T) { } defer os.Clearenv() - if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr { + if err := readEnvVars(tt.cfg, parseOsEnvs(tt.cfg), false); (err != nil) != tt.wantErr { t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr) } if !reflect.DeepEqual(tt.cfg, tt.want) { @@ -467,7 +467,7 @@ func TestReadEnvVarsWithPrefix(t *testing.T) { } var cfg Config - if err := readEnvVars(&cfg, false); err != nil { + if err := readEnvVars(&cfg, parseOsEnvs(&cfg), false); err != nil { t.Fatal("failed to read env vars", err) } @@ -554,7 +554,7 @@ func TestReadUpdateFunctions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr { + if err := readEnvVars(tt.cfg, parseOsEnvs(tt.cfg), false); (err != nil) != tt.wantErr { t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr) } if !reflect.DeepEqual(tt.cfg, tt.want) { @@ -693,7 +693,11 @@ two = 2`, } func TestParseFileEnv(t *testing.T) { - type dummy struct{} + type dummy struct { + Test1 string `env:"TEST1"` + Test2 string `env:"TEST2"` + Test3 string `env:"TEST3"` + } tests := []struct { name string @@ -757,8 +761,15 @@ func TestParseFileEnv(t *testing.T) { if err = parseFile(tmpFile.Name(), &cfg); (err != nil) != tt.wantErr { t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr) } + + vals := map[string]string{ + "TEST1": cfg.Test1, + "TEST2": cfg.Test2, + "TEST3": cfg.Test3, + } + for key, val := range tt.has { - if envVal := os.Getenv(key); err == nil && val != envVal { + if envVal := vals[key]; err == nil && val != envVal { t.Errorf("wrong value %s of var %s, want %s", envVal, key, val) } } @@ -1150,8 +1161,8 @@ func TestReadConfig(t *testing.T) { "TEST_STRING": "fromEnv", }, want: &config{ - Number: 3, - String: "fromEnv", + Number: 2, + String: "test", NoDefault: "NoDefault", NoEnv: "this", }, @@ -1186,8 +1197,8 @@ no-env: this "TEST_STRING": "test", }, want: &config{ - Number: 2, - String: "test", + Number: 1, + String: "default", NoDefault: "", NoEnv: "default", }, @@ -1208,8 +1219,8 @@ no-env: this "TEST_STRING": "fromEnv", }, want: &config{ - Number: 3, - String: "fromEnv", + Number: 2, + String: "test", NoDefault: "NoDefault", NoEnv: "this", },