Skip to content

Commit

Permalink
Add Unmarshal function
Browse files Browse the repository at this point in the history
  • Loading branch information
jszwec committed Oct 29, 2017
1 parent 695b3b2 commit 986500e
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 6 deletions.
56 changes: 56 additions & 0 deletions csvutil.go
Original file line number Diff line number Diff line change
@@ -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
}
83 changes: 83 additions & 0 deletions csvutil_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
})
}
6 changes: 3 additions & 3 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 19 additions & 3 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
}
Expand All @@ -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)"
}

0 comments on commit 986500e

Please sign in to comment.