From 4ef24bfd38fd12b7760ae1a816f9f8b5e00a8d08 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 15 May 2014 14:56:55 -0400 Subject: [PATCH] more tests, handle unexported and tagged fields moar better --- decode.go | 5 +-- decode_test.go | 8 ++-- encode.go | 57 ++++++++++++++++++++++------- encode_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 21 deletions(-) diff --git a/decode.go b/decode.go index 9526f6b..bc8f644 100644 --- a/decode.go +++ b/decode.go @@ -31,13 +31,12 @@ func NewDecoder(r io.Reader) Decoder { // DecodeNext reads the next CSV-encoded value from its input and stores it in the value pointed to by v. func (d *decoder) DecodeNext(v interface{}) error { - rv := reflect.ValueOf(v) - // v is nil, skip this line and proceed. - if rv.Kind() == reflect.Invalid { + if v == nil { _, err := d.read() return err } + rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr { return errors.New("must be pointer") } diff --git a/decode_test.go b/decode_test.go index bbb5087..904cac0 100644 --- a/decode_test.go +++ b/decode_test.go @@ -25,15 +25,15 @@ d,e,f }, { // Rows that only have partial data are only partially filled. data: `Foo,Bar,Baz -a,, -,b, +a,"","" +"",b,"" `, out: []row{{"a", "", ""}, {"", "b", ""}}, }, { // Rows that don't define all the columns are partially filled. data: `Foo,Bar -a, -,b +a,"" +"",b `, out: []row{{"a", "", ""}, {"", "b", ""}}, }, { diff --git a/encode.go b/encode.go index cbffa24..30ba73e 100644 --- a/encode.go +++ b/encode.go @@ -14,7 +14,7 @@ type Encoder interface { type encoder struct { w csv.Writer - hw bool + hm map[string]int } // NewEncoder returns an encoder that writes to w. @@ -24,52 +24,81 @@ func NewEncoder(w io.Writer) Encoder { // EncodeNext writes the CSV encoding of v to the stream. func (e *encoder) EncodeNext(v interface{}) error { + if v == nil { + return nil + } + t := reflect.ValueOf(v).Type() - if !e.hw { - headers := make([]string, t.NumField()) + if e.hm == nil { + e.hm = make(map[string]int) + headers := []string{} for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.PkgPath != "" { // Filter unexported fields continue } - h := f.Name + n := f.Name if f.Tag.Get("csv") != "" { - h = f.Tag.Get("csv") + n = f.Tag.Get("csv") + if n == "-" { + continue + } } - headers[i] = h + headers = append(headers, n) + e.hm[n] = i } if err := e.w.Write(headers); err != nil { return err } - e.hw = true } rv := reflect.ValueOf(v) - row := make([]string, t.NumField()) + row := []string{} + add := false // Whether there has been a row to write in this call. for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.PkgPath != "" { // Filter unexported fields continue } + n := f.Name + if f.Tag.Get("csv") != "" { + n = f.Tag.Get("csv") + } + + fi, ok := e.hm[n] + if !ok { + // Unmapped header value + continue + } + + // Increase the row size to fit the new row. + for fi >= len(row) { + row = append(row, "") + } + + add = true vf := rv.Field(i) switch vf.Kind() { case reflect.String: - row[i] = vf.String() + row[fi] = vf.String() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - row[i] = fmt.Sprintf("%d", vf.Int()) + row[fi] = fmt.Sprintf("%d", vf.Int()) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - row[i] = fmt.Sprintf("%d", vf.Uint()) + row[fi] = fmt.Sprintf("%d", vf.Uint()) case reflect.Float64: - row[i] = fmt.Sprintf("%f", vf.Float()) + row[fi] = fmt.Sprintf("%f", vf.Float()) case reflect.Bool: - row[i] = fmt.Sprintf("%t", vf.Bool()) + row[fi] = fmt.Sprintf("%t", vf.Bool()) default: return fmt.Errorf("can't decode type %v", f.Type) } } + if !add { + return nil + } if err := e.w.Write(row); err != nil { return err } e.w.Flush() - return nil + return e.w.Error() } diff --git a/encode_test.go b/encode_test.go index 4b4b487..74ace78 100644 --- a/encode_test.go +++ b/encode_test.go @@ -18,6 +18,18 @@ func TestEncodeNext(t *testing.T) { `Foo,Bar,Baz a,b,c d,e,f +`, + }, { + []row{{"a", "", ""}, {"", "b", ""}}, + `Foo,Bar,Baz +a,"","" +"",b,"" +`, + }, { + []row{{"a", "", ""}, {"", "b", ""}}, + `Foo,Bar,Baz +a,"","" +"",b,"" `, }} { var buf bytes.Buffer @@ -33,3 +45,90 @@ d,e,f } } } + +func TestEncode_Unexported(t *testing.T) { + r := struct { + Exported, unexported string + }{"a", "b"} + var buf bytes.Buffer + if err := NewEncoder(&buf).EncodeNext(r); err != nil { + t.Errorf("unexpected error: %v", err) + } + exp := `Exported +a +` + got := buf.String() + if got != exp { + t.Errorf("unexpected result, got %s, want %s", got, exp) + } +} + +func TestEncode_Tags(t *testing.T) { + type row struct { + Foo string `csv:"renamed_foo"` + Bar string + Ignored string `csv:"-"` + } + var buf bytes.Buffer + if err := NewEncoder(&buf).EncodeNext(row{"a", "b", "c"}); err != nil { + t.Errorf("unexpected error: %v", err) + } + exp := `renamed_foo,Bar +a,b +` + got := buf.String() + if got != exp { + t.Errorf("unexpected result, got %s, want %s", got, exp) + } +} + +func TestEncode_NonStrings(t *testing.T) { + r := struct { + Int int + Int64 int64 + Uint64 uint64 + Float64 float64 + Bool bool + }{123, -123456789, 123456789, 123.456, true} + var buf bytes.Buffer + if err := NewEncoder(&buf).EncodeNext(r); err != nil { + t.Errorf("unexpected error: %v", err) + } + exp := `Int,Int64,Uint64,Float64,Bool +123,-123456789,123456789,123.456000,true +` + got := buf.String() + if got != exp { + t.Errorf("unexpected result, got %s, want %s", got, exp) + } +} + +func TestEncodeDifferent(t *testing.T) { + type row1 struct { + Foo, Bar string + } + type row2 struct { + Baz string + } + type row3 struct { + Bar, Baz string + } + var buf bytes.Buffer + e := NewEncoder(&buf) + // Headers are taken from the fields in the first call to EncodeNext. + // Further calls add whatever fields they can, and if no fields are + // shared then the row is not written. + for _, r := range []interface{}{row1{"foo", "bar"}, row2{"baz"}, row3{"bar", "baz"}} { + if err := e.EncodeNext(r); err != nil { + t.Errorf("unexpected error: %v", err) + } + } + exp := `Foo,Bar +foo,bar +"",bar +` + got := buf.String() + if got != exp { + t.Errorf("unexpected result, got %s, want %s", got, exp) + } +}