diff --git a/src/cmd/projectList.go b/src/cmd/projectList.go index 908b0bc6..a2b03947 100644 --- a/src/cmd/projectList.go +++ b/src/cmd/projectList.go @@ -5,6 +5,7 @@ import ( "github.com/zeropsio/zcli/src/cmdBuilder" "github.com/zeropsio/zcli/src/i18n" + "github.com/zeropsio/zcli/src/output" "github.com/zeropsio/zcli/src/uxHelpers" ) @@ -12,9 +13,15 @@ func projectListCmd() *cmdBuilder.Cmd { return cmdBuilder.NewCmd(). Use("list"). Short(i18n.T(i18n.CmdDescProjectList)). + StringFlag("output", "table", i18n.T(i18n.OutputFormatFlag)). HelpFlag(i18n.T(i18n.CmdHelpProjectList)). LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error { - err := uxHelpers.PrintProjectList(ctx, cmdData.RestApiClient, cmdData.Stdout) + outputFormat, err := output.ParseFormat(cmdData.Params.GetString("output")) + if err != nil { + return err + } + + err = uxHelpers.PrintProjectList(ctx, cmdData.RestApiClient, cmdData.Stdout, outputFormat) if err != nil { return err } diff --git a/src/cmd/serviceList.go b/src/cmd/serviceList.go index 9a6b252e..52b5a3e3 100644 --- a/src/cmd/serviceList.go +++ b/src/cmd/serviceList.go @@ -5,6 +5,7 @@ import ( "github.com/zeropsio/zcli/src/cmdBuilder" "github.com/zeropsio/zcli/src/i18n" + "github.com/zeropsio/zcli/src/output" "github.com/zeropsio/zcli/src/uxHelpers" ) @@ -14,8 +15,14 @@ func serviceListCmd() *cmdBuilder.Cmd { Short(i18n.T(i18n.CmdDescServiceList)). ScopeLevel(cmdBuilder.ScopeProject()). Arg(cmdBuilder.ProjectArgName, cmdBuilder.OptionalArg()). + StringFlag("output", "table", i18n.T(i18n.OutputFormatFlag)). HelpFlag(i18n.T(i18n.CmdHelpServiceList)). LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error { + outputFormat, err := output.ParseFormat(cmdData.Params.GetString("output")) + if err != nil { + return err + } + project, err := cmdData.Project.Expect("project is null") if err != nil { return err @@ -25,6 +32,7 @@ func serviceListCmd() *cmdBuilder.Cmd { cmdData.RestApiClient, cmdData.Stdout, project, + outputFormat, ); err != nil { return err } diff --git a/src/i18n/en.go b/src/i18n/en.go index a7b24222..7da43a6c 100644 --- a/src/i18n/en.go +++ b/src/i18n/en.go @@ -263,6 +263,7 @@ at https://docs.zerops.io/references/cli for further details.`, VerboseFlag: "If set, additional data will be logged to the zcli debug log file.", ZeropsYamlSetup: "Choose setup to be used from zerops.yml.", DisableLogs: "Disable log output.", + OutputFormatFlag: "Output format. Supported: table, json, csv.", // archiveClient ArchClientWorkingDirectory: "working directory: %s", diff --git a/src/i18n/i18n.go b/src/i18n/i18n.go index b87a70df..ffd4b3b4 100644 --- a/src/i18n/i18n.go +++ b/src/i18n/i18n.go @@ -237,6 +237,7 @@ const ( VerboseFlag = "VerboseFlag" ZeropsYamlSetup = "ZeropsYamlSetup" DisableLogs = "DisableLogs" + OutputFormatFlag = "OutputFormatFlag" // archiveClient ArchClientWorkingDirectory = "ArchClientWorkingDirectory" diff --git a/src/output/formatter.go b/src/output/formatter.go new file mode 100644 index 00000000..055040c2 --- /dev/null +++ b/src/output/formatter.go @@ -0,0 +1,88 @@ +package output + +import ( + "encoding/csv" + "encoding/json" + "fmt" + "strings" + + "github.com/zeropsio/zcli/src/uxBlock/models/table" +) + +type Format string + +const ( + FormatTable Format = "table" + FormatJSON Format = "json" + FormatCSV Format = "csv" +) + +func ParseFormat(s string) (Format, error) { + switch strings.ToLower(s) { + case "", "table": + return FormatTable, nil + case "json": + return FormatJSON, nil + case "csv": + return FormatCSV, nil + default: + return FormatTable, fmt.Errorf("unsupported output format %q (supported: %s)", s, SupportedFormats()) + } +} + +func SupportedFormats() string { + return "table, json, csv" +} + +func PrintData(headers []string, rows [][]string, format Format) (string, error) { + switch format { + case FormatJSON: + return printJSON(headers, rows) + case FormatCSV: + return printCSV(headers, rows) + default: + return printTable(headers, rows), nil + } +} + +func printTable(headers []string, rows [][]string) string { + headerRow := table.NewRowFromStrings(headers...) + body := table.NewBody() + for _, row := range rows { + body.AddStringsRow(row...) + } + return table.Render(body, table.WithHeader(headerRow)) +} + +func printJSON(headers []string, rows [][]string) (string, error) { + items := make([]map[string]string, 0, len(rows)) + for _, row := range rows { + item := make(map[string]string, len(headers)) + for i, h := range headers { + if i < len(row) { + item[h] = row[i] + } + } + items = append(items, item) + } + out, err := json.MarshalIndent(items, "", " ") + if err != nil { + return "", err + } + return string(out), nil +} + +func printCSV(headers []string, rows [][]string) (string, error) { + var b strings.Builder + w := csv.NewWriter(&b) + if err := w.Write(headers); err != nil { + return "", err + } + for _, row := range rows { + if err := w.Write(row); err != nil { + return "", err + } + } + w.Flush() + return b.String(), w.Error() +} diff --git a/src/output/formatter_test.go b/src/output/formatter_test.go new file mode 100644 index 00000000..2fd2fe65 --- /dev/null +++ b/src/output/formatter_test.go @@ -0,0 +1,102 @@ +package output + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseFormat(t *testing.T) { + tests := []struct { + input string + want Format + wantErr bool + }{ + {"", FormatTable, false}, + {"table", FormatTable, false}, + {"json", FormatJSON, false}, + {"csv", FormatCSV, false}, + {"JSON", FormatJSON, false}, + {"CSV", FormatCSV, false}, + {"yaml", FormatTable, true}, + {"xml", FormatTable, true}, + } + for _, tt := range tests { + got, err := ParseFormat(tt.input) + if tt.wantErr { + require.Error(t, err, "input %q", tt.input) + } else { + require.NoError(t, err, "input %q", tt.input) + require.Equal(t, tt.want, got, "input %q", tt.input) + } + } +} + +func TestPrintDataTable(t *testing.T) { + headers := []string{"id", "name", "status"} + rows := [][]string{ + {"1", "foo", "active"}, + {"2", "bar", "inactive"}, + } + result, err := PrintData(headers, rows, FormatTable) + require.NoError(t, err) + require.Contains(t, result, "ID") + require.Contains(t, result, "foo") + require.Contains(t, result, "bar") +} + +func TestPrintDataJSON(t *testing.T) { + headers := []string{"id", "name", "status"} + rows := [][]string{ + {"1", "foo", "active"}, + {"2", "bar", "inactive"}, + } + result, err := PrintData(headers, rows, FormatJSON) + require.NoError(t, err) + + require.Contains(t, result, `"id": "1"`) + require.Contains(t, result, `"name": "foo"`) + require.Contains(t, result, `"status": "active"`) + require.Contains(t, result, `"id": "2"`) + require.Contains(t, result, `"name": "bar"`) + + var parsed []map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(result), &parsed)) + require.Len(t, parsed, 2) +} + +func TestPrintDataCSV(t *testing.T) { + headers := []string{"id", "name", "status"} + rows := [][]string{ + {"1", "foo", "active"}, + {"2", "bar", "inactive"}, + } + result, err := PrintData(headers, rows, FormatCSV) + require.NoError(t, err) + + lines := strings.Split(strings.TrimSpace(result), "\n") + require.Len(t, lines, 3) + require.Equal(t, "id,name,status", lines[0]) + require.Equal(t, "1,foo,active", lines[1]) + require.Equal(t, "2,bar,inactive", lines[2]) +} + +func TestPrintDataEmpty(t *testing.T) { + headers := []string{"id", "name"} + var rows [][]string + + result, err := PrintData(headers, rows, FormatJSON) + require.NoError(t, err) + require.Equal(t, "[]", result) + + result, err = PrintData(headers, rows, FormatCSV) + require.NoError(t, err) + require.Equal(t, "id,name\n", result) + + result, err = PrintData(headers, rows, FormatTable) + require.NoError(t, err) + require.Contains(t, result, "ID") + require.Contains(t, result, "NAME") +} diff --git a/src/uxHelpers/project.go b/src/uxHelpers/project.go index a313d3a6..e9882998 100644 --- a/src/uxHelpers/project.go +++ b/src/uxHelpers/project.go @@ -11,6 +11,7 @@ import ( "github.com/zeropsio/zcli/src/gn" "github.com/zeropsio/zcli/src/i18n" "github.com/zeropsio/zcli/src/optional" + "github.com/zeropsio/zcli/src/output" "github.com/zeropsio/zcli/src/uxBlock" "github.com/zeropsio/zcli/src/uxBlock/models/selector" "github.com/zeropsio/zcli/src/uxBlock/models/table" @@ -96,6 +97,7 @@ func PrintProjectList( ctx context.Context, restApiClient *zeropsRestApiClient.Handler, out io.Writer, + format output.Format, ) error { projects, err := repository.GetAllProjects(ctx, restApiClient) if err != nil { @@ -104,12 +106,38 @@ func PrintProjectList( header, body := createProjectTableRows(projects, false) - t := table.Render(body, table.WithHeader(header)) + headers := extractHeaders(header) + rows := extractRows(body) - _, err = fmt.Fprintln(out, t) + result, err := output.PrintData(headers, rows, format) + if err != nil { + return err + } + + _, err = fmt.Fprintln(out, result) return err } +func extractHeaders(header *table.Row) []string { + var h []string + for _, c := range header.Cells() { + h = append(h, c.String()) + } + return h +} + +func extractRows(body *table.Body) [][]string { + var rows [][]string + for _, r := range body.Rows() { + var row []string + for _, c := range r.Cells() { + row = append(row, c.String()) + } + rows = append(rows, row) + } + return rows +} + func createProjectTableRows(projects []entity.Project, createNewProject bool) (*table.Row, *table.Body) { header := table.NewRowFromStrings("id", "name", "org name", "org id", "status", "mode") diff --git a/src/uxHelpers/service.go b/src/uxHelpers/service.go index bcce733e..052cc604 100644 --- a/src/uxHelpers/service.go +++ b/src/uxHelpers/service.go @@ -11,6 +11,7 @@ import ( "github.com/zeropsio/zcli/src/gn" "github.com/zeropsio/zcli/src/i18n" "github.com/zeropsio/zcli/src/optional" + "github.com/zeropsio/zcli/src/output" "github.com/zeropsio/zcli/src/uxBlock" "github.com/zeropsio/zcli/src/uxBlock/models/selector" "github.com/zeropsio/zcli/src/uxBlock/models/table" @@ -75,6 +76,7 @@ func PrintServiceList( restApiClient *zeropsRestApiClient.Handler, out io.Writer, project entity.Project, + format output.Format, ) error { services, err := repository.GetNonSystemServicesByProject(ctx, restApiClient, project) if err != nil { @@ -83,9 +85,15 @@ func PrintServiceList( header, body := createServiceTableRows(services, false) - t := table.Render(body, table.WithHeader(header)) + headers := extractHeaders(header) + rows := extractRows(body) - _, err = fmt.Fprintln(out, t) + result, err := output.PrintData(headers, rows, format) + if err != nil { + return err + } + + _, err = fmt.Fprintln(out, result) return err }