From 986500e4511cc455ee78db24f140a244916cd3a9 Mon Sep 17 00:00:00 2001 From: Jacek Szwec Date: Sun, 29 Oct 2017 19:44:08 -0400 Subject: [PATCH] Add Unmarshal function --- csvutil.go | 56 +++++++++++++++++++++++++++++++++ csvutil_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ decoder.go | 6 ++-- error.go | 22 +++++++++++-- 4 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 csvutil.go create mode 100644 csvutil_test.go diff --git a/csvutil.go b/csvutil.go new file mode 100644 index 0000000..8c17ee7 --- /dev/null +++ b/csvutil.go @@ -0,0 +1,56 @@ +package csvutil + +import ( + "bytes" + "encoding/csv" + "io" + "reflect" +) + +// Unmarshal parses the CSV-encoded data and stores the result in the slice +// pointed to by v. If v is nil or not a pointer to a slice, Unmarshal returns +// an InvalidUnmarshalError. +// +// Unmarshal uses the std encoding/csv.Reader for parsing and csvutil.Decoder +// for populating the struct elements in the provided slice. For exact decoding +// rules look at the Decoder's documentation. +// +// The first line in data is treated as a header. Decoder will use it to map +// csv columns to struct's fields. +// +// In case of success the provided slice will be reinitialized and its content +// fully replaced with decoded data. +func Unmarshal(data []byte, v interface{}) error { + vv := reflect.ValueOf(v) + + if vv.Kind() != reflect.Ptr || vv.IsNil() { + return &InvalidUnmarshalError{Type: reflect.TypeOf(v)} + } + + if vv.Type().Elem().Kind() != reflect.Slice { + return &InvalidUnmarshalError{Type: vv.Type()} + } + + typ := vv.Type().Elem() + slice := reflect.MakeSlice(typ, 0, 2) + + dec, err := NewDecoder(csv.NewReader(bytes.NewReader(data))) + if err == io.EOF { + return nil + } else if err != nil { + return err + } + + for { + elem := reflect.New(typ.Elem()) + if err := dec.Decode(elem.Interface()); err == io.EOF { + break + } else if err != nil { + return err + } + slice = reflect.Append(slice, elem.Elem()) + } + + vv.Elem().Set(slice) + return nil +} diff --git a/csvutil_test.go b/csvutil_test.go new file mode 100644 index 0000000..1cc3ba5 --- /dev/null +++ b/csvutil_test.go @@ -0,0 +1,83 @@ +package csvutil + +import ( + "reflect" + "testing" +) + +func TestUnmarshal(t *testing.T) { + fixture := []struct { + desc string + src []byte + in interface{} + out interface{} + }{ + { + desc: "type with two records", + src: []byte("String,int\nstring1,1\nstring2,2"), + in: new([]TypeI), + out: &[]TypeI{ + {"string1", 1}, + {"string2", 2}, + }, + }, + { + desc: "pointer types with two records", + src: []byte("String,int\nstring1,1\nstring2,2"), + in: &[]*TypeI{}, + out: &[]*TypeI{ + {"string1", 1}, + {"string2", 2}, + }, + }, + { + desc: "header only", + src: []byte("String,int\n"), + in: &[]TypeI{}, + out: &[]TypeI{}, + }, + { + desc: "no data", + src: []byte(""), + in: &[]TypeI{}, + out: &[]TypeI{}, + }, + } + + for _, f := range fixture { + t.Run(f.desc, func(t *testing.T) { + if err := Unmarshal(f.src, f.in); err != nil { + t.Fatalf("want err=nil; got %v", err) + } + + if !reflect.DeepEqual(f.in, f.out) { + t.Errorf("want out=%v; got %v", f.out, f.in) + } + }) + } + + t.Run("test invalid arguments", func(t *testing.T) { + var fixtures = []struct { + desc string + v interface{} + expected string + }{ + {"nil interface", interface{}(nil), "csvutil: Unmarshal(nil)"}, + {"nil", nil, "csvutil: Unmarshal(nil)"}, + {"non pointer struct", struct{}{}, "csvutil: Unmarshal(non-pointer struct {})"}, + {"non-slice pointer", (*int)(nil), "csvutil: Unmarshal(non-slice pointer)"}, + } + + for _, f := range fixtures { + t.Run(f.desc, func(t *testing.T) { + err := Unmarshal([]byte(""), f.v) + if err == nil { + t.Fatalf("want err != nil") + } + if got := err.Error(); got != f.expected { + t.Errorf("want err=%s; got %s", f.expected, got) + } + }) + } + }) +} diff --git a/decoder.go b/decoder.go index 1e3d636..79fc20b 100644 --- a/decoder.go +++ b/decoder.go @@ -85,7 +85,7 @@ func NewDecoder(r Reader, header ...string) (dec *Decoder, err error) { // Decoder.Tag field. // // To Decode into a custom type v must implement csvutil.Unmarshaler or -// encoding.TextUnarshaler. +// encoding.TextUnmarshaler. // // Anonymous struct fields with tags are treated like normal fields and they // must implement csvutil.Unmarshaler or encoding.TextUnmarshaler. @@ -134,14 +134,14 @@ func (d *Decoder) Unused() (indexes []int) { func (d *Decoder) unmarshal(record []string, v interface{}) error { vv := reflect.ValueOf(v) if vv.Kind() != reflect.Ptr || vv.IsNil() { - return &InvalidUnmarshalError{Type: reflect.TypeOf(v)} + return &InvalidDecodeError{Type: reflect.TypeOf(v)} } elem := indirect(vv.Elem()) typ := elem.Type() if typ.Kind() != reflect.Struct { - return &InvalidUnmarshalError{Type: reflect.PtrTo(typ)} + return &InvalidDecodeError{Type: reflect.PtrTo(typ)} } for i := range d.used { diff --git a/error.go b/error.go index a7732cc..699c205 100644 --- a/error.go +++ b/error.go @@ -30,13 +30,13 @@ func (e *UnsupportedTypeError) Error() string { return "csvutil: unsupported type: " + e.Type.String() } -// An InvalidUnmarshalError describes an invalid argument passed to Decode. +// An InvalidDecodeError describes an invalid argument passed to Decode. // (The argument to Decode must be a non-nil pointer.) -type InvalidUnmarshalError struct { +type InvalidDecodeError struct { Type reflect.Type } -func (e *InvalidUnmarshalError) Error() string { +func (e *InvalidDecodeError) Error() string { if e.Type == nil { return "csvutil: Decode(nil)" } @@ -51,3 +51,19 @@ func (e *InvalidUnmarshalError) Error() string { return "csvutil: Decode(nil " + e.Type.String() + ")" } + +type InvalidUnmarshalError struct { + Type reflect.Type +} + +func (e *InvalidUnmarshalError) Error() string { + if e.Type == nil { + return "csvutil: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "csvutil: Unmarshal(non-pointer " + e.Type.String() + ")" + } + + return "csvutil: Unmarshal(non-slice pointer)" +}