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

feat: include more context in errors #31

Merged
merged 4 commits into from
Jan 9, 2024
Merged
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
14 changes: 7 additions & 7 deletions xload/async.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func processAsync(p *pool.ContextPool, o *options, loader Loader, obj any, cb fu
continue
}

meta, err := parseField(tag)
meta, err := parseField(fTyp.Name, tag)
if err != nil {
return err
}
Expand Down Expand Up @@ -95,7 +95,7 @@ func processAsync(p *pool.ContextPool, o *options, loader Loader, obj any, cb fu

// if the struct has a key, load it
// and set the value to the struct
if meta.name != "" && hasDecoder(fVal) {
if meta.key != "" && hasDecoder(fVal) {
las := loadAndSetWithOriginal(loader, meta)

original := value.Field(i)
Expand All @@ -121,7 +121,7 @@ func processAsync(p *pool.ContextPool, o *options, loader Loader, obj any, cb fu
}

if meta.prefix != "" {
return ErrInvalidPrefix
return &ErrInvalidPrefix{field: fTyp.Name, kind: fVal.Kind()}
}

las := loadAndSetVal(loader, meta)
Expand All @@ -148,13 +148,13 @@ func setNilStructPtr(original reflect.Value, v reflect.Value, isNilStructPtr boo

func loadAndSetWithOriginal(loader Loader, meta *field) loadAndSetPointer {
return func(ctx context.Context, original reflect.Value, fVal reflect.Value, isNilStructPtr bool) error {
val, err := loader.Load(ctx, meta.name)
val, err := loader.Load(ctx, meta.key)
if err != nil {
return err
}

if val == "" && meta.required {
return ErrRequired
return &ErrRequired{key: meta.key}
}

if ok, err := decode(fVal, val); ok {
Expand All @@ -172,13 +172,13 @@ func loadAndSetWithOriginal(loader Loader, meta *field) loadAndSetPointer {
func loadAndSetVal(loader Loader, meta *field) loadAndSet {
return func(ctx context.Context, fVal reflect.Value) error {
// lookup value
val, err := loader.Load(ctx, meta.name)
val, err := loader.Load(ctx, meta.key)
if err != nil {
return err
}

if val == "" && meta.required {
return ErrRequired
return &ErrRequired{key: meta.key}
}

// set value
Expand Down
13 changes: 8 additions & 5 deletions xload/async_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func Test_loadAndSetWithOriginal(t *testing.T) {
}

t.Run("successful load and set", func(t *testing.T) {
meta := &field{name: "testName", required: true}
meta := &field{key: "testName", required: true}

obj := &Args{
Nest: &struct {
Expand All @@ -137,7 +137,7 @@ func Test_loadAndSetWithOriginal(t *testing.T) {
})

t.Run("loader returns error", func(t *testing.T) {
meta := &field{name: "testName", required: true}
meta := &field{key: "testName", required: true}
original := reflect.ValueOf(new(string))
fVal := reflect.ValueOf(new(string))

Expand All @@ -148,15 +148,18 @@ func Test_loadAndSetWithOriginal(t *testing.T) {
assert.Equal(t, "load error", err.Error())
})

t.Run("field is required but loader val is empty", func(t *testing.T) {
meta := &field{name: "testName", required: true}
t.Run("key is required but loader val is empty", func(t *testing.T) {
meta := &field{key: "testName", required: true}
original := reflect.ValueOf(new(string))
fVal := reflect.ValueOf(new(string))

err := loadAndSetWithOriginal(LoaderFunc(func(ctx context.Context, key string) (string, error) {
return "", nil
}), meta)(context.Background(), original, fVal, true)
assert.NotNil(t, err)
assert.Equal(t, ErrRequired, err)

wantErr := &ErrRequired{}
assert.ErrorAs(t, err, &wantErr)
assert.Equal(t, "testName", wantErr.key)
})
}
63 changes: 63 additions & 0 deletions xload/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package xload

import (
"fmt"
"reflect"
)

// ErrRequired is returned when a required key is missing.
type ErrRequired struct{ key string }

func (e ErrRequired) Error() string { return "required key missing: " + e.key }

// ErrUnknownTagOption is returned when an unknown tag option is used.
type ErrUnknownTagOption struct {
key string
opt string
}

func (e ErrUnknownTagOption) Error() string {
if e.key == "" {
return fmt.Sprintf("unknown tag option: %s", e.opt)
}

return fmt.Sprintf("`%s` key has unknown tag option: %s", e.key, e.opt)
}

// ErrUnknownFieldType is returned when the key type is not supported.
type ErrUnknownFieldType struct {
field string
kind reflect.Kind
key string
}

func (e ErrUnknownFieldType) Error() string {
return fmt.Sprintf("`%s: %s` key=%s has an invalid value", e.field, e.kind, e.key)
}

// ErrInvalidMapValue is returned when the map value is invalid.
type ErrInvalidMapValue struct{ key string }

func (e ErrInvalidMapValue) Error() string {
return fmt.Sprintf("`%s` key has an invalid map value", e.key)
}

// ErrInvalidPrefix is returned when the prefix option is used on a non-struct key.
type ErrInvalidPrefix struct {
field string
kind reflect.Kind
}

func (e ErrInvalidPrefix) Error() string {
return fmt.Sprintf("prefix is only valid on struct types, found `%s: %s`", e.field, e.kind)
}

// ErrInvalidPrefixAndKey is returned when the prefix option is used with a key.
type ErrInvalidPrefixAndKey struct {
field string
key string
}

func (e ErrInvalidPrefixAndKey) Error() string {
return fmt.Sprintf("`%s` key=%s has both prefix and key", e.field, e.key)
}
38 changes: 38 additions & 0 deletions xload/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package xload

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestErrUnknownTagOption_Error(t *testing.T) {
tests := []struct {
name string
key string
opt string
want string
}{
{
name: "key and opt",
key: "key",
opt: "opt",
want: "`key` key has unknown tag option: opt",
},
{
name: "opt only",
opt: "opt",
want: "unknown tag option: opt",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &ErrUnknownTagOption{
key: tt.key,
opt: tt.opt,
}
assert.Equalf(t, tt.want, e.Error(), "Error()")
})
}
}
42 changes: 16 additions & 26 deletions xload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,8 @@ var (
ErrNotPointer = errors.New("xload: config must be a pointer")
// ErrNotStruct is returned when the given config is not a struct.
ErrNotStruct = errors.New("xload: config must be a struct")
// ErrUnknownTagOption is returned when an unknown tag option is used.
ErrUnknownTagOption = errors.New("xload: unknown tag option")
// ErrRequired is returned when a required field is missing.
ErrRequired = errors.New("xload: missing required value")
// ErrUnknownFieldType is returned when the field type is not supported.
ErrUnknownFieldType = errors.New("xload: unknown field type")
// ErrInvalidMapValue is returned when the map value is invalid.
ErrInvalidMapValue = errors.New("xload: invalid map value")
// ErrMissingKey is returned when the key is missing from the tag.
ErrMissingKey = errors.New("xload: missing key")
// ErrInvalidPrefix is returned when the prefix option is used on a non-struct field.
ErrInvalidPrefix = errors.New("xload: prefix is only valid on struct types")
// ErrInvalidPrefixAndKey is returned when the prefix option is used with a key.
ErrInvalidPrefixAndKey = errors.New("xload: prefix cannot be used when field name is set")
ErrMissingKey = errors.New("xload: missing key on required field")
)

const (
Expand Down Expand Up @@ -86,7 +74,7 @@ func process(ctx context.Context, obj any, tagKey string, loader Loader) error {
continue
}

meta, err := parseField(tag)
meta, err := parseField(fTyp.Name, tag)
if err != nil {
return err
}
Expand Down Expand Up @@ -127,14 +115,14 @@ func process(ctx context.Context, obj any, tagKey string, loader Loader) error {

// if the struct has a key, load it
// and set the value to the struct
if meta.name != "" {
val, err := loader.Load(ctx, meta.name)
if meta.key != "" {
val, err := loader.Load(ctx, meta.key)
if err != nil {
return err
}

if val == "" && meta.required {
return ErrRequired
return &ErrRequired{key: meta.key}
}

if ok, err := decode(fVal, val); ok {
Expand Down Expand Up @@ -164,17 +152,17 @@ func process(ctx context.Context, obj any, tagKey string, loader Loader) error {
}

if meta.prefix != "" {
return ErrInvalidPrefix
return &ErrInvalidPrefix{field: fTyp.Name, kind: fVal.Kind()}
}

// lookup value
val, err := loader.Load(ctx, meta.name)
val, err := loader.Load(ctx, meta.key)
if err != nil {
return err
}

if val == "" && meta.required {
return ErrRequired
return &ErrRequired{key: meta.key}
}

// set value
Expand All @@ -189,18 +177,20 @@ func process(ctx context.Context, obj any, tagKey string, loader Loader) error {

type field struct {
name string
key string
prefix string
required bool
delimiter string
separator string
}

func parseField(tag string) (*field, error) {
func parseField(name, tag string) (*field, error) {
parts := strings.Split(tag, ",")
key, tagOpts := strings.TrimSpace(parts[0]), parts[1:]

f := &field{
name: key,
name: name,
key: key,
delimiter: defaultDelimiter,
separator: defaultSeparator,
}
Expand All @@ -219,14 +209,14 @@ func parseField(tag string) (*field, error) {
f.prefix = strings.TrimPrefix(opt, optPrefix)

if key != "" && f.prefix != "" {
return nil, ErrInvalidPrefixAndKey
return nil, &ErrInvalidPrefixAndKey{field: name, key: key}
}
case strings.HasPrefix(opt, optDelimiter):
f.delimiter = strings.TrimPrefix(opt, optDelimiter)
case strings.HasPrefix(opt, optSeparator):
f.separator = strings.TrimPrefix(opt, optSeparator)
default:
return nil, ErrUnknownTagOption
return nil, &ErrUnknownTagOption{key: key, opt: opt}
}
}

Expand Down Expand Up @@ -318,7 +308,7 @@ func setVal(field reflect.Value, val string, meta *field) error {
for _, v := range vals {
kv := strings.Split(v, meta.separator)
if len(kv) != 2 {
return ErrInvalidMapValue
return &ErrInvalidMapValue{key: meta.key}
}

k, v := strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])
Expand Down Expand Up @@ -365,7 +355,7 @@ func setVal(field reflect.Value, val string, meta *field) error {
field.Set(slice)

default:
return ErrUnknownFieldType
return &ErrUnknownFieldType{field: meta.name, key: meta.key, kind: kd}
}

return nil
Expand Down
13 changes: 8 additions & 5 deletions xload/load_struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"reflect"
"testing"
"time"

Expand Down Expand Up @@ -74,6 +75,8 @@ func (p *Plots) Decode(s string) error {
func TestLoad_Structs(t *testing.T) {
t.Parallel()

strKind := reflect.TypeOf("").Kind()

testcases := []testcase{
{
name: "nested struct: using prefix",
Expand Down Expand Up @@ -168,19 +171,19 @@ func TestLoad_Structs(t *testing.T) {
},
},
{
name: "non-struct field with prefix",
name: "non-struct key with prefix",
input: &struct {
Name string `env:",prefix=CLUSTER"`
}{},
err: ErrInvalidPrefix,
err: &ErrInvalidPrefix{field: "Name", kind: strKind},
loader: MapLoader{},
},
{
name: "struct field with name and prefix",
name: "struct with key and prefix",
input: &struct {
Address Address `env:"ADDRESS,prefix=CLUSTER"`
}{},
err: ErrInvalidPrefixAndKey,
err: &ErrInvalidPrefixAndKey{field: "Address", key: "ADDRESS"},
loader: MapLoader{},
},
}
Expand Down Expand Up @@ -318,7 +321,7 @@ func TestLoad_JSON(t *testing.T) {
input: &struct {
Plot Plot `env:"PLOT,required"`
}{},
err: ErrRequired,
err: &ErrRequired{key: "PLOT"},
loader: MapLoader{},
},
}
Expand Down
Loading
Loading