1
0
Fork 0
mirror of https://github.com/imjasonh/csvstruct synced 2026-07-08 02:55:12 +00:00

more tests, handle unexported and tagged fields moar better

This commit is contained in:
Jason Hall 2014-05-15 14:56:55 -04:00
parent 56e5f3185f
commit 4ef24bfd38
4 changed files with 148 additions and 21 deletions

View file

@ -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. // 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 { func (d *decoder) DecodeNext(v interface{}) error {
rv := reflect.ValueOf(v)
// v is nil, skip this line and proceed. // v is nil, skip this line and proceed.
if rv.Kind() == reflect.Invalid { if v == nil {
_, err := d.read() _, err := d.read()
return err return err
} }
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr { if rv.Kind() != reflect.Ptr {
return errors.New("must be pointer") return errors.New("must be pointer")
} }

View file

@ -25,15 +25,15 @@ d,e,f
}, { }, {
// Rows that only have partial data are only partially filled. // Rows that only have partial data are only partially filled.
data: `Foo,Bar,Baz data: `Foo,Bar,Baz
a,, a,"",""
,b, "",b,""
`, `,
out: []row{{"a", "", ""}, {"", "b", ""}}, out: []row{{"a", "", ""}, {"", "b", ""}},
}, { }, {
// Rows that don't define all the columns are partially filled. // Rows that don't define all the columns are partially filled.
data: `Foo,Bar data: `Foo,Bar
a, a,""
,b "",b
`, `,
out: []row{{"a", "", ""}, {"", "b", ""}}, out: []row{{"a", "", ""}, {"", "b", ""}},
}, { }, {

View file

@ -14,7 +14,7 @@ type Encoder interface {
type encoder struct { type encoder struct {
w csv.Writer w csv.Writer
hw bool hm map[string]int
} }
// NewEncoder returns an encoder that writes to w. // 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. // EncodeNext writes the CSV encoding of v to the stream.
func (e *encoder) EncodeNext(v interface{}) error { func (e *encoder) EncodeNext(v interface{}) error {
if v == nil {
return nil
}
t := reflect.ValueOf(v).Type() t := reflect.ValueOf(v).Type()
if !e.hw { if e.hm == nil {
headers := make([]string, t.NumField()) e.hm = make(map[string]int)
headers := []string{}
for i := 0; i < t.NumField(); i++ { for i := 0; i < t.NumField(); i++ {
f := t.Field(i) f := t.Field(i)
if f.PkgPath != "" { // Filter unexported fields if f.PkgPath != "" { // Filter unexported fields
continue continue
} }
h := f.Name n := f.Name
if f.Tag.Get("csv") != "" { 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 { if err := e.w.Write(headers); err != nil {
return err return err
} }
e.hw = true
} }
rv := reflect.ValueOf(v) 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++ { for i := 0; i < t.NumField(); i++ {
f := t.Field(i) f := t.Field(i)
if f.PkgPath != "" { // Filter unexported fields if f.PkgPath != "" { // Filter unexported fields
continue 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) vf := rv.Field(i)
switch vf.Kind() { switch vf.Kind() {
case reflect.String: case reflect.String:
row[i] = vf.String() row[fi] = vf.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 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: 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: case reflect.Float64:
row[i] = fmt.Sprintf("%f", vf.Float()) row[fi] = fmt.Sprintf("%f", vf.Float())
case reflect.Bool: case reflect.Bool:
row[i] = fmt.Sprintf("%t", vf.Bool()) row[fi] = fmt.Sprintf("%t", vf.Bool())
default: default:
return fmt.Errorf("can't decode type %v", f.Type) return fmt.Errorf("can't decode type %v", f.Type)
} }
} }
if !add {
return nil
}
if err := e.w.Write(row); err != nil { if err := e.w.Write(row); err != nil {
return err return err
} }
e.w.Flush() e.w.Flush()
return nil return e.w.Error()
} }

View file

@ -18,6 +18,18 @@ func TestEncodeNext(t *testing.T) {
`Foo,Bar,Baz `Foo,Bar,Baz
a,b,c a,b,c
d,e,f d,e,f
`,
}, {
[]row{{"a", "", ""}, {"", "b", ""}},
`Foo,Bar,Baz
a,"",""
"",b,""
`,
}, {
[]row{{"a", "", ""}, {"", "b", ""}},
`Foo,Bar,Baz
a,"",""
"",b,""
`, `,
}} { }} {
var buf bytes.Buffer 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)
}
}