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

Breaking change. New parsing .env files logic #127

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
98 changes: 76 additions & 22 deletions cleanenv.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
37 changes: 24 additions & 13 deletions cleanenv_test.go
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -1186,8 +1197,8 @@ no-env: this
"TEST_STRING": "test",
},
want: &config{
Number: 2,
String: "test",
Number: 1,
String: "default",
NoDefault: "",
NoEnv: "default",
},
Expand All @@ -1208,8 +1219,8 @@ no-env: this
"TEST_STRING": "fromEnv",
},
want: &config{
Number: 3,
String: "fromEnv",
Number: 2,
String: "test",
NoDefault: "NoDefault",
NoEnv: "this",
},
Expand Down