Skip to content

Commit

Permalink
feat: include more context in errors (#31)
Browse files Browse the repository at this point in the history
* feat: include more context in errors

* chore: make receivers as non-pointer type for errors
  • Loading branch information
ajatprabha authored Jan 9, 2024
1 parent 0020145 commit 6f7f235
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 54 deletions.
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

0 comments on commit 6f7f235

Please sign in to comment.