Skip to content

Commit

Permalink
Using error struct for additionals data (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyakaznacheev authored Jan 5, 2025
2 parents 8ca6c6a + f7cb9dd commit 0513259
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 17 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ jobs:
strategy:
matrix:
go-version:
- '1.23'
- '1.22'
- '1.21'
- '1.20'
- '1.19'
- '1.18'
- '1.17'
- '1.16'
- '1.15'
os:
- ubuntu-latest
- macos-latest
Expand Down
38 changes: 23 additions & 15 deletions cleanenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const (
// TagEnvRequired flag to mark a field as required
TagEnvRequired = "env-required"

// TagEnvPrefix аlag to specify prefix for structure fields
// TagEnvPrefix flag to specify prefix for structure fields
TagEnvPrefix = "env-prefix"
)

Expand Down Expand Up @@ -113,7 +113,7 @@ func UpdateEnv(cfg interface{}) error {
return readEnvVars(cfg, true)
}

// parseFile parses configuration file according to it's extension
// parseFile parses configuration file according to its extension
//
// Currently following file extensions are supported:
//
Expand Down Expand Up @@ -249,6 +249,7 @@ type structMeta struct {
description string
updatable bool
required bool
path string
}

// isFieldValueZero determines if fieldValue empty or not
Expand Down Expand Up @@ -302,9 +303,10 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
type cfgNode struct {
Val interface{}
Prefix string
Path string
}

cfgStack := []cfgNode{{cfgRoot, ""}}
cfgStack := []cfgNode{{cfgRoot, "", ""}}
metas := make([]structMeta, 0)

for i := 0; i < len(cfgStack); i++ {
Expand Down Expand Up @@ -342,7 +344,11 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
// add structure to parsing stack
if _, found := validStructs[fld.Type()]; !found {
prefix, _ := fType.Tag.Lookup(TagEnvPrefix)
cfgStack = append(cfgStack, cfgNode{fld.Addr().Interface(), sPrefix + prefix})
cfgStack = append(cfgStack, cfgNode{
Val: fld.Addr().Interface(),
Prefix: sPrefix + prefix,
Path: fmt.Sprintf("%s%s.", cfgStack[i].Path, fType.Name),
})
continue
}

Expand Down Expand Up @@ -392,6 +398,7 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
description: fType.Tag.Get(TagEnvDescription),
updatable: upd,
required: required,
path: cfgStack[i].Path,
})
}

Expand All @@ -408,7 +415,7 @@ func readEnvVars(cfg interface{}, update bool) error {
}

if updater, ok := cfg.(Updater); ok {
if err := updater.Update(); err != nil {
if err = updater.Update(); err != nil {
return err
}
}
Expand All @@ -428,10 +435,14 @@ func readEnvVars(cfg interface{}, update bool) error {
}
}

var envName string
if len(meta.envList) > 0 {
envName = meta.envList[0]
}

if rawValue == nil && meta.required && meta.isFieldValueZero() {
return fmt.Errorf(
"field %q is required but the value is not provided",
meta.fieldName,
return fmt.Errorf("field %q is required but the value is not provided",
meta.path+meta.fieldName,
)
}

Expand All @@ -443,13 +454,10 @@ func readEnvVars(cfg interface{}, update bool) error {
continue
}

var envName string
if len(meta.envList) > 0 {
envName = meta.envList[0]
}

if err := parseValue(meta.fieldValue, *rawValue, meta.separator, meta.layout); err != nil {
return fmt.Errorf("parsing field %v env %v: %v", meta.fieldName, envName, err)
if err = parseValue(meta.fieldValue, *rawValue, meta.separator, meta.layout); err != nil {
return fmt.Errorf("parsing field %q env %q: %v",
meta.path+meta.fieldName, envName, err,
)
}
}

Expand Down
81 changes: 81 additions & 0 deletions cleanenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,87 @@ func TestReadEnvVars(t *testing.T) {
}
}

func TestReadEnvErrors(t *testing.T) {
type testOneLevel struct {
Host string `env:"HOST" env-required:"true"`
}

type testTwoLevels struct {
Queue struct {
Host string `env:"HOST"`
} `env-prefix:"TEST_ERRORS_"`
Database struct {
Host string `env:"HOST" env-required:"true"`
TTL time.Duration `env:"TTL"`
} `env-prefix:"TEST_ERRORS_DATABASE_"`
ThirdStruct struct {
Host string `env:"HOST"`
} `env-prefix:"TEST_ERRORS_THIRD_"`
}

type testThreeLevels struct {
Database struct {
URL struct {
Host string `env:"HOST" env-required:"true"`
}
}
}

tests := []struct {
name string
env map[string]string
cfg interface{}
expectedError string
}{
{
name: "required error - one level",
env: nil,
cfg: &testOneLevel{},
expectedError: `field "Host" is required but the value is not provided`,
},
{
name: "required error - two levels",
env: nil,
cfg: &testTwoLevels{},
expectedError: `field "Database.Host" is required but the value is not provided`,
},
{
name: "required error - three levels",
env: nil,
cfg: &testThreeLevels{},
expectedError: `field "Database.URL.Host" is required but the value is not provided`,
},
{
name: "parsing error",
env: map[string]string{
"TEST_ERRORS_DATABASE_HOST": "localhost",
"TEST_ERRORS_DATABASE_TTL": "bad-value",
},
cfg: &testTwoLevels{},
expectedError: `parsing field "Database.TTL" env "TEST_ERRORS_DATABASE_TTL": time: invalid duration "bad-value"`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for env, val := range tt.env {
os.Setenv(env, val)
}
defer os.Clearenv()

err := readEnvVars(tt.cfg, false)

if err == nil {
t.Fatalf("expected error but got nil")
}

if err.Error() != tt.expectedError {
t.Errorf("unexpected error message: got %q, want %q", err.Error(), tt.expectedError)
}
})
}
}

func TestReadEnvVarsURL(t *testing.T) {
urlFunc := func(u string) url.URL {
parsed, err := url.Parse(u)
Expand Down

0 comments on commit 0513259

Please sign in to comment.