Skip to content

Commit 2a367b9

Browse files
authored
Merge pull request #8 from sullivtr/main
output wrapper lib
2 parents 8f93b96 + 52ce928 commit 2a367b9

File tree

3 files changed

+164
-30
lines changed

3 files changed

+164
-30
lines changed

internal/cmd/organizations/list.go

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ package organizations
22

33
import (
44
"fmt"
5+
"os"
56

67
resourcemanagerv1alpha "buf.build/gen/go/datum-cloud/datum-os/protocolbuffers/go/datum/os/resourcemanager/v1alpha"
7-
"buf.build/go/protoyaml"
8-
"github.com/rodaine/table"
98
"github.com/spf13/cobra"
10-
"google.golang.org/protobuf/encoding/protojson"
119

1210
"go.datum.net/datumctl/internal/keyring"
11+
"go.datum.net/datumctl/internal/output"
1312
"go.datum.net/datumctl/internal/resourcemanager"
1413
)
1514

@@ -35,37 +34,17 @@ func listOrgsCommand() *cobra.Command {
3534
return fmt.Errorf("failed to list organizations: %w", err)
3635
}
3736

38-
// TODO: We should look at abstracting the formatting here into a library
39-
// that can be used by multiple commands needing to offer multiple
40-
// output formats from a command.
41-
switch outputFormat {
42-
case "yaml":
43-
marshaller := &protoyaml.MarshalOptions{
44-
Indent: 2,
45-
}
46-
output, err := marshaller.Marshal(listOrgs)
47-
if err != nil {
48-
return fmt.Errorf("failed to list organizations: %w", err)
49-
}
50-
fmt.Print(string(output))
51-
case "json":
52-
output, err := protojson.Marshal(listOrgs)
53-
if err != nil {
54-
return fmt.Errorf("failed to list organizations: %w", err)
55-
}
56-
fmt.Print(string(output))
57-
case "table":
58-
orgTable := table.New("DISPLAY NAME", "RESOURCE ID")
59-
if len(listOrgs.Organizations) == 0 {
60-
fmt.Printf("No organizations found")
61-
} else {
37+
if err := output.CLIPrint(os.Stdout, outputFormat, listOrgs, func() (output.ColumnFormatter, output.RowFormatterFunc) {
38+
return output.ColumnFormatter{"DISPLAY NAME", "RESOURCE ID"}, func() output.RowFormatter {
39+
var rowData output.RowFormatter
6240
for _, org := range listOrgs.Organizations {
63-
orgTable.AddRow(org.DisplayName, org.OrganizationId)
41+
rowData = append(rowData, []any{org.DisplayName, org.OrganizationId})
6442
}
43+
return rowData
6544
}
66-
orgTable.Print()
45+
}); err != nil {
46+
return fmt.Errorf("a problem occured while printing organizations list: %w", err)
6747
}
68-
6948
return nil
7049
},
7150
}

internal/output/output.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package output
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"buf.build/go/protoyaml"
8+
"github.com/rodaine/table"
9+
"google.golang.org/protobuf/encoding/protojson"
10+
"google.golang.org/protobuf/proto"
11+
)
12+
13+
type TableFormatterFunc func() (ColumnFormatter, RowFormatterFunc)
14+
type RowFormatterFunc func() RowFormatter
15+
type RowFormatter [][]any
16+
type ColumnFormatter []any
17+
18+
func CLIPrint(w io.Writer, format string, data proto.Message, tableFormatterFunc TableFormatterFunc) error {
19+
switch format {
20+
case "yaml":
21+
return printYAML(w, data)
22+
case "json":
23+
return printJSON(w, data)
24+
case "table":
25+
headers, rowDataFunc := tableFormatterFunc()
26+
if headers == nil {
27+
return fmt.Errorf("headers must be provided for table output")
28+
}
29+
return printTable(w, headers, rowDataFunc())
30+
default:
31+
return fmt.Errorf("unsupported format: %s", format)
32+
}
33+
}
34+
35+
func printYAML(w io.Writer, data proto.Message) error {
36+
marshaller := protoyaml.MarshalOptions{
37+
Indent: 2,
38+
}
39+
40+
output, err := marshaller.Marshal(data)
41+
if err != nil {
42+
return fmt.Errorf("failed to marshal data to YAML: %w", err)
43+
}
44+
fmt.Fprint(w, string(output))
45+
return nil
46+
}
47+
48+
func printJSON(w io.Writer, data proto.Message) error {
49+
output, err := protojson.Marshal(data)
50+
if err != nil {
51+
return fmt.Errorf("failed to marshal data to JSON: %w", err)
52+
}
53+
fmt.Fprint(w, string(output))
54+
return nil
55+
}
56+
57+
func printTable(w io.Writer, headers ColumnFormatter, rowData [][]any) error {
58+
t := table.New(headers...)
59+
t.WithWriter(w)
60+
for _, row := range rowData {
61+
t.AddRow(row)
62+
}
63+
t.Print()
64+
return nil
65+
}

internal/output/output_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package output
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
resourcemanagerv1alpha "buf.build/gen/go/datum-cloud/datum-os/protocolbuffers/go/datum/os/resourcemanager/v1alpha"
9+
"google.golang.org/protobuf/proto"
10+
)
11+
12+
func TestCLIPrint(t *testing.T) {
13+
14+
testOrgProto := &resourcemanagerv1alpha.Organization{
15+
DisplayName: "Test Organization",
16+
OrganizationId: "1234",
17+
}
18+
19+
tests := []struct {
20+
name string
21+
format string
22+
data proto.Message
23+
headers []any
24+
rowData [][]any
25+
wantErr bool
26+
wantOutput string
27+
}{
28+
{
29+
name: "Print YAML",
30+
format: "yaml",
31+
data: testOrgProto,
32+
wantErr: false,
33+
wantOutput: "organizationId: \"1234\"\ndisplayName: Test Organization\n",
34+
},
35+
{
36+
name: "Print JSON",
37+
format: "json",
38+
data: testOrgProto,
39+
wantErr: false,
40+
wantOutput: "{\"organizationId\":\"1234\",\"displayName\":\"Test Organization\"}",
41+
},
42+
{
43+
name: "Print Table",
44+
format: "table",
45+
headers: []any{"Header1", "Header2"},
46+
rowData: [][]any{{"Row1Col1", "Row1Col2"}, {"Row2Col1", "Row2Col2"}},
47+
wantErr: false,
48+
},
49+
{
50+
name: "Unsupported Format",
51+
format: "unsupported",
52+
data: testOrgProto,
53+
wantErr: true,
54+
},
55+
{
56+
name: "Table without headers and rowData",
57+
format: "table",
58+
wantErr: true,
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
var buf bytes.Buffer
65+
66+
err := CLIPrint(&buf, tt.format, tt.data, func() (ColumnFormatter, RowFormatterFunc) {
67+
return tt.headers, func() RowFormatter {
68+
return tt.rowData
69+
}
70+
})
71+
if (err != nil) != tt.wantErr {
72+
t.Errorf("CLIPrint() error = %v, wantErr %v", err, tt.wantErr)
73+
return
74+
}
75+
76+
if !tt.wantErr {
77+
if tt.format == "table" {
78+
out := buf.String()
79+
if !strings.Contains(out, tt.headers[0].(string)) || !strings.Contains(out, tt.headers[1].(string)) {
80+
t.Errorf("CLIPrint() output = %v, does not have correct headers", out)
81+
}
82+
} else {
83+
if gotOutput := buf.String(); gotOutput != tt.wantOutput {
84+
t.Errorf("CLIPrint() output = \n%v, want \n%v", gotOutput, tt.wantOutput)
85+
}
86+
}
87+
}
88+
})
89+
}
90+
}

0 commit comments

Comments
 (0)