Skip to content

Commit a33d5a4

Browse files
committed
feat: add unmarshal to callback function
1 parent abe3fc2 commit a33d5a4

File tree

6 files changed

+218
-49
lines changed

6 files changed

+218
-49
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ type Record struct {
1212
Height float32 `flat:"-"` // ignored
1313
}
1414

15-
ch := make(chan Record)
15+
...
16+
17+
goflat.MarshalSliceToWriter[Record](ctx,inputCh,csvWriter,options)
1618

1719
...
1820

19-
goflat.MarshalSliceToWriter[Record](ctx,ch,csvWriter,options)
21+
goflat.UnmarshalToChan[Record](ctx,csvReader,options,outputCh)
22+
2023
```
2124

2225
Will result in:

helper_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package goflat_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/google/go-cmp/cmp"
9+
)
10+
11+
func assertChannel[T any](t *testing.T, ch <-chan T, expected []T, cmpOpts ...cmp.Option) {
12+
t.Helper()
13+
14+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
15+
var got []T
16+
17+
go func() {
18+
defer cancel()
19+
20+
for {
21+
select {
22+
case <-ctx.Done():
23+
return
24+
case v, ok := <-ch:
25+
if !ok {
26+
return
27+
}
28+
29+
got = append(got, v)
30+
}
31+
}
32+
}()
33+
34+
t.Cleanup(func() {
35+
<-ctx.Done()
36+
37+
if diff := cmp.Diff(expected, got, cmpOpts...); diff != "" {
38+
t.Errorf("(-expected,+got):\n%s", diff)
39+
}
40+
})
41+
}

marshal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func MarshalChannelToWriter[T any](ctx context.Context, inputCh <-chan T, writer
5353

5454
select {
5555
case <-ctx.Done():
56-
return ctx.Err() //nolint:wrapcheck // No need here.
56+
return context.Cause(ctx) //nolint:wrapcheck // Fine here.
5757
case value, channelHasValue = <-inputCh:
5858
}
5959

reflect.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ import (
77
"strings"
88
)
99

10+
// Options is used to configure the marshalling and unmarshalling processes.
11+
type Options struct {
12+
headersFromStruct bool
13+
// ErrorIfTaglessField causes goflat to error out if any struct field is
14+
// missing the `flat` tag.
15+
ErrorIfTaglessField bool
16+
// ErrorIfDuplicateHeaders causes goflat to error out if two struct fields
17+
// share the same `flat` tag value.
18+
ErrorIfDuplicateHeaders bool
19+
// ErrorIfMissingHeaders causes goflat to error out at unmarshalling time if
20+
// a header has no struct field with a corresponding `flat` tag.
21+
ErrorIfMissingHeaders bool
22+
// UnmarshalIgnoreEmpty causes the unmarshaller to skip any column which is
23+
// an empty string. This is useful for instance if you have integer values
24+
// and you are okay with empty string mapping to the zero value (0). For the
25+
// same reason this will cause booleans to be false if the column is empty.
26+
UnmarshalIgnoreEmpty bool
27+
}
28+
1029
type structFactory[T any] struct {
1130
structType reflect.Type
1231
pointer bool

unmarshal.go

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,6 @@ import (
1010
"golang.org/x/sync/errgroup"
1111
)
1212

13-
// Options is used to configure the marshalling and unmarshalling processes.
14-
type Options struct {
15-
headersFromStruct bool
16-
// ErrorIfTaglessField causes goflat to error out if any struct field is
17-
// missing the `flat` tag.
18-
ErrorIfTaglessField bool
19-
// ErrorIfDuplicateHeaders causes goflat to error out if two struct fields
20-
// share the same `flat` tag value.
21-
ErrorIfDuplicateHeaders bool
22-
// ErrorIfMissingHeaders causes goflat to error out at unmarshalling time if
23-
// a header has no struct field with a corresponding `flat` tag.
24-
ErrorIfMissingHeaders bool
25-
// UnmarshalIgnoreEmpty causes the unmarshaller to skip any column which is
26-
// an empty string. This is useful for instance if you have integer values
27-
// and you are okay with empty string mapping to the zero value (0). For the
28-
// same reason this will cause booleans to be false if the column is empty.
29-
UnmarshalIgnoreEmpty bool
30-
}
31-
3213
// Unmarshaller can be used to tell goflat to use custom logic to convert the
3314
// input string into the type itself.
3415
type Unmarshaller interface {
@@ -103,3 +84,31 @@ func UnmarshalToSlice[T any](ctx context.Context, reader *csv.Reader, opts Optio
10384

10485
return slice, nil
10586
}
87+
88+
// UnmarshalToCallback unamrshals a CSV file invoking a callback function on
89+
// each row.
90+
func UnmarshalToCallback[T any](ctx context.Context, reader *csv.Reader, opts Options, callback func(T) error) error {
91+
g, ctx := errgroup.WithContext(ctx) //nolint:varnamelen // Fine here.
92+
93+
ch := make(chan T) //nolint:varnamelen // Fine here.
94+
95+
g.Go(func() error {
96+
for v := range ch {
97+
if err := callback(v); err != nil {
98+
return fmt.Errorf("callback: %w", err)
99+
}
100+
}
101+
102+
return nil
103+
})
104+
105+
g.Go(func() error {
106+
return UnmarshalToChannel(ctx, reader, opts, ch)
107+
})
108+
109+
if err := g.Wait(); err != nil {
110+
return fmt.Errorf("wait: %w", err)
111+
}
112+
113+
return nil
114+
}

unmarshal_test.go

Lines changed: 124 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,24 @@ import (
44
"context"
55
"embed"
66
"testing"
7-
"time"
87

98
"github.com/google/go-cmp/cmp"
109

1110
"github.com/lzambarda/goflat"
1211
)
1312

1413
func TestUnmarshal(t *testing.T) {
15-
t.Run("error empty", testUnmarshalErrorEmpty)
14+
t.Run("error", testUnmarshalError)
1615
t.Run("success", testUnmarshalSuccess)
17-
t.Run("success ignore empty", testUnmarshalSuccessIgnoreEmpty)
18-
t.Run("success pointer", testUnmarshalSuccessPointer)
1916
}
2017

2118
//go:embed testdata
2219
var testdata embed.FS
2320

21+
func testUnmarshalError(t *testing.T) {
22+
t.Run("empty", testUnmarshalErrorEmpty)
23+
}
24+
2425
func testUnmarshalErrorEmpty(t *testing.T) {
2526
file, err := testdata.Open("testdata/unmarshal/success empty.csv")
2627
if err != nil {
@@ -57,6 +58,14 @@ func testUnmarshalErrorEmpty(t *testing.T) {
5758
}
5859

5960
func testUnmarshalSuccess(t *testing.T) {
61+
t.Run("full", testUnmarshalSuccessFull)
62+
t.Run("ignore empty", testUnmarshalSuccessIgnoreEmpty)
63+
t.Run("pointer", testUnmarshalSuccessPointer)
64+
t.Run("slice", testUnmarshalSuccessSlice)
65+
t.Run("callback", testUnmarshalSuccessCallback)
66+
}
67+
68+
func testUnmarshalSuccessFull(t *testing.T) {
6069
file, err := testdata.Open("testdata/unmarshal/success.csv")
6170
if err != nil {
6271
t.Fatalf("open test file: %v", err)
@@ -225,34 +234,122 @@ func testUnmarshalSuccessPointer(t *testing.T) {
225234
}
226235
}
227236

228-
func assertChannel[T any](t *testing.T, ch <-chan T, expected []T, cmpOpts ...cmp.Option) {
229-
t.Helper()
237+
func testUnmarshalSuccessSlice(t *testing.T) {
238+
file, err := testdata.Open("testdata/unmarshal/success.csv")
239+
if err != nil {
240+
t.Fatalf("open test file: %v", err)
241+
}
242+
243+
type record struct {
244+
FirstName string `flat:"first_name"`
245+
LastName string `flat:"last_name"`
246+
Age int `flat:"age"`
247+
Height float32 `flat:"height"`
248+
}
249+
250+
expected := []record{
251+
{
252+
FirstName: "Guybrush",
253+
LastName: "Threepwood",
254+
Age: 28,
255+
Height: 1.78,
256+
},
257+
{
258+
FirstName: "Elaine",
259+
LastName: "Marley",
260+
Age: 20,
261+
Height: 1.6,
262+
},
263+
{
264+
FirstName: "LeChuck",
265+
LastName: "",
266+
Age: 100,
267+
Height: 2.01,
268+
},
269+
}
270+
271+
ctx := context.Background()
272+
273+
csvReader, err := goflat.DetectReader(file)
274+
if err != nil {
275+
t.Fatalf("detect reader: %v", err)
276+
}
277+
278+
options := goflat.Options{
279+
ErrorIfTaglessField: true,
280+
ErrorIfDuplicateHeaders: true,
281+
ErrorIfMissingHeaders: true,
282+
}
283+
284+
got, err := goflat.UnmarshalToSlice[record](ctx, csvReader, options)
285+
if err != nil {
286+
t.Fatalf("unmarshal: %v", err)
287+
}
288+
289+
if diff := cmp.Diff(expected, got, cmp.AllowUnexported(record{})); diff != "" {
290+
t.Errorf("(-expected,+got):\n%s", diff)
291+
}
292+
}
293+
294+
func testUnmarshalSuccessCallback(t *testing.T) {
295+
file, err := testdata.Open("testdata/unmarshal/success.csv")
296+
if err != nil {
297+
t.Fatalf("open test file: %v", err)
298+
}
299+
300+
type record struct {
301+
FirstName string `flat:"first_name"`
302+
LastName string `flat:"last_name"`
303+
Age int `flat:"age"`
304+
Height float32 `flat:"height"`
305+
}
306+
307+
expected := []record{
308+
{
309+
FirstName: "Guybrush",
310+
LastName: "Threepwood",
311+
Age: 28,
312+
Height: 1.78,
313+
},
314+
{
315+
FirstName: "Elaine",
316+
LastName: "Marley",
317+
Age: 20,
318+
Height: 1.6,
319+
},
320+
{
321+
FirstName: "LeChuck",
322+
LastName: "",
323+
Age: 100,
324+
Height: 2.01,
325+
},
326+
}
230327

231-
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
232-
var got []T
328+
ctx := context.Background()
233329

234-
go func() {
235-
defer cancel()
330+
csvReader, err := goflat.DetectReader(file)
331+
if err != nil {
332+
t.Fatalf("detect reader: %v", err)
333+
}
236334

237-
for {
238-
select {
239-
case <-ctx.Done():
240-
return
241-
case v, ok := <-ch:
242-
if !ok {
243-
return
244-
}
335+
options := goflat.Options{
336+
ErrorIfTaglessField: true,
337+
ErrorIfDuplicateHeaders: true,
338+
ErrorIfMissingHeaders: true,
339+
}
245340

246-
got = append(got, v)
247-
}
248-
}
249-
}()
341+
var got []record
250342

251-
t.Cleanup(func() {
252-
<-ctx.Done()
343+
err = goflat.UnmarshalToCallback(ctx, csvReader, options, func(r record) error {
344+
got = append(got, r)
253345

254-
if diff := cmp.Diff(expected, got, cmpOpts...); diff != "" {
255-
t.Errorf("(-expected,+got):\n%s", diff)
256-
}
346+
return nil
257347
})
348+
if err != nil {
349+
t.Fatalf("unmarshal: %v", err)
350+
}
351+
352+
if diff := cmp.Diff(expected, got, cmp.AllowUnexported(record{})); diff != "" {
353+
t.Errorf("(-expected,+got):\n%s", diff)
354+
}
258355
}

0 commit comments

Comments
 (0)