Skip to content

Commit 08193f1

Browse files
committed
refactor: drop gocsv to stdlib encoding/csv
1 parent d1fd7d3 commit 08193f1

6 files changed

Lines changed: 174 additions & 17 deletions

File tree

cmd/tuple/read.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,50 @@ type readResponse struct {
4848
}
4949

5050
type readResponseCSVDTO struct {
51-
UserType string `csv:"user_type"`
52-
UserID string `csv:"user_id"`
53-
UserRelation string `csv:"user_relation,omitempty"`
54-
Relation string `csv:"relation"`
55-
ObjectType string `csv:"object_type"`
56-
ObjectID string `csv:"object_id"`
57-
ConditionName string `csv:"condition_name,omitempty"`
58-
ConditionContext string `csv:"condition_context,omitempty"`
51+
UserType string
52+
UserID string
53+
UserRelation string
54+
Relation string
55+
ObjectType string
56+
ObjectID string
57+
ConditionName string
58+
ConditionContext string
5959
}
6060

61-
func (r readResponse) toCsvDTO() ([]readResponseCSVDTO, error) {
62-
readResponseDTO := make([]readResponseCSVDTO, 0, len(r.simple))
61+
type readResponseCSVDTOList []readResponseCSVDTO
62+
63+
var readResponseCSVHeaders = []string{
64+
"user_type",
65+
"user_id",
66+
"user_relation",
67+
"relation",
68+
"object_type",
69+
"object_id",
70+
"condition_name",
71+
"condition_context",
72+
}
73+
74+
func (l readResponseCSVDTOList) MarshalCSV() ([]string, [][]string, error) {
75+
records := make([][]string, 0, len(l))
76+
77+
for _, dto := range l {
78+
records = append(records, []string{
79+
dto.UserType,
80+
dto.UserID,
81+
dto.UserRelation,
82+
dto.Relation,
83+
dto.ObjectType,
84+
dto.ObjectID,
85+
dto.ConditionName,
86+
dto.ConditionContext,
87+
})
88+
}
89+
90+
return readResponseCSVHeaders, records, nil
91+
}
92+
93+
func (r readResponse) toCsvDTO() (readResponseCSVDTOList, error) {
94+
readResponseDTO := make(readResponseCSVDTOList, 0, len(r.simple))
6395

6496
for _, readRes := range r.simple {
6597
// Handle Condition

cmd/tuple/read_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ func TestReadResponseCSVDTOParser(t *testing.T) {
438438

439439
testCases := []struct {
440440
readRes readResponse
441-
expected []readResponseCSVDTO
441+
expected readResponseCSVDTOList
442442
}{
443443
{
444444
readRes: readResponse{
@@ -460,7 +460,7 @@ func TestReadResponseCSVDTOParser(t *testing.T) {
460460
},
461461
},
462462
},
463-
expected: []readResponseCSVDTO{
463+
expected: readResponseCSVDTOList{
464464
{
465465
UserType: "user",
466466
UserID: "anne",

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ go 1.25.7
55
toolchain go1.26.4
66

77
require (
8-
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
98
github.com/hashicorp/go-multierror v1.1.1
109
github.com/mattn/go-isatty v0.0.22
1110
github.com/muesli/mango-cobra v1.3.0

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF
7474
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
7575
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
7676
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
77-
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
78-
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
7977
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
8078
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
8179
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=

internal/output/marshal.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ limitations under the License.
1818
package output
1919

2020
import (
21+
"bytes"
22+
"encoding/csv"
2123
"encoding/json"
24+
"errors"
2225
"fmt"
2326
"os"
2427

25-
"github.com/gocarina/gocsv"
2628
"gopkg.in/yaml.v3"
2729

2830
"github.com/mattn/go-isatty"
@@ -74,7 +76,7 @@ func (prt *csvPrinter) DisplayColor(data any) error {
7476
}
7577

7678
func (prt *csvPrinter) DisplayNoColor(data any) error {
77-
b, err := gocsv.MarshalBytes(data)
79+
b, err := marshalCSV(data)
7880
if err != nil {
7981
return fmt.Errorf("unable to marshal CSV with error: %w", err)
8082
}
@@ -84,6 +86,43 @@ func (prt *csvPrinter) DisplayNoColor(data any) error {
8486
return nil
8587
}
8688

89+
var errNotCSVMarshaler = errors.New("type does not implement output.CSVMarshaler")
90+
91+
// CSVMarshaler is implemented by types that can render themselves as CSV:
92+
// a header row followed by one record per element.
93+
type CSVMarshaler interface {
94+
MarshalCSV() (headers []string, records [][]string, err error)
95+
}
96+
97+
func marshalCSV(data any) ([]byte, error) {
98+
marshaler, ok := data.(CSVMarshaler)
99+
if !ok {
100+
return nil, fmt.Errorf("cannot marshal %T to csv: %w", data, errNotCSVMarshaler)
101+
}
102+
103+
headers, records, err := marshaler.MarshalCSV()
104+
if err != nil {
105+
return nil, fmt.Errorf("failed to marshal csv: %w", err)
106+
}
107+
108+
buffer := &bytes.Buffer{}
109+
writer := csv.NewWriter(buffer)
110+
111+
if err := writer.Write(headers); err != nil {
112+
return nil, fmt.Errorf("failed to write csv header: %w", err)
113+
}
114+
115+
if err := writer.WriteAll(records); err != nil {
116+
return nil, fmt.Errorf("failed to write csv records: %w", err)
117+
}
118+
119+
if err := writer.Error(); err != nil {
120+
return nil, fmt.Errorf("failed to flush csv: %w", err)
121+
}
122+
123+
return buffer.Bytes(), nil
124+
}
125+
87126
func (prt *yamlPrinter) DisplayColor(data any) error {
88127
return prt.DisplayNoColor(data)
89128
}

internal/output/marshal_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package output
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
type fakeCSVMarshaler struct {
12+
headers []string
13+
records [][]string
14+
err error
15+
}
16+
17+
func (f fakeCSVMarshaler) MarshalCSV() ([]string, [][]string, error) {
18+
return f.headers, f.records, f.err
19+
}
20+
21+
func TestMarshalCSV(t *testing.T) {
22+
t.Parallel()
23+
24+
headers := []string{"user_type", "user_id", "relation", "object_type", "object_id", "condition_context"}
25+
26+
tests := []struct {
27+
name string
28+
records [][]string
29+
expected string
30+
}{
31+
{
32+
name: "no records writes only headers",
33+
records: nil,
34+
expected: "user_type,user_id,relation,object_type,object_id,condition_context\n",
35+
},
36+
{
37+
name: "single record",
38+
records: [][]string{
39+
{"user", "john", "writer", "document", "abc.doc", ""},
40+
},
41+
expected: "user_type,user_id,relation,object_type,object_id,condition_context\n" +
42+
"user,john,writer,document,abc.doc,\n",
43+
},
44+
{
45+
name: "multiple records",
46+
records: [][]string{
47+
{"user", "anne", "reader", "document", "x", ""},
48+
{"group", "eng", "owner", "repo", "y", ""},
49+
},
50+
expected: "user_type,user_id,relation,object_type,object_id,condition_context\n" +
51+
"user,anne,reader,document,x,\n" +
52+
"group,eng,owner,repo,y,\n",
53+
},
54+
{
55+
name: "values with commas, quotes and newlines are escaped",
56+
records: [][]string{
57+
{"user", "a,b", "say \"hi\"", "doc", "line\nbreak", `{"ip_addr":"10.0.0.1"}`},
58+
},
59+
expected: "user_type,user_id,relation,object_type,object_id,condition_context\n" +
60+
"user,\"a,b\",\"say \"\"hi\"\"\",doc,\"line\nbreak\",\"{\"\"ip_addr\"\":\"\"10.0.0.1\"\"}\"\n",
61+
},
62+
}
63+
64+
for _, test := range tests {
65+
t.Run(test.name, func(t *testing.T) {
66+
t.Parallel()
67+
68+
got, err := marshalCSV(fakeCSVMarshaler{headers: headers, records: test.records})
69+
require.NoError(t, err)
70+
assert.Equal(t, test.expected, string(got))
71+
})
72+
}
73+
}
74+
75+
func TestMarshalCSVNotAMarshaler(t *testing.T) {
76+
t.Parallel()
77+
78+
_, err := marshalCSV([]string{"a", "b"})
79+
assert.ErrorIs(t, err, errNotCSVMarshaler)
80+
}
81+
82+
func TestMarshalCSVMarshalerError(t *testing.T) {
83+
t.Parallel()
84+
85+
sentinel := errors.New("boom")
86+
87+
_, err := marshalCSV(fakeCSVMarshaler{err: sentinel})
88+
assert.ErrorIs(t, err, sentinel)
89+
}

0 commit comments

Comments
 (0)