Compare commits

..

3 Commits

Author SHA1 Message Date
Alex Flint 1f90c779f2 put back call to Addr() 2021-04-19 22:32:47 -07:00
Alex Flint a081287ba6 drop extraneous log 2021-04-19 22:27:42 -07:00
Alex Flint 3a035a19bd deal with chan, map, and slice types that implement TextUnmarshaler 2021-04-19 22:23:18 -07:00
8 changed files with 193 additions and 30 deletions

View File

@ -15,12 +15,12 @@ jobs:
strategy:
fail-fast: false
matrix:
go: ['1.17', '1.18', '1.19']
go: ['1.13', '1.14', '1.15', '1.16']
steps:
- id: go
name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go }}

2
.gitignore vendored
View File

@ -22,5 +22,3 @@ _testmain.go
*.exe
*.test
*.prof
go.*

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: go
go:
- tip
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

View File

@ -1,10 +0,0 @@
all:
@echo
@echo
test:
redomod:
rm -f go.*
GO111MODULE= go mod init
GO111MODULE= go mod tidy

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/alexflint/go-scalar
go 1.15
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

View File

@ -8,7 +8,6 @@ import (
"fmt"
"net"
"net/mail"
"net/url"
"reflect"
"strconv"
"time"
@ -20,7 +19,6 @@ var (
durationType = reflect.TypeOf(time.Duration(0))
mailAddressType = reflect.TypeOf(mail.Address{})
macType = reflect.TypeOf(net.HardwareAddr{})
urlType = reflect.TypeOf(url.URL{})
)
var (
@ -33,6 +31,40 @@ func Parse(dest interface{}, s string) error {
return ParseValue(reflect.ValueOf(dest), s)
}
func parseAsTextUnmarshaler(v reflect.Value, s string) (bool, error) {
t := v.Type()
if !t.Implements(textUnmarshalerType) {
return false, nil
}
if v.IsNil() && v.CanSet() {
switch t.Kind() {
case reflect.Ptr:
v.Set(reflect.New(v.Type().Elem()))
case reflect.Slice:
v.Set(reflect.MakeSlice(t, 0, 0))
case reflect.Map:
v.Set(reflect.MakeMap(t))
case reflect.Chan:
v.Set(reflect.MakeChan(t, 0))
}
}
if !v.IsNil() && t.Kind() == reflect.Ptr {
switch t.Elem().Kind() {
case reflect.Slice:
v.Elem().Set(reflect.MakeSlice(t.Elem(), 0, 0))
case reflect.Map:
v.Elem().Set(reflect.MakeMap(t.Elem()))
case reflect.Chan:
v.Elem().Set(reflect.MakeChan(t.Elem(), 0))
}
}
err := v.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(s))
return true, err
}
// 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
@ -45,14 +77,15 @@ func ParseValue(v reflect.Value, s string) error {
}
// If it implements encoding.TextUnmarshaler then use that
if scalar, ok := v.Interface().(encoding.TextUnmarshaler); ok {
return scalar.UnmarshalText([]byte(s))
if matched, err := parseAsTextUnmarshaler(v, s); matched {
return err
}
// If it's a value instead of a pointer, check that we can unmarshal it
// via TextUnmarshaler as well
if v.CanAddr() {
if scalar, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok {
return scalar.UnmarshalText([]byte(s))
if matched, err := parseAsTextUnmarshaler(v.Addr(), s); matched {
return err
}
}
@ -88,13 +121,6 @@ func ParseValue(v reflect.Value, s string) error {
}
v.Set(reflect.ValueOf(ip))
return nil
case url.URL:
url, err := url.Parse(s)
if err != nil {
return err
}
v.Set(reflect.ValueOf(*url))
return nil
}
// Switch on kind so that we can handle derived types
@ -108,13 +134,13 @@ func ParseValue(v reflect.Value, s string) error {
}
v.SetBool(x)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x, err := strconv.ParseInt(s, 0, v.Type().Bits())
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, 0, v.Type().Bits())
x, err := strconv.ParseUint(s, 10, v.Type().Bits())
if err != nil {
return err
}
@ -145,7 +171,7 @@ func CanParse(t reflect.Type) bool {
// Check for other special types
switch t {
case durationType, mailAddressType, macType, urlType:
case durationType, mailAddressType, macType:
return true
}

125
scalar_test.go Normal file
View File

@ -0,0 +1,125 @@
package scalar
import (
"net"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type textUnmarshaler struct {
val int
}
func (f *textUnmarshaler) UnmarshalText(b []byte) error {
f.val = len(b)
return nil
}
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.Interface())
}
ptr := reflect.New(reflect.PtrTo(reflect.TypeOf(expected))).Elem()
err = ParseValue(ptr, str)
if assert.NoError(t, err) {
assert.Equal(t, expected, ptr.Elem().Interface())
}
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")
// MAC addresses
assertParse(t, net.HardwareAddr("\x01\x23\x45\x67\x89\xab"), "01:23:45:67:89:ab")
// custom text unmarshaler
assertParse(t, textUnmarshaler{3}, "abc")
}
func TestParse(t *testing.T) {
var v int
err := Parse(&v, "123")
require.NoError(t, err)
assert.Equal(t, 123, v)
}
type sliceUnmarshaler []int
func (sliceUnmarshaler) UnmarshalText(b []byte) error {
return nil
}
type mapUnmarshaler map[string]string
func (m mapUnmarshaler) UnmarshalText(b []byte) error {
m["a"] = string(b)
return nil
}
type chanUnmarshaler chan string
func (ch chanUnmarshaler) UnmarshalText(b []byte) error {
return nil
}
func TestParseReferenceTypes(t *testing.T) {
var err error
var s sliceUnmarshaler
err = Parse(&s, "test1")
require.NoError(t, err)
var m mapUnmarshaler
err = Parse(&m, "test2")
require.NoError(t, err)
assert.Equal(t, "test2", m["a"])
var c chanUnmarshaler
err = Parse(&c, "test3")
require.NoError(t, err)
}