From 27a36e3b375ec389a65f436196c1a37c557a01a8 Mon Sep 17 00:00:00 2001 From: Roman Shatsov Date: Tue, 20 May 2025 20:49:00 +0200 Subject: [PATCH 1/4] Support for uint --- reader.go | 6 ++++++ reader_test.go | 8 +++++--- shared_test.go | 1 + writer_test.go | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/reader.go b/reader.go index 5e06a10..233c550 100644 --- a/reader.go +++ b/reader.go @@ -198,6 +198,12 @@ func setFieldValue(field reflect.Value, value string) error { return err } field.SetInt(intValue) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + uintValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + field.SetUint(uintValue) case reflect.Float32, reflect.Float64: floatValue, err := strconv.ParseFloat(value, 64) if err != nil { diff --git a/reader_test.go b/reader_test.go index 00b45a2..522ae00 100644 --- a/reader_test.go +++ b/reader_test.go @@ -101,9 +101,9 @@ Bob,bob@example.com,25 func TestComplexTypes(t *testing.T) { // CSV data with various types - csvData := `name,created_at,active,score,count,rate,tags -John,2023-01-02T15:04:05Z,true,98.6,42,3.14,test;debug -Jane,2023-06-15T09:30:00Z,false,75.2,100,2.718,prod;live` + csvData := `name,created_at,active,score,count,rate,tags,rank +John,2023-01-02T15:04:05Z,true,98.6,42,3.14,test;debug,1 +Jane,2023-06-15T09:30:00Z,false,75.2,100,2.718,prod;live,42` reader := strings.NewReader(csvData) @@ -125,6 +125,7 @@ Jane,2023-06-15T09:30:00Z,false,75.2,100,2.718,prod;live` Count: 42, Rate: 3.14, Tags: "test;debug", + Rank: 1, }, { Name: "Jane", @@ -134,6 +135,7 @@ Jane,2023-06-15T09:30:00Z,false,75.2,100,2.718,prod;live` Count: 100, Rate: 2.718, Tags: "prod;live", + Rank: 42, }, } diff --git a/shared_test.go b/shared_test.go index 4261bbf..26a4f53 100644 --- a/shared_test.go +++ b/shared_test.go @@ -21,6 +21,7 @@ type ComplexRecord struct { Count int `csv:"count"` Rate float32 `csv:"rate"` Tags string `csv:"tags"` + Rank uint `csv:"rank"` } type Point struct { diff --git a/writer_test.go b/writer_test.go index 64e55a0..d293b54 100644 --- a/writer_test.go +++ b/writer_test.go @@ -72,6 +72,7 @@ func TestComplexWriter(t *testing.T) { Count: 42, Rate: 3.14, Tags: "test;debug", + Rank: 1, }, { Name: "Jane", @@ -81,6 +82,7 @@ func TestComplexWriter(t *testing.T) { Count: 100, Rate: 2.718, Tags: "prod;live", + Rank: 42, }, } @@ -127,6 +129,7 @@ func TestWriteAll(t *testing.T) { Count: 42, Rate: 3.14, Tags: "test;debug", + Rank: 1, }, { Name: "Jane", @@ -136,6 +139,7 @@ func TestWriteAll(t *testing.T) { Count: 100, Rate: 2.718, Tags: "prod;live", + Rank: 42, }, } From 960a2652dc655315ca667e39965713a1539634f6 Mon Sep 17 00:00:00 2001 From: Roman Shatsov Date: Sun, 1 Jun 2025 21:14:03 +0200 Subject: [PATCH 2/4] Test error on invalid data --- reader_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/reader_test.go b/reader_test.go index 522ae00..82b0d4f 100644 --- a/reader_test.go +++ b/reader_test.go @@ -168,3 +168,48 @@ func TestCustomUnmarshaler(t *testing.T) { t.Errorf("Parsed results do not match expected.\nExpected: %+v\nGot: %+v", expected, results) } } + +func TestInvalidData(t *testing.T) { + t.Parallel() + + panickedValue := func(f func()) (value any) { + defer func() { + value = recover() + }() + + f() + return + } + + test := func(t *testing.T, input string, expectedMessage string) { + reader := strings.NewReader(input) + + rb, err := rowboat.NewReader[ComplexRecord](reader) + if err != nil { + t.Fatalf("Failed to create RowBoat: %v", err) + } + + actualValue := panickedValue(func() { + slices.Collect(rb.All()) + }) + + if actualValue == nil { + t.Errorf("Expected a panic for input '%s', but it was parsed without error", input) + return + } + + actualError, ok := actualValue.(error) + if !ok { + t.Errorf("Expected a panic for input '%s', but received a non-error value: %v", input, actualValue) + return + } + + actualMessage := actualError.Error() + if expectedMessage != actualMessage { + t.Errorf("Expected a panic for input '%s' with error message '%s', but got '%s'", input, expectedMessage, actualMessage) + } + } + + test(t, "score\nnot-float", `error setting field Score: strconv.ParseFloat: parsing "not-float": invalid syntax`) + test(t, "rank\n-1", `error setting field Rank: strconv.ParseUint: parsing "-1": invalid syntax`) +} From 892ee01bc7cd2f7e3489ad0acf634ee55540145f Mon Sep 17 00:00:00 2001 From: Raman Shatsou Date: Sun, 8 Jun 2025 13:36:57 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- reader_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/reader_test.go b/reader_test.go index 82b0d4f..adfbb18 100644 --- a/reader_test.go +++ b/reader_test.go @@ -182,6 +182,8 @@ func TestInvalidData(t *testing.T) { } test := func(t *testing.T, input string, expectedMessage string) { + t.Helper() + reader := strings.NewReader(input) rb, err := rowboat.NewReader[ComplexRecord](reader) @@ -200,13 +202,13 @@ func TestInvalidData(t *testing.T) { actualError, ok := actualValue.(error) if !ok { - t.Errorf("Expected a panic for input '%s', but received a non-error value: %v", input, actualValue) + t.Errorf("Expected a panic for input '%s', but received a non-error value: %v (%[2]T)", input, actualValue) return } actualMessage := actualError.Error() if expectedMessage != actualMessage { - t.Errorf("Expected a panic for input '%s' with error message '%s', but got '%s'", input, expectedMessage, actualMessage) + t.Errorf("Expected a panic for input %q with error message %q, but got %q", input, expectedMessage, actualMessage) } } From 2d4c6a812ac0ef7bca4e9f0958a68ed78664d765 Mon Sep 17 00:00:00 2001 From: Roman Shatsov Date: Mon, 9 Jun 2025 13:16:36 +0200 Subject: [PATCH 4/4] Test overflow --- reader.go | 6 ++++++ reader_test.go | 3 +++ shared_test.go | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/reader.go b/reader.go index 233c550..69f4fd5 100644 --- a/reader.go +++ b/reader.go @@ -197,12 +197,18 @@ func setFieldValue(field reflect.Value, value string) error { if err != nil { return err } + if field.OverflowInt(intValue) { + return fmt.Errorf("value %d out of range for type %s", intValue, field.Kind()) + } field.SetInt(intValue) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: uintValue, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } + if field.OverflowUint(uintValue) { + return fmt.Errorf("value %d out of range for type %s", uintValue, field.Kind()) + } field.SetUint(uintValue) case reflect.Float32, reflect.Float64: floatValue, err := strconv.ParseFloat(value, 64) diff --git a/reader_test.go b/reader_test.go index adfbb18..3e73a5f 100644 --- a/reader_test.go +++ b/reader_test.go @@ -214,4 +214,7 @@ func TestInvalidData(t *testing.T) { test(t, "score\nnot-float", `error setting field Score: strconv.ParseFloat: parsing "not-float": invalid syntax`) test(t, "rank\n-1", `error setting field Rank: strconv.ParseUint: parsing "-1": invalid syntax`) + test(t, "rank\n1000", "error setting field Rank: value 1000 out of range for type uint8") + test(t, "int8\n1000", "error setting field Int8: value 1000 out of range for type int8") + test(t, "int8\n-1000", "error setting field Int8: value -1000 out of range for type int8") } diff --git a/shared_test.go b/shared_test.go index 26a4f53..41eb056 100644 --- a/shared_test.go +++ b/shared_test.go @@ -21,7 +21,8 @@ type ComplexRecord struct { Count int `csv:"count"` Rate float32 `csv:"rate"` Tags string `csv:"tags"` - Rank uint `csv:"rank"` + Rank uint8 `csv:"rank"` + Int8 int8 `csv:"int8"` } type Point struct {