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

Using error struct for additionals data #141

Open
wants to merge 4 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions cleanenv.go
Original file line number Diff line number Diff line change
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,11 +435,13 @@ 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 newRequireError(meta.fieldName, meta.path, envName)
}

if rawValue == nil && meta.isFieldValueZero() {
Expand All @@ -443,13 +452,8 @@ 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 newParsingError(meta.fieldName, meta.path, envName, err)
}
}

Expand Down
111 changes: 111 additions & 0 deletions cleanenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cleanenv

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -323,6 +324,116 @@ 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{}
errorAs interface{}
errorWant interface{}
}{
{
name: "required error - one level",
env: nil,
cfg: &testOneLevel{},
errorAs: RequireError{},
errorWant: RequireError{
FieldPath: "",
FieldName: "Host",
EnvName: "HOST",
},
},
{
name: "required error - three levels",
env: nil,
cfg: &testThreeLevels{},
errorAs: RequireError{},
errorWant: RequireError{
FieldPath: "Database.URL.",
FieldName: "Host",
EnvName: "HOST",
},
},
{
name: "required error - two levels",
env: nil,
cfg: &testTwoLevels{},
errorAs: RequireError{},
errorWant: RequireError{
FieldPath: "Database.",
FieldName: "Host",
EnvName: "TEST_ERRORS_DATABASE_HOST",
},
},
{
name: "parsing error",
env: map[string]string{
"TEST_ERRORS_DATABASE_HOST": "localhost",
"TEST_ERRORS_DATABASE_TTL": "bad-value",
},
cfg: &testTwoLevels{},
errorAs: ParsingError{},
errorWant: ParsingError{
Err: fmt.Errorf("time: invalid duration \"bad-value\""),
FieldName: "TTL",
FieldPath: "Database.",
EnvName: "TEST_ERRORS_DATABASE_TTL",
},
},
}

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.Errorf("wrong behavior %v, want error", err)
}

if !errors.As(err, &tt.errorAs) {
t.Errorf("wrong error as %T, want %T", tt.errorAs, err)
}

if tt.errorWant != nil && !reflect.DeepEqual(tt.errorAs, tt.errorWant) {
// not using error interface for printing value
bytes1, _ := json.Marshal(tt.errorAs)
bytes2, _ := json.Marshal(tt.errorWant)

t.Errorf("wrong error data %s, want %s", bytes1, bytes2)
}
})
}
}

func TestReadEnvVarsURL(t *testing.T) {
urlFunc := func(u string) url.URL {
parsed, err := url.Parse(u)
Expand Down
55 changes: 55 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cleanenv

import (
"fmt"
)

type RequireError struct {
FieldName string
FieldPath string
EnvName string
}

func newRequireError(fieldName string, fieldPath string, envName string) RequireError {
return RequireError{
FieldName: fieldName,
FieldPath: fieldPath,
EnvName: envName,
}
}

func (r RequireError) Error() string {
return fmt.Sprintf(
"field %q is required but the value is not provided",
r.FieldPath+r.FieldName,
)
}

type ParsingError struct {
Err error
FieldName string
FieldPath string
EnvName string
}

func newParsingError(fieldName string, fieldPath string, envName string, err error) ParsingError {
return ParsingError{
FieldName: fieldName,
FieldPath: fieldPath,
EnvName: envName,
Err: err,
}
}

func (p ParsingError) Error() string {
return fmt.Sprintf(
"parsing field %q env %q: %v",
p.FieldPath+p.FieldName,
p.EnvName,
p.Err,
)
}

func (p ParsingError) Unwrap() error {
return p.Err
}