Visitar URL original
refactor: drop gocsv to stdlib encoding/csv · openfga/cli@7d01c30 · GitHub
Skip to content

Commit 7d01c30

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

6 files changed

Lines changed: 218 additions & 32 deletions

File tree

‎cmd/tuple/read.go‎

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"errors"
2323
"fmt"
24+
"os"
2425
"strings"
2526

2627
openfga "github.com/openfga/go-sdk"
@@ -48,18 +49,44 @@ type readResponse struct {
4849
}
4950

5051
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"`
52+
UserType string
53+
UserID string
54+
UserRelation string
55+
Relation string
56+
ObjectType string
57+
ObjectID string
58+
ConditionName string
59+
ConditionContext string
5960
}
6061

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

6491
for _, readRes := range r.simple {
6592
// Handle Condition
@@ -196,19 +223,22 @@ var readCmd = &cobra.Command{
196223

197224
simpleOutput, _ := cmd.Flags().GetBool("simple-output")
198225
outputFormat, _ := cmd.Flags().GetString("output-format")
199-
dataPrinter := output.NewUniPrinter(outputFormat)
200226

201227
if outputFormat == "csv" {
202-
data, _ := response.toCsvDTO()
203-
204-
err := dataPrinter.Display(data)
228+
records, err := response.toCsvDTO()
205229
if err != nil {
230+
return fmt.Errorf("failed to convert response to csv: %w", err)
231+
}
232+
233+
if err := output.MarshalCSV(records, os.Stdout, readResponseCSVHeaders...); err != nil {
206234
return fmt.Errorf("failed to display csv: %w", err)
207235
}
208236

209237
return nil
210238
}
211239

240+
dataPrinter := output.NewUniPrinter(outputFormat)
241+
212242
var data any
213243

214244
data = *response.complete

‎cmd/tuple/read_test.go‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
1011

1112
openfga "github.com/openfga/go-sdk"
1213
"github.com/openfga/go-sdk/client"
@@ -438,7 +439,7 @@ func TestReadResponseCSVDTOParser(t *testing.T) {
438439

439440
testCases := []struct {
440441
readRes readResponse
441-
expected []readResponseCSVDTO
442+
expected readResponseCSVDTOList
442443
}{
443444
{
444445
readRes: readResponse{
@@ -460,7 +461,7 @@ func TestReadResponseCSVDTOParser(t *testing.T) {
460461
},
461462
},
462463
},
463-
expected: []readResponseCSVDTO{
464+
expected: readResponseCSVDTOList{
464465
{
465466
UserType: "user",
466467
UserID: "anne",
@@ -487,6 +488,43 @@ func TestReadResponseCSVDTOParser(t *testing.T) {
487488
}
488489
}
489490

491+
func TestReadResponseCSVDTOListMarshalCSV(t *testing.T) {
492+
t.Parallel()
493+
494+
list := readResponseCSVDTOList{
495+
{
496+
UserType: "user",
497+
UserID: "anne",
498+
Relation: "reader",
499+
ObjectType: "document",
500+
ObjectID: "secret.doc",
501+
ConditionName: "inOfficeIP",
502+
ConditionContext: `{"ip_addr":"10.0.0.1"}`,
503+
},
504+
{
505+
UserType: "user",
506+
UserID: "john",
507+
Relation: "writer",
508+
ObjectType: "document",
509+
ObjectID: "abc.doc",
510+
},
511+
}
512+
513+
rows := make([][]string, 0, len(list))
514+
515+
for _, record := range list {
516+
row, err := record.MarshalCSV()
517+
require.NoError(t, err)
518+
519+
rows = append(rows, row)
520+
}
521+
522+
assert.Equal(t, [][]string{
523+
{"user", "anne", "", "reader", "document", "secret.doc", "inOfficeIP", `{"ip_addr":"10.0.0.1"}`},
524+
{"user", "john", "", "writer", "document", "abc.doc", "", ""},
525+
}, rows)
526+
}
527+
490528
func toPointer[T any](p T) *T {
491529
return &p
492530
}

‎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: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ limitations under the License.
1818
package output
1919

2020
import (
21+
"encoding/csv"
2122
"encoding/json"
2223
"fmt"
24+
"io"
2325
"os"
2426

25-
"github.com/gocarina/gocsv"
2627
"gopkg.in/yaml.v3"
2728

2829
"github.com/mattn/go-isatty"
@@ -38,9 +39,6 @@ type Printer interface {
3839
// jsonPrinter implements the Printer interface for JSON output.
3940
type jsonPrinter struct{}
4041

41-
// csvPrinter implements the Printer interface for CSV output.
42-
type csvPrinter struct{}
43-
4442
// yamlPrinter implements the Printer interface for YAML output.
4543
type yamlPrinter struct{}
4644

@@ -69,17 +67,37 @@ func (prt *jsonPrinter) DisplayColor(data any) error {
6967
return nil
7068
}
7169

72-
func (prt *csvPrinter) DisplayColor(data any) error {
73-
return prt.DisplayNoColor(data)
70+
// CSVMarshaler is implemented by a value that can encode itself as a CSV record.
71+
type CSVMarshaler interface {
72+
MarshalCSV() ([]string, error)
7473
}
7574

76-
func (prt *csvPrinter) DisplayNoColor(data any) error {
77-
b, err := gocsv.MarshalBytes(data)
78-
if err != nil {
79-
return fmt.Errorf("unable to marshal CSV with error: %w", err)
75+
// MarshalCSV writes an optional header row followed by one record per element to w.
76+
func MarshalCSV[T CSVMarshaler](records []T, w io.Writer, header ...string) error {
77+
writer := csv.NewWriter(w)
78+
79+
if len(header) > 0 {
80+
if err := writer.Write(header); err != nil {
81+
return fmt.Errorf("failed to write csv header: %w", err)
82+
}
8083
}
8184

82-
fmt.Println(string(b))
85+
for _, record := range records {
86+
row, err := record.MarshalCSV()
87+
if err != nil {
88+
return fmt.Errorf("failed to marshal csv record: %w", err)
89+
}
90+
91+
if err := writer.Write(row); err != nil {
92+
return fmt.Errorf("failed to write csv record: %w", err)
93+
}
94+
}
95+
96+
writer.Flush()
97+
98+
if err := writer.Error(); err != nil {
99+
return fmt.Errorf("failed to flush csv: %w", err)
100+
}
83101

84102
return nil
85103
}
@@ -115,8 +133,6 @@ func NewUniPrinter(outputFormat string) *UniPrinter {
115133
switch outputFormat {
116134
case "yaml":
117135
uniPrinter.Printer = &yamlPrinter{}
118-
case "csv":
119-
uniPrinter.Printer = &csvPrinter{}
120136
default:
121137
uniPrinter.Printer = &jsonPrinter{}
122138
}

‎internal/output/marshal_test.go‎

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

0 commit comments

Comments
 (0)