diff --git a/xload/async.go b/xload/async.go index 5e3c2e6..46ac31a 100644 --- a/xload/async.go +++ b/xload/async.go @@ -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 } @@ -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) @@ -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) @@ -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 { @@ -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 diff --git a/xload/async_test.go b/xload/async_test.go index 3b1c80e..81fca41 100644 --- a/xload/async_test.go +++ b/xload/async_test.go @@ -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 { @@ -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)) @@ -148,8 +148,8 @@ 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)) @@ -157,6 +157,9 @@ func Test_loadAndSetWithOriginal(t *testing.T) { 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) }) } diff --git a/xload/errors.go b/xload/errors.go new file mode 100644 index 0000000..1dc79ec --- /dev/null +++ b/xload/errors.go @@ -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) +} diff --git a/xload/errors_test.go b/xload/errors_test.go new file mode 100644 index 0000000..1b4018c --- /dev/null +++ b/xload/errors_test.go @@ -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()") + }) + } +} diff --git a/xload/load.go b/xload/load.go index 490524d..6496301 100644 --- a/xload/load.go +++ b/xload/load.go @@ -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 ( @@ -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 } @@ -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 { @@ -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 @@ -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, } @@ -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} } } @@ -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]) @@ -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 diff --git a/xload/load_struct_test.go b/xload/load_struct_test.go index dabdb21..b4884a0 100644 --- a/xload/load_struct_test.go +++ b/xload/load_struct_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "reflect" "testing" "time" @@ -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", @@ -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{}, }, } @@ -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{}, }, } diff --git a/xload/load_test.go b/xload/load_test.go index e4cf0d4..1db4855 100644 --- a/xload/load_test.go +++ b/xload/load_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "os" + "reflect" "testing" "time" @@ -114,7 +115,7 @@ func TestLoad_Errors(t *testing.T) { Host string `env:"HOST,unknown"` }{}, loader: MapLoader{}, - err: ErrUnknownTagOption, + err: &ErrUnknownTagOption{key: "HOST", opt: "unknown"}, }, } @@ -124,6 +125,8 @@ func TestLoad_Errors(t *testing.T) { func TestLoad_NativeTypes(t *testing.T) { t.Parallel() + anyKind := reflect.TypeOf(new(any)).Elem().Kind() + testcases := []testcase{ // boolean value { @@ -408,7 +411,7 @@ func TestLoad_NativeTypes(t *testing.T) { StringMap map[string]string `env:"STRING_MAP"` }{}, loader: MapLoader{"STRING_MAP": "key1::value1,key2::value2"}, - err: ErrInvalidMapValue, + err: &ErrInvalidMapValue{key: "STRING_MAP"}, }, { name: "map: invalid value", @@ -427,24 +430,24 @@ func TestLoad_NativeTypes(t *testing.T) { err: errors.New("unable to cast"), }, - // unknown field type + // unknown key type { - name: "unknown field type", + name: "unknown key type", input: &struct { Unknown interface{} `env:"UNKNOWN"` }{}, loader: MapLoader{"UNKNOWN": "1+2i"}, - err: ErrUnknownFieldType, + err: &ErrUnknownFieldType{field: "Unknown", key: "UNKNOWN", kind: anyKind}, }, { - name: "nested unknown field type", + name: "nested unknown key type", input: &struct { Nested struct { Unknown interface{} `env:"UNKNOWN"` } `env:",prefix=NESTED_"` }{}, loader: MapLoader{"NESTED_UNKNOWN": "1+2i"}, - err: ErrUnknownFieldType, + err: &ErrUnknownFieldType{field: "Unknown", key: "UNKNOWN", kind: anyKind}, }, } @@ -574,7 +577,7 @@ func TestLoad_MapTypes(t *testing.T) { StringMap map[string]string `env:"STRING_MAP"` }{}, loader: MapLoader{"STRING_MAP": "key1::value1,key2::value2"}, - err: ErrInvalidMapValue, + err: &ErrInvalidMapValue{key: "STRING_MAP"}, }, { name: "map: invalid value", @@ -669,7 +672,7 @@ func TestOption_Required(t *testing.T) { input: &struct { Name string `env:"NAME,required"` }{}, - err: ErrRequired, + err: &ErrRequired{key: "NAME"}, loader: MapLoader{}, }, { @@ -677,7 +680,7 @@ func TestOption_Required(t *testing.T) { input: &struct { Name CustomGob `env:"NAME,required"` }{}, - err: ErrRequired, + err: &ErrRequired{key: "NAME"}, loader: MapLoader{}, }, { @@ -685,7 +688,7 @@ func TestOption_Required(t *testing.T) { input: &struct { Name *string `env:"NAME,required"` }{}, - err: ErrRequired, + err: &ErrRequired{key: "NAME"}, loader: MapLoader{"NAME": ""}, }, {