From 9f70be16a66a66a9c157d4f5272664d85dcc2121 Mon Sep 17 00:00:00 2001 From: Luca Zambarda Date: Fri, 9 Feb 2024 13:04:27 +0000 Subject: [PATCH 1/3] add generics to chan and callback-using functions --- csv.go | 143 +++++++++++++++++++++------------------------------- csv_test.go | 6 +-- encode.go | 6 +-- go.mod | 2 +- 4 files changed, 63 insertions(+), 94 deletions(-) diff --git a/csv.go b/csv.go index e34819b..2ef9158 100644 --- a/csv.go +++ b/csv.go @@ -162,12 +162,12 @@ func MarshalWithoutHeaders(in interface{}, out io.Writer) (err error) { } // MarshalChan returns the CSV read from the channel. -func MarshalChan(c <-chan interface{}, out CSVWriter) error { +func MarshalChan[T any](c <-chan T, out CSVWriter) error { return writeFromChan(out, c, false) } // MarshalChanWithoutHeaders returns the CSV read from the channel. -func MarshalChanWithoutHeaders(c <-chan interface{}, out CSVWriter) error { +func MarshalChanWithoutHeaders[T any](c <-chan T, out CSVWriter) error { return writeFromChan(out, c, true) } @@ -278,15 +278,15 @@ func UnmarshalCSVToMap(in CSVReader, out interface{}) error { // UnmarshalToChan parses the CSV from the reader and send each value in the chan c. // The channel must have a concrete type. -func UnmarshalToChan(in io.Reader, c interface{}) error { +func UnmarshalToChan[T any](in io.Reader, c chan<- T) error { if c == nil { return fmt.Errorf("goscv: channel is %v", c) } return readEach(newSimpleDecoderFromReader(in), nil, c) } -// UnmarshalToChanWithErrorHandler parses the CSV from the reader in the interface. -func UnmarshalToChanWithErrorHandler(in io.Reader, errorHandler ErrorHandler, c interface{}) error { +// UnmarshalToChanWithErrorHandler parses the CSV from the reader in the channel. +func UnmarshalToChanWithErrorHandler[T any](in io.Reader, errorHandler ErrorHandler, c chan<- T) error { if c == nil { return fmt.Errorf("goscv: channel is %v", c) } @@ -294,8 +294,7 @@ func UnmarshalToChanWithErrorHandler(in io.Reader, errorHandler ErrorHandler, c } // UnmarshalToChanWithoutHeaders parses the CSV from the reader and send each value in the chan c. -// The channel must have a concrete type. -func UnmarshalToChanWithoutHeaders(in io.Reader, c interface{}) error { +func UnmarshalToChanWithoutHeaders[T any](in io.Reader, c chan<- T) error { if c == nil { return fmt.Errorf("goscv: channel is %v", c) } @@ -304,37 +303,32 @@ func UnmarshalToChanWithoutHeaders(in io.Reader, c interface{}) error { // UnmarshalDecoderToChan parses the CSV from the decoder and send each value in the chan c. // The channel must have a concrete type. -func UnmarshalDecoderToChan(in SimpleDecoder, c interface{}) error { +func UnmarshalDecoderToChan[T any](in SimpleDecoder, c chan<- T) error { if c == nil { return fmt.Errorf("goscv: channel is %v", c) } return readEach(in, nil, c) } -// UnmarshalStringToChan parses the CSV from the string and send each value in the chan c. -// The channel must have a concrete type. -func UnmarshalStringToChan(in string, c interface{}) error { +// UnmarshalStringToChan parses the CSV from the string and send each value in +// the chan c. +func UnmarshalStringToChan[T any](in string, c chan<- T) error { return UnmarshalToChan(strings.NewReader(in), c) } -// UnmarshalBytesToChan parses the CSV from the bytes and send each value in the chan c. -// The channel must have a concrete type. -func UnmarshalBytesToChan(in []byte, c interface{}) error { +// UnmarshalBytesToChan parses the CSV from the bytes and send each value in the +// chan c. +func UnmarshalBytesToChan[T any](in []byte, c chan<- T) error { return UnmarshalToChan(bytes.NewReader(in), c) } -// UnmarshalToCallback parses the CSV from the reader and send each value to the given func f. -// The func must look like func(Struct). -func UnmarshalToCallback(in io.Reader, f interface{}) error { - valueFunc := reflect.ValueOf(f) - t := reflect.TypeOf(f) - if t.NumIn() != 1 { - return fmt.Errorf("the given function must have exactly one parameter") - } +// UnmarshalToCallback parses the CSV from the reader and send each value to the +// given func callback. +func UnmarshalToCallback[T any](in io.Reader, callback func(T) error) error { cerr := make(chan error) - c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0) + c := make(chan T) go func() { - cerr <- UnmarshalToChan(in, c.Interface()) + cerr <- UnmarshalToChan(in, c) }() for { select { @@ -342,33 +336,24 @@ func UnmarshalToCallback(in io.Reader, f interface{}) error { return err default: } - v, notClosed := c.Recv() - if !notClosed || v.Interface() == nil { + v, notClosed := <-c + if !notClosed { break } - callResults := valueFunc.Call([]reflect.Value{v}) - // if last returned value from Call() is an error, return it - if len(callResults) > 0 { - if err, ok := callResults[len(callResults)-1].Interface().(error); ok { - return err - } + err := callback(v) + if err != nil { + return err } } return <-cerr } -// UnmarshalDecoderToCallback parses the CSV from the decoder and send each value to the given func f. -// The func must look like func(Struct). -func UnmarshalDecoderToCallback(in SimpleDecoder, f interface{}) error { - valueFunc := reflect.ValueOf(f) - t := reflect.TypeOf(f) - if t.NumIn() != 1 { - return fmt.Errorf("the given function must have exactly one parameter") - } +// UnmarshalDecoderToCallback parses the CSV from the decoder and send each value to the given func callback. +func UnmarshalDecoderToCallback[T any](in SimpleDecoder, callback func(T) error) error { cerr := make(chan error) - c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0) + c := make(chan T) go func() { - cerr <- UnmarshalDecoderToChan(in, c.Interface()) + cerr <- UnmarshalDecoderToChan(in, c) }() for { select { @@ -376,53 +361,41 @@ func UnmarshalDecoderToCallback(in SimpleDecoder, f interface{}) error { return err default: } - v, notClosed := c.Recv() - if !notClosed || v.Interface() == nil { + v, notClosed := <-c + if !notClosed { break } - valueFunc.Call([]reflect.Value{v}) + err := callback(v) + if err != nil { + return err + } } return <-cerr } -// UnmarshalBytesToCallback parses the CSV from the bytes and send each value to the given func f. -// The func must look like func(Struct). -func UnmarshalBytesToCallback(in []byte, f interface{}) error { - return UnmarshalToCallback(bytes.NewReader(in), f) +// UnmarshalBytesToCallback parses the CSV from the bytes and send each value to +// the given func callback. +func UnmarshalBytesToCallback[T any](in []byte, callback func(T) error) error { + return UnmarshalToCallback(bytes.NewReader(in), callback) } -// UnmarshalStringToCallback parses the CSV from the string and send each value to the given func f. -// The func must look like func(Struct). -func UnmarshalStringToCallback(in string, c interface{}) (err error) { - return UnmarshalToCallback(strings.NewReader(in), c) +// UnmarshalStringToCallback parses the CSV from the string and send each value +// to the given func callback. +func UnmarshalStringToCallback[T any](in string, callback func(T) error) error { + return UnmarshalToCallback(strings.NewReader(in), callback) } // UnmarshalToCallbackWithError parses the CSV from the reader and -// send each value to the given func f. +// send each value to the given func callback. // // If func returns error, it will stop processing, drain the // parser and propagate the error to caller. -// -// The func must look like func(Struct) error. -func UnmarshalToCallbackWithError(in io.Reader, f interface{}) error { - valueFunc := reflect.ValueOf(f) - t := reflect.TypeOf(f) - if t.NumIn() != 1 { - return fmt.Errorf("the given function must have exactly one parameter") - } - if t.NumOut() != 1 { - return fmt.Errorf("the given function must have exactly one return value") - } - if !isErrorType(t.Out(0)) { - return fmt.Errorf("the given function must only return error") - } - +func UnmarshalToCallbackWithError[T any](in io.Reader, callback func(T) error) error { cerr := make(chan error) - c := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, t.In(0)), 0) + c := make(chan T) go func() { - cerr <- UnmarshalToChan(in, c.Interface()) + cerr <- UnmarshalToChan(in, c) }() - var fErr error for { select { @@ -433,39 +406,37 @@ func UnmarshalToCallbackWithError(in io.Reader, f interface{}) error { return fErr default: } - v, notClosed := c.Recv() - if !notClosed || v.Interface() == nil { + v, notClosed := <-c + if !notClosed { if err := <-cerr; err != nil { fErr = err } break } - // callback f has already returned an error, stop processing but keep draining the chan c + // callback has already returned an error, stop processing but keep draining the chan c if fErr != nil { continue } - results := valueFunc.Call([]reflect.Value{v}) - - // If the callback f returns an error, stores it and returns it in future. - errValue := results[0].Interface() - if errValue != nil { - fErr = errValue.(error) + // If the callback returns an error, stores it and returns it in future. + err := callback(v) + if err != nil { + fErr = err } } return fErr } // UnmarshalBytesToCallbackWithError parses the CSV from the bytes and -// send each value to the given func f. +// send each value to the given func callback. // // If func returns error, it will stop processing, drain the // parser and propagate the error to caller. // // The func must look like func(Struct) error. -func UnmarshalBytesToCallbackWithError(in []byte, f interface{}) error { - return UnmarshalToCallbackWithError(bytes.NewReader(in), f) +func UnmarshalBytesToCallbackWithError[T any](in []byte, callback func(T) error) error { + return UnmarshalToCallbackWithError(bytes.NewReader(in), callback) } // UnmarshalStringToCallbackWithError parses the CSV from the string and @@ -475,8 +446,8 @@ func UnmarshalBytesToCallbackWithError(in []byte, f interface{}) error { // parser and propagate the error to caller. // // The func must look like func(Struct) error. -func UnmarshalStringToCallbackWithError(in string, c interface{}) (err error) { - return UnmarshalToCallbackWithError(strings.NewReader(in), c) +func UnmarshalStringToCallbackWithError[T any](in string, callback func(T) error) error { + return UnmarshalToCallbackWithError(strings.NewReader(in), callback) } // CSVToMap creates a simple map from a CSV of 2 columns. diff --git a/csv_test.go b/csv_test.go index 85390a6..f5bfd01 100644 --- a/csv_test.go +++ b/csv_test.go @@ -7,14 +7,14 @@ import ( func TestUnmarshalToCallback_ReaderError(t *testing.T) { type Dummy struct{} - var reader = &errorReader{} + reader := &errorReader{} - err := UnmarshalToCallback(reader, func(Dummy) {}) + err := UnmarshalToCallback(reader, func(Dummy) error { return nil }) if !errors.Is(err, readerErr) { t.Error("UnmarshalToCallback should return first reader error") } - err = UnmarshalDecoderToCallback(newSimpleDecoderFromReader(reader), func(Dummy) {}) + err = UnmarshalDecoderToCallback(newSimpleDecoderFromReader(reader), func(Dummy) error { return nil }) if !errors.Is(err, readerErr) { t.Error("UnmarshalDecoderToCallback should return first reader error") } diff --git a/encode.go b/encode.go index a7c0e72..e41f278 100644 --- a/encode.go +++ b/encode.go @@ -7,9 +7,7 @@ import ( "reflect" ) -var ( - ErrChannelIsClosed = errors.New("channel is closed") -) +var ErrChannelIsClosed = errors.New("channel is closed") type encoder struct { out io.Writer @@ -19,7 +17,7 @@ func newEncoder(out io.Writer) *encoder { return &encoder{out} } -func writeFromChan(writer CSVWriter, c <-chan interface{}, omitHeaders bool) error { +func writeFromChan[T any](writer CSVWriter, c <-chan T, omitHeaders bool) error { // Get the first value. It wil determine the header structure. firstValue, ok := <-c if !ok { diff --git a/go.mod b/go.mod index c746a5a..751294f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/gocarina/gocsv -go 1.13 +go 1.18 From 05ffc3e5f8e138ddaf180280cffc880fd782ac1d Mon Sep 17 00:00:00 2001 From: Luca Zambarda Date: Fri, 9 Feb 2024 14:31:26 +0000 Subject: [PATCH 2/3] update go version in github action --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6ba1dd3..53406a8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v2 with: - go-version: ^1.11 + go-version: ^1.18 - name: Check out code into the Go module directory uses: actions/checkout@v2 From 606f13c9c2571b5933ee06280eb2b2ba2d0d9470 Mon Sep 17 00:00:00 2001 From: Luca Zambarda Date: Fri, 9 Feb 2024 14:36:06 +0000 Subject: [PATCH 3/3] fix broken tests --- decode_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/decode_test.go b/decode_test.go index 7eebc73..5b3b0c0 100644 --- a/decode_test.go +++ b/decode_test.go @@ -616,8 +616,9 @@ func TestUnmarshalToCallback(t *testing.T) { aa,bb,11,cc,dd,ee ff,gg,22,hh,ii,jj`) var samples []SkipFieldSample - if err := UnmarshalBytesToCallback(b.Bytes(), func(s SkipFieldSample) { + if err := UnmarshalBytesToCallback(b.Bytes(), func(s SkipFieldSample) error { samples = append(samples, s) + return nil }); err != nil { t.Fatal(err) } @@ -1020,8 +1021,9 @@ f,1,baz,, *string e,3,b,, `) var samples []Sample - if err := UnmarshalDecoderToCallback(&trimDecoder{LazyCSVReader(b)}, func(s Sample) { + if err := UnmarshalDecoderToCallback(&trimDecoder{LazyCSVReader(b)}, func(s Sample) error { samples = append(samples, s) + return nil }); err != nil { t.Fatal(err) }