Skip to content

Commit

Permalink
Add Decoder option DisallowMissingColumns
Browse files Browse the repository at this point in the history
Closes #33
  • Loading branch information
jszwec committed Feb 6, 2021
1 parent 54d7adb commit ef4f3de
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 4 deletions.
25 changes: 21 additions & 4 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ type Decoder struct {
// options (Default: 'csv').
Tag string

// If true, Decoder will return a MissingColumnsError if it discovers
// that any of the columns are missing. This means that a CSV input
// will be required to contain all columns that were defined in the
// provided struct.
DisallowMissingColumns bool

// If not nil, Map is a function that is called for each field in the csv
// record before decoding the data. It allows mapping certain string values
// for specific columns or types to a known format. Decoder calls Map with
Expand Down Expand Up @@ -393,13 +399,18 @@ func (d *Decoder) fields(k typeKey) ([]decField, error) {
return d.cache, nil
}

fields := cachedFields(k)
decFields := make([]decField, 0, len(fields))
used := make([]bool, len(d.header))

var (
fields = cachedFields(k)
decFields = make([]decField, 0, len(fields))
used = make([]bool, len(d.header))
missingCols []string
)
for _, f := range fields {
i, ok := d.hmap[f.name]
if !ok {
if d.DisallowMissingColumns {
missingCols = append(missingCols, f.name)
}
continue
}

Expand Down Expand Up @@ -427,6 +438,12 @@ func (d *Decoder) fields(k typeKey) ([]decField, error) {
used[i] = true
}

if len(missingCols) > 0 {
return nil, &MissingColumnsError{
Columns: missingCols,
}
}

d.unused = d.unused[:0]
for i, b := range used {
if !b {
Expand Down
93 changes: 93 additions & 0 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,99 @@ string,"{""key"":""value""}"
}
})

t.Run("decode with disallow missing columns", func(t *testing.T) {
type Type struct {
String string
Int int
Float float64
}

t.Run("all present", func(t *testing.T) {
dec, err := NewDecoder(NewReader(
[]string{"String", "Int", "Float"},
[]string{"lol", "1", "2.0"},
))
if err != nil {
t.Fatal(err)
}
dec.DisallowMissingColumns = true

var tt Type
if err := dec.Decode(&tt); err != nil {
t.Fatalf("expected err to be nil; got %v", err)
}

if expected := (Type{"lol", 1, 2}); !reflect.DeepEqual(tt, expected) {
t.Errorf("want=%v; got %v", expected, tt)
}
})

fixtures := []struct {
desc string
recs [][]string
missingCols []string
msg string
}{
{
desc: "one missing",
recs: [][]string{
{"String", "Int"},
{"lol", "1"},
},
missingCols: []string{"Float"},
msg: `csvutil: missing columns: "Float"`,
},
{
desc: "two missing",
recs: [][]string{
{"String"},
{"lol"},
},
missingCols: []string{"Int", "Float"},
msg: `csvutil: missing columns: "Int", "Float"`,
},
{
desc: "all missing",
recs: [][]string{
{"w00t"},
{"lol"},
},
missingCols: []string{"String", "Int", "Float"},
msg: `csvutil: missing columns: "String", "Int", "Float"`,
},
}

for _, f := range fixtures {
t.Run(f.desc, func(t *testing.T) {
dec, err := NewDecoder(NewReader(f.recs...))
if err != nil {
t.Fatal(err)
}
dec.DisallowMissingColumns = true

var tt Type
err = dec.Decode(&tt)

if err == nil {
t.Fatal("expected err != nil")
}

mcerr, ok := err.(*MissingColumnsError)
if !ok {
t.Fatalf("expected err to be of *MissingColumnErr; got %[1]T (%[1]v)", err)
}

if !reflect.DeepEqual(mcerr.Columns, f.missingCols) {
t.Errorf("expected missing columns to be %v; got %v", f.missingCols, mcerr.Columns)
}

if err.Error() != f.msg {
t.Errorf("expected err message to be %q; got %q", f.msg, err.Error())
}
})
}
})

t.Run("invalid unmarshal tests", func(t *testing.T) {
var fixtures = []struct {
v interface{}
Expand Down
19 changes: 19 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package csvutil

import (
"bytes"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -130,3 +131,21 @@ func (e *MarshalerError) Error() string {
func errPtrUnexportedStruct(typ reflect.Type) error {
return fmt.Errorf("csvutil: cannot decode into a pointer to unexported struct: %s", typ)
}

// MissingColumnsError is returned by Decoder only when DisallowMissingColumns
// option was set to true. It contains a list of all missing columns.
type MissingColumnsError struct {
Columns []string
}

func (e *MissingColumnsError) Error() string {
var b bytes.Buffer
b.WriteString("csvutil: missing columns: ")
for i, c := range e.Columns {
if i > 0 {
b.WriteString(", ")
}
fmt.Fprintf(&b, "%q", c)
}
return b.String()
}

0 comments on commit ef4f3de

Please sign in to comment.