From 8769aea246c524914dedb2f0c62f09551e619c6f Mon Sep 17 00:00:00 2001 From: urso Date: Wed, 5 Oct 2016 14:04:13 +0200 Subject: [PATCH] fix failing type conversation Some edge cases using environment variables or direct conversation including floating values have been shown to fail - add direct support for float conversation by identifying if target type is float + support casting int/uint to float - string type can be parsed/converted to types bool, int, uint and float. In few cases, strings for numberic values have been read from yaml and put into ucfg (e.g. user using quotes, environment variable expansion). Allowing to convert strings dynamically to primitives solves some failing edge cases (new unit tests included). --- CHANGELOG.md | 3 ++ reify.go | 24 ++++++++++++++ reify_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 10 ++++-- util.go | 9 +++++ 5 files changed, 136 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e45eb..3cf95d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Removed ### Fixed +- Fix int/uint to float type conversation. #68 +- Fix primitive type unpacking for variables expanded from environment variables + or strings read/created by config file parsers. #67 ## [0.3.6] diff --git a/reify.go b/reify.go index bc2fc2a..65a4a44 100644 --- a/reify.go +++ b/reify.go @@ -521,6 +521,13 @@ func doReifyPrimitive( } return v, nil + case isFloat(kind): + v, err := reifyFloat(opts, val, baseType) + if err != nil { + return v, err + } + return v, nil + case kind == reflect.Bool: v, err := reifyBool(opts, val, baseType) if err != nil { @@ -624,6 +631,23 @@ func reifyUint( return reflect.ValueOf(u).Convert(t), nil } +func reifyFloat( + opts fieldOptions, + val value, + t reflect.Type, +) (reflect.Value, Error) { + f, err := val.toFloat(opts.opts) + if err != nil { + return reflect.Value{}, raiseConversion(opts.opts, val, err, "float") + } + + tmp := reflect.Zero(t) + if tmp.OverflowFloat(f) { + return reflect.Value{}, raiseConversion(opts.opts, val, ErrOverflow, "float") + } + return reflect.ValueOf(f).Convert(t), nil +} + func reifyBool( opts fieldOptions, val value, diff --git a/reify_test.go b/reify_test.go index 205de71..a2ab4ad 100644 --- a/reify_test.go +++ b/reify_test.go @@ -144,6 +144,98 @@ func TestUnpackPrimitiveValues(t *testing.T) { } } +func TestUnpackPrimitivesValuesResolve(t *testing.T) { + tests := []interface{}{ + New(), + &map[string]interface{}{}, + map[string]interface{}{}, + node{}, + &node{}, + &struct { + B bool + I int + U uint + F float64 + S string + }{}, + &struct { + B interface{} + I interface{} + U interface{} + F interface{} + S interface{} + }{}, + &struct { + B *bool + I *int + U *uint + F *float64 + S *string + }{}, + } + + cfgOpts := []Option{ + VarExp, + Resolve(func(name string) (string, error) { + return map[string]string{ + "v_b": "true", + "v_i": "42", + "v_u": "23", + "v_f": "3.14", + "v_s": "string", + }[name], nil + }), + } + + c, _ := NewFrom(node{ + "b": "${v_b}", + "i": "${v_i}", + "u": "${v_u}", + "f": "${v_f}", + "s": "${v_s}", + }, cfgOpts...) + + for i, out := range tests { + t.Logf("test unpack primitives(%v) into: %v", i, out) + err := c.Unpack(out, cfgOpts...) + if err != nil { + t.Fatalf("failed to unpack: %v", err) + } + } + + // validate content by merging struct + for i, in := range tests { + t.Logf("test unpack primitives(%v) check: %v", i, in) + + c, err := NewFrom(in, cfgOpts...) + if err != nil { + t.Errorf("failed") + continue + } + + b, err := c.Bool("b", -1, cfgOpts...) + assert.NoError(t, err) + + i, err := c.Int("i", -1, cfgOpts...) + assert.NoError(t, err) + + u, err := c.Uint("u", -1, cfgOpts...) + assert.NoError(t, err) + + f, err := c.Float("f", -1, cfgOpts...) + assert.NoError(t, err) + + s, err := c.String("s", -1, cfgOpts...) + assert.NoError(t, err) + + assert.Equal(t, true, b) + assert.Equal(t, 42, int(i)) + assert.Equal(t, 23, int(u)) + assert.Equal(t, 3.14, f) + assert.Equal(t, "string", s) + } +} + func TestUnpackNested(t *testing.T) { var genSub = func(name string) *Config { s := New() diff --git a/types.go b/types.go index 4df7621..dd96a81 100644 --- a/types.go +++ b/types.go @@ -195,6 +195,7 @@ func (c *cfgBool) typ(*options) (typeInfo, error) { return typeInfo{"bo func (c *cfgInt) cpy(ctx context) value { return newInt(ctx, c.meta(), c.i) } func (c *cfgInt) toInt(*options) (int64, error) { return c.i, nil } +func (c *cfgInt) toFloat(*options) (float64, error) { return float64(c.i), nil } func (c *cfgInt) reflect(*options) (reflect.Value, error) { return reflect.ValueOf(c.i), nil } func (c *cfgInt) reify(*options) (interface{}, error) { return c.i, nil } func (c *cfgInt) toString(*options) (string, error) { return fmt.Sprintf("%d", c.i), nil } @@ -212,6 +213,7 @@ func (c *cfgUint) reify(*options) (interface{}, error) { return c.u, nil } func (c *cfgUint) toString(*options) (string, error) { return fmt.Sprintf("%d", c.u), nil } func (c *cfgUint) typ(*options) (typeInfo, error) { return typeInfo{"uint", tUint64}, nil } func (c *cfgUint) toUint(*options) (uint64, error) { return c.u, nil } +func (c *cfgUint) toFloat(*options) (float64, error) { return float64(c.u), nil } func (c *cfgUint) toInt(*options) (int64, error) { if c.u > math.MaxInt64 { return 0, ErrOverflow @@ -243,13 +245,17 @@ func (c *cfgFloat) toInt(*options) (int64, error) { return int64(c.f), nil } -func (c *cfgString) cpy(ctx context) value { return newString(ctx, c.meta(), c.s) } -func (c *cfgString) toString(*options) (string, error) { return c.s, nil } +func (c *cfgString) cpy(ctx context) value { return newString(ctx, c.meta(), c.s) } func (c *cfgString) reflect(*options) (reflect.Value, error) { return reflect.ValueOf(c.s), nil } func (c *cfgString) reify(*options) (interface{}, error) { return c.s, nil } func (c *cfgString) typ(*options) (typeInfo, error) { return typeInfo{"string", tString}, nil } +func (c *cfgString) toBool(*options) (bool, error) { return strconv.ParseBool(c.s) } +func (c *cfgString) toString(*options) (string, error) { return c.s, nil } +func (c *cfgString) toInt(*options) (int64, error) { return strconv.ParseInt(c.s, 0, 64) } +func (c *cfgString) toUint(*options) (uint64, error) { return strconv.ParseUint(c.s, 0, 64) } +func (c *cfgString) toFloat(*options) (float64, error) { return strconv.ParseFloat(c.s, 64) } func (c cfgSub) Context() context { return c.c.ctx } func (cfgSub) toBool(*options) (bool, error) { return false, ErrTypeMismatch } diff --git a/util.go b/util.go index e826160..cb80d83 100644 --- a/util.go +++ b/util.go @@ -123,6 +123,15 @@ func isUint(k reflect.Kind) bool { } } +func isFloat(k reflect.Kind) bool { + switch k { + case reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + func implementsUnpacker(v reflect.Value) (reflect.Value, bool) { for { if v.Type().Implements(tUnpacker) {