Skip to content

Commit

Permalink
Merge pull request #62 from heetch/rog-001-cleanup
Browse files Browse the repository at this point in the history
code cleanups; more tests
  • Loading branch information
rogpeppe authored May 10, 2019
2 parents ce4cc83 + 3054e05 commit fb17a8f
Show file tree
Hide file tree
Showing 4 changed files with 588 additions and 104 deletions.
128 changes: 66 additions & 62 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ type Loader struct {
Tag string
}

// Unmarshaler can be implemented by backends to receive the struct directly and load values into it.
type Unmarshaler interface {
Unmarshal(ctx context.Context, to interface{}) error
}

// StructLoader can be implemented by backends to receive the parsed struct informations and load values into it.
type StructLoader interface {
LoadStruct(ctx context.Context, cfg *StructConfig) error
}

// NewLoader creates a Loader. If no backend is specified, the loader uses the environment.
func NewLoader(backends ...backend.Backend) *Loader {
l := Loader{
Expand Down Expand Up @@ -53,12 +63,12 @@ func (l *Loader) Load(ctx context.Context, to interface{}) error {

ref = ref.Elem()

s := l.parseStruct(&ref)
s := l.parseStruct(ref)
s.S = to
return l.resolve(ctx, s)
}

func (l *Loader) parseStruct(ref *reflect.Value) *StructConfig {
func (l *Loader) parseStruct(ref reflect.Value) *StructConfig {
var s StructConfig

t := ref.Type()
Expand All @@ -85,14 +95,13 @@ func (l *Loader) parseStruct(ref *reflect.Value) *StructConfig {
}

// if struct or *struct, parse recursively
switch {
case typ.Kind() == reflect.Struct:
s.Fields = append(s.Fields, l.parseStruct(&value).Fields...)
switch typ.Kind() {
case reflect.Struct:
s.Fields = append(s.Fields, l.parseStruct(value).Fields...)
continue
case typ.Kind() == reflect.Ptr:
if value.Type().Elem().Kind() == reflect.Struct && !value.IsNil() {
value = value.Elem()
s.Fields = append(s.Fields, l.parseStruct(&value).Fields...)
case reflect.Ptr:
if typ.Elem().Kind() == reflect.Struct && !value.IsNil() {
s.Fields = append(s.Fields, l.parseStruct(value.Elem()).Fields...)
continue
}
}
Expand All @@ -105,13 +114,13 @@ func (l *Loader) parseStruct(ref *reflect.Value) *StructConfig {
f := FieldConfig{
Name: field.Name,
Key: tag,
Value: &value,
Value: value,
}

// copying field content to a new value
clone := reflect.Indirect(reflect.New(f.Value.Type()))
clone.Set(*f.Value)
f.Default = &clone
clone.Set(f.Value)
f.Default = clone

if idx := strings.Index(tag, ","); idx != -1 {
f.Key = tag[:idx]
Expand Down Expand Up @@ -216,8 +225,8 @@ type StructConfig struct {
type FieldConfig struct {
Name string
Key string
Value *reflect.Value
Default *reflect.Value
Value reflect.Value
Default reflect.Value
Required bool
Backend string
}
Expand All @@ -227,47 +236,26 @@ func (f *FieldConfig) Set(data string) error {
return convert(data, f.Value)
}

func convert(data string, value *reflect.Value) error {
k := value.Kind()
t := value.Type()
var durationType = reflect.TypeOf(time.Duration(0))

switch {
case t.String() == "time.Duration":
func convert(data string, value reflect.Value) error {
t := value.Type()
if t == durationType {
d, err := time.ParseDuration(data)
if err != nil {
return err
}

value.SetInt(int64(d))
case k == reflect.Bool:
return nil
}
switch t.Kind() {
case reflect.Bool:
b, err := strconv.ParseBool(data)
if err != nil {
return err
}

value.SetBool(b)
case k >= reflect.Int && k <= reflect.Int64:
i, err := strconv.ParseInt(data, 10, 64)
if err != nil {
return err
}

value.SetInt(i)
case k >= reflect.Uint && k <= reflect.Uint64:
i, err := strconv.ParseUint(data, 10, 64)
if err != nil {
return err
}

value.SetUint(i)
case k >= reflect.Float32 && k <= reflect.Float64:
f, err := strconv.ParseFloat(data, 64)
if err != nil {
return err
}

value.SetFloat(f)
case k == reflect.Slice:
case reflect.Slice:
var err error
// create a new temporary slice to override the actual Value if it's not empty
nv := reflect.MakeSlice(value.Type(), 0, 0)
Expand All @@ -276,40 +264,56 @@ func convert(data string, value *reflect.Value) error {
// create a new Value v based on the type of the slice
v := reflect.Indirect(reflect.New(t.Elem()))
// call convert to set the current value of the slice to v
err = convert(s, &v)
err = convert(s, v)
// append v to the temporary slice
nv = reflect.Append(nv, v)
}
// Set the newly created temporary slice to the target Value
value.Set(nv)
return err

case k == reflect.String:
case reflect.String:
value.SetString(data)
case k == reflect.Ptr:
case reflect.Ptr:
n := reflect.New(value.Type().Elem())
value.Set(n)
e := n.Elem()
return convert(data, &e)
return convert(data, n.Elem())
case reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64:
i, err := strconv.ParseInt(data, 10, t.Bits())
if err != nil {
return err
}

value.SetInt(i)
case reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64:
i, err := strconv.ParseUint(data, 10, t.Bits())
if err != nil {
return err
}

value.SetUint(i)
case reflect.Float32, reflect.Float64:
f, err := strconv.ParseFloat(data, t.Bits())
if err != nil {
return err
}
value.SetFloat(f)
default:
return fmt.Errorf("field type '%s' not supported", k)
return fmt.Errorf("field type '%s' not supported", t.Kind())
}

return nil
}

func isZero(v *reflect.Value) bool {
func isZero(v reflect.Value) bool {
zero := reflect.Zero(v.Type()).Interface()
current := v.Interface()
return reflect.DeepEqual(current, zero)
}

// Unmarshaler can be implemented by backends to receive the struct directly and load values into it.
type Unmarshaler interface {
Unmarshal(ctx context.Context, to interface{}) error
}

// StructLoader can be implemented by backends to receive the parsed struct informations and load values into it.
type StructLoader interface {
LoadStruct(ctx context.Context, cfg *StructConfig) error
}
99 changes: 99 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"math"
"regexp"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -85,6 +86,7 @@ func TestLoad(t *testing.T) {
StructPtrNil *nested
StructPtrNotNil *nested
Ignored string
unexported int `config:"ignore"`
}

var s testStruct
Expand Down Expand Up @@ -493,3 +495,100 @@ func TestSliceField(t *testing.T) {
require.Equal(t, 42, *s.Numbers[2])
})
}

var errorTests = []struct {
testName string
store store
into interface{}
expectError string
}{{
testName: "bad-duration",
store: store{
"X": "xxxx",
},
into: new(struct {
X time.Duration `config:"X"`
}),
expectError: `time: invalid duration xxxx`,
}, {
testName: "bad-bool",
store: store{
"X": "xxxx",
},
into: new(struct {
X bool `config:"X"`
}),
expectError: `strconv.ParseBool: parsing "xxxx": invalid syntax`,
}, {
testName: "bad-int",
store: store{
"X": "xxxx",
},
into: new(struct {
X int `config:"X"`
}),
expectError: `strconv.ParseInt: parsing "xxxx": invalid syntax`,
}, {
testName: "bad-uint",
store: store{
"X": "xxxx",
},
into: new(struct {
X uint `config:"X"`
}),
expectError: `strconv.ParseUint: parsing "xxxx": invalid syntax`,
}, {
testName: "out-of-range-int",
store: store{
"X": "128",
},
into: new(struct {
X int8 `config:"X"`
}),
expectError: `strconv.ParseInt: parsing "128": value out of range`,
}, {
testName: "out-of-range-uint",
store: store{
"X": "256",
},
into: new(struct {
X uint8 `config:"X"`
}),
expectError: `strconv.ParseUint: parsing "256": value out of range`,
}, {
testName: "bad-float",
store: store{
"X": "xxxx",
},
into: new(struct {
X float64 `config:"X"`
}),
expectError: `strconv.ParseFloat: parsing "xxxx": invalid syntax`,
}, {
testName: "unsupported-field-type",
store: store{
"X": "xxxx",
},
into: new(struct {
X uintptr `config:"X"`
}),
expectError: `field type 'uintptr' not supported`,
}, {
testName: "not-struct-pointer",
into: struct{}{},
expectError: `provided target must be a pointer to struct`,
}}

func TestError(t *testing.T) {
for _, test := range errorTests {
t.Run(test.testName, func(t *testing.T) {
err := confita.NewLoader(test.store).Load(context.Background(), test.into)
if err == nil {
t.Fatalf("unexpected success; into %#v", test.into)
}
if ok, err1 := regexp.MatchString("^("+test.expectError+")$", err.Error()); !ok || err1 != nil {
t.Fatalf("error mismatch; got %q want %q", err, test.expectError)
}
})
}
}
Loading

0 comments on commit fb17a8f

Please sign in to comment.