Skip to content

Commit ed0442b

Browse files
author
Stefan Tudose
committed
better handle unmatching records and targets
1 parent b3af273 commit ed0442b

File tree

3 files changed

+124
-8
lines changed

3 files changed

+124
-8
lines changed

errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ var (
88
// ErrEOF is thrown if the EOF is reached by the Next method.
99
ErrEOF = errors.New("end of file reached")
1010

11+
ErrScanTargetsNotMatch = errors.New("the number of scan targets does not match the number of csv records")
12+
1113
errNilPtr = errors.New("destination is a nil pointer")
1214
errNotPtr = errors.New("destination not a pointer")
1315
)

parser.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ type Parser struct {
1414
lastErr error
1515
}
1616

17-
// ParserConfig has information for parser
17+
// ParserConfig allows to configure the parser.
1818
type ParserConfig struct {
19-
Comma rune
20-
IgnoreHeaders bool
19+
Comma rune
20+
IgnoreHeaders bool
21+
IgnoreUnmatchingFields bool
2122
}
2223

2324
type Decoder interface {
@@ -54,9 +55,11 @@ func newParser(reader io.Reader, config ParserConfig) (*Parser, error) {
5455
}
5556

5657
// Scan copies the values in the current row into the values pointed
57-
// at by dest. The number of values in dest can be different than the
58-
// number of values. In this case Scan will only fill the available
59-
// values.
58+
// at by dest.
59+
// With the defult behaviour, it will throw an error if the number of values in dest
60+
// is different from the number of values.
61+
// If the `IgnoreUnmatchingFields` flag is set, it will ignore the records and the
62+
// arguments that have no match.
6063
//
6164
// Scan converts columns read from the database into the following
6265
// common types:
@@ -72,12 +75,23 @@ func newParser(reader io.Reader, config ParserConfig) (*Parser, error) {
7275
//
7376
func (p *Parser) Scan(dest ...interface{}) error {
7477
if p.currentRowValues == nil {
75-
return errors.New("Scan called without calling Next")
78+
return errors.New("scan called without calling Next")
79+
}
80+
if !p.config.IgnoreUnmatchingFields && len(p.currentRowValues) != len(dest) {
81+
return fmt.Errorf("%w: got %d scan targets and %d records",
82+
ErrScanTargetsNotMatch,
83+
len(dest),
84+
len(p.currentRowValues),
85+
)
7686
}
7787
for i, val := range p.currentRowValues {
88+
if i >= len(dest) {
89+
// ignore the remaining records as they have no scan target
90+
break
91+
}
7892
err := convertAssignValue(dest[i], val)
7993
if err != nil {
80-
return fmt.Errorf(`Scan error on value index %d: %v`, i, err)
94+
return fmt.Errorf("scan error on value index %d: %v", i, err)
8195
}
8296
}
8397
return nil

parser_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package csvdecoder
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestParserIgnoreUnmatchingFields(t *testing.T) {
10+
var strVal string
11+
var intVal int
12+
var anotherIntVal int
13+
14+
for _, tc := range []struct {
15+
name string
16+
config ParserConfig
17+
data string
18+
scanTargets []interface{}
19+
expectedError error
20+
}{
21+
{
22+
name: "should work when numbers match and flag is false",
23+
config: ParserConfig{
24+
IgnoreUnmatchingFields: false,
25+
},
26+
data: "rec,2\n",
27+
scanTargets: []interface{}{&strVal, &intVal},
28+
expectedError: nil,
29+
},
30+
{
31+
name: "should work when numbers match and flag is true",
32+
config: ParserConfig{
33+
IgnoreUnmatchingFields: true,
34+
},
35+
data: "rec,2\n",
36+
scanTargets: []interface{}{&strVal, &intVal},
37+
expectedError: nil,
38+
},
39+
{
40+
name: "should work when numbers match with default config",
41+
config: ParserConfig{},
42+
data: "rec,2\n",
43+
scanTargets: []interface{}{&strVal, &intVal},
44+
expectedError: nil,
45+
},
46+
{
47+
name: "should work with more targets when the flag is true",
48+
config: ParserConfig{
49+
IgnoreUnmatchingFields: true,
50+
},
51+
data: "rec,2\n",
52+
scanTargets: []interface{}{&strVal, &intVal, &anotherIntVal},
53+
expectedError: nil,
54+
},
55+
{
56+
name: "should work with more records when the flag is true",
57+
config: ParserConfig{
58+
IgnoreUnmatchingFields: true,
59+
},
60+
data: "rec,2\n",
61+
scanTargets: []interface{}{&strVal},
62+
expectedError: nil,
63+
},
64+
{
65+
name: "should fail with more targets when the flag is false",
66+
config: ParserConfig{
67+
IgnoreUnmatchingFields: false,
68+
},
69+
data: "rec,2\n",
70+
scanTargets: []interface{}{&strVal, &intVal, &anotherIntVal},
71+
expectedError: ErrScanTargetsNotMatch,
72+
},
73+
{
74+
name: "should fail with more records when the flag is false",
75+
config: ParserConfig{
76+
IgnoreUnmatchingFields: false,
77+
},
78+
data: "rec,2\n",
79+
scanTargets: []interface{}{&strVal},
80+
expectedError: ErrScanTargetsNotMatch,
81+
},
82+
} {
83+
tc := tc
84+
t.Run(tc.name, func(t *testing.T) {
85+
parser, err := NewParserWithConfig(strings.NewReader(tc.data), tc.config)
86+
if err != nil {
87+
t.Fatalf("could not create parser: %w", err)
88+
}
89+
90+
for parser.Next() {
91+
if err := parser.Scan(tc.scanTargets...); !errors.Is(err, tc.expectedError) {
92+
t.Errorf("expected '%s', got '%v'", tc.expectedError, err)
93+
}
94+
}
95+
if parser.Err() != nil {
96+
t.Error(err)
97+
}
98+
})
99+
}
100+
}

0 commit comments

Comments
 (0)