From 679dadd066b3a2df0ea42cdc0975937086cc522c Mon Sep 17 00:00:00 2001 From: Alex Flint Date: Wed, 15 Feb 2017 17:42:57 -0800 Subject: [PATCH] based functions all working --- scalar.go | 154 +++++++++++++++++++++++++++++++++++++++++++++++++ scalar_test.go | 74 ++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 scalar.go create mode 100644 scalar_test.go diff --git a/scalar.go b/scalar.go new file mode 100644 index 0000000..663f143 --- /dev/null +++ b/scalar.go @@ -0,0 +1,154 @@ +// Package scalar parses strings into values of scalar type. + +package scalar + +import ( + "encoding" + "errors" + "fmt" + "net" + "net/mail" + "reflect" + "strconv" + "time" +) + +// The reflected form of some special types +var ( + textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem() + durationType = reflect.TypeOf(time.Duration(0)) + mailAddressType = reflect.TypeOf(mail.Address{}) + ipType = reflect.TypeOf(net.IP{}) + macType = reflect.TypeOf(net.HardwareAddr{}) +) + +var ( + errNotSettable = errors.New("value is not settable") + errPtrNotSettable = errors.New("value is a nil pointer and is not settable") +) + +// Parse assigns a value to v by parsing s. +func Parse(dest interface{}, s string) error { + return ParseValue(reflect.ValueOf(dest), s) +} + +// ParseValue assigns a value to v by parsing s. +func ParseValue(v reflect.Value, s string) error { + // If we have a nil pointer then allocate a new object + if v.Kind() == reflect.Ptr && v.IsNil() { + if !v.CanSet() { + return errPtrNotSettable + } + + v.Set(reflect.New(v.Type().Elem())) + } + + // If it implements encoding.TextUnmarshaler then use that + if scalar, ok := v.Interface().(encoding.TextUnmarshaler); ok { + return scalar.UnmarshalText([]byte(s)) + } + + // If we have a pointer then dereference it + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if !v.CanSet() { + return errNotSettable + } + + // Switch on concrete type + switch scalar := v.Interface(); scalar.(type) { + case time.Duration: + duration, err := time.ParseDuration(s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(duration)) + return nil + case mail.Address: + addr, err := mail.ParseAddress(s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(*addr)) + return nil + case net.IP: + ip := net.ParseIP(s) + if ip == nil { + return fmt.Errorf(`invalid IP address: "%s"`, s) + } + v.Set(reflect.ValueOf(ip)) + return nil + case net.HardwareAddr: + ip, err := net.ParseMAC(s) + if err != nil { + return err + } + v.Set(reflect.ValueOf(ip)) + return nil + } + + // Switch on kind so that we can handle derived types + switch v.Kind() { + case reflect.String: + v.SetString(s) + case reflect.Bool: + x, err := strconv.ParseBool(s) + if err != nil { + return err + } + v.SetBool(x) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + x, err := strconv.ParseInt(s, 10, v.Type().Bits()) + if err != nil { + return err + } + v.SetInt(x) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + x, err := strconv.ParseUint(s, 10, v.Type().Bits()) + if err != nil { + return err + } + v.SetUint(x) + case reflect.Float32, reflect.Float64: + x, err := strconv.ParseFloat(s, v.Type().Bits()) + if err != nil { + return err + } + v.SetFloat(x) + default: + return fmt.Errorf("cannot parse into %v", v.Type()) + } + return nil +} + +// CanParse returns true if the type can be parsed from a string. +func CanParse(t reflect.Type) bool { + // If it implements encoding.TextUnmarshaler then use that + if t.Implements(textUnmarshalerType) { + return true + } + + // If we have a pointer then dereference it + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + // Check for other special types + switch t { + case durationType, mailAddressType, ipType, macType: + return true + } + + // Fall back to checking the kind + switch t.Kind() { + case reflect.Bool: + return true + case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64: + return true + } + return false +} diff --git a/scalar_test.go b/scalar_test.go new file mode 100644 index 0000000..30df997 --- /dev/null +++ b/scalar_test.go @@ -0,0 +1,74 @@ +package scalar + +import ( + "net" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func assertParse(t *testing.T, expected interface{}, str string) { + v := reflect.New(reflect.TypeOf(expected)).Elem() + err := ParseValue(v, str) + if assert.NoError(t, err) { + assert.Equal(t, expected, v) + } + + ptr := reflect.New(reflect.PtrTo(reflect.TypeOf(expected))).Elem() + err = ParseValue(ptr, str) + if assert.NoError(t, err) { + assert.Equal(t, expected, ptr.Elem()) + } + + assert.True(t, CanParse(v.Type())) + assert.True(t, CanParse(ptr.Type())) +} + +func TestParseValue(t *testing.T) { + // strings + assertParse(t, "abc", "abc") + + // booleans + assertParse(t, true, "true") + assertParse(t, false, "false") + + // integers + assertParse(t, int(123), "123") + assertParse(t, int8(123), "123") + assertParse(t, int16(123), "123") + assertParse(t, int32(123), "123") + assertParse(t, int64(123), "123") + + // unsigned integers + assertParse(t, uint(123), "123") + assertParse(t, byte(123), "123") + assertParse(t, uint8(123), "123") + assertParse(t, uint16(123), "123") + assertParse(t, uint32(123), "123") + assertParse(t, uint64(123), "123") + assertParse(t, uintptr(123), "123") + assertParse(t, rune(123), "123") + + // floats + assertParse(t, float32(123), "123") + assertParse(t, float64(123), "123") + + // durations + assertParse(t, 3*time.Hour+15*time.Minute, "3h15m") + + // IP addresses + assertParse(t, net.IPv4(1, 2, 3, 4), "1.2.3.4") + + // MAC addresses + assertParse(t, net.HardwareAddr("\x01\x23\x45\x67\x89\xab"), "01:23:45:67:89:ab") +} + +func TestParse(t *testing.T) { + var v int + err := Parse(&v, "123") + require.NoError(t, err) + assert.Equal(t, 123, v) +}