From 77e24f8db6374c2a8e8d7a2202bc9528c81268af Mon Sep 17 00:00:00 2001 From: Taeyeon <90550065+TaeyeonRoyce@users.noreply.github.com> Date: Mon, 30 Sep 2024 19:46:01 +0900 Subject: [PATCH] Provide global output flag for all cli commands (#1021) Provide a consistent way for users to set the output format globally across all CLI commands. Implement JSON and YAML options for the output flag. Update validation for user input options to be global. Apply new output formats to: context ls, project create/ls/update, document ls, and history ls commands. --- cmd/yorkie/commands.go | 17 +++++++ cmd/yorkie/context/list.go | 64 +++++++++++++++++++++----- cmd/yorkie/delete_account.go | 2 +- cmd/yorkie/document/list.go | 79 ++++++++++++++++++++++---------- cmd/yorkie/history.go | 68 ++++++++++++++++++++-------- cmd/yorkie/output.go | 7 +++ cmd/yorkie/project/create.go | 31 ++++++++++--- cmd/yorkie/project/list.go | 84 ++++++++++++++++++++++++----------- cmd/yorkie/project/project.go | 6 +++ cmd/yorkie/project/update.go | 31 ++++++++++--- cmd/yorkie/version.go | 38 ++++------------ 11 files changed, 306 insertions(+), 121 deletions(-) create mode 100644 cmd/yorkie/output.go diff --git a/cmd/yorkie/commands.go b/cmd/yorkie/commands.go index 7e5418daa..0ea295900 100644 --- a/cmd/yorkie/commands.go +++ b/cmd/yorkie/commands.go @@ -18,6 +18,7 @@ package main import ( + "errors" "os" "path" @@ -55,4 +56,20 @@ func init() { rootCmd.PersistentFlags().String("rpc-addr", "localhost:8080", "Address of the rpc server") _ = viper.BindPFlag("rpcAddr", rootCmd.PersistentFlags().Lookup("rpc-addr")) + + rootCmd.PersistentFlags().StringP("output", "o", "", "One of 'yaml' or 'json'.") + _ = viper.BindPFlag("output", rootCmd.PersistentFlags().Lookup("output")) + + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + return validateOutputOpts() + } +} + +// validateOutputOpts validates the output options. +func validateOutputOpts() error { + output := viper.GetString("output") + if output != DefaultOutput && output != YamlOutput && output != JSONOutput { + return errors.New(`--output must be 'yaml' or 'json'`) + } + return nil } diff --git a/cmd/yorkie/context/list.go b/cmd/yorkie/context/list.go index e06387996..d6f4d8a02 100644 --- a/cmd/yorkie/context/list.go +++ b/cmd/yorkie/context/list.go @@ -17,15 +17,24 @@ package context import ( + "encoding/json" "fmt" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/spf13/viper" + "gopkg.in/yaml.v3" "github.com/yorkie-team/yorkie/cmd/yorkie/config" ) +type contextInfo struct { + Current string `json:"current" yaml:"current"` + RPCAddr string `json:"rpc_addr" yaml:"rpc_addr"` + Insecure string `json:"insecure" yaml:"insecure"` + Token string `json:"token" yaml:"token"` +} + func newListCommand() *cobra.Command { return &cobra.Command{ Use: "ls", @@ -37,14 +46,7 @@ func newListCommand() *cobra.Command { return err } - tw := table.NewWriter() - tw.Style().Options.DrawBorder = false - tw.Style().Options.SeparateColumns = false - tw.Style().Options.SeparateFooter = false - tw.Style().Options.SeparateHeader = false - tw.Style().Options.SeparateRows = false - - tw.AppendHeader(table.Row{"CURRENT", "RPC ADDR", "INSECURE", "TOKEN"}) + contexts := make([]contextInfo, 0, len(conf.Auths)) for rpcAddr, auth := range conf.Auths { current := "" if rpcAddr == viper.GetString("rpcAddr") { @@ -61,16 +63,58 @@ func newListCommand() *cobra.Command { ellipsisToken = auth.Token[:10] + "..." + auth.Token[len(auth.Token)-10:] } - tw.AppendRow(table.Row{current, rpcAddr, insecure, ellipsisToken}) + contexts = append(contexts, contextInfo{ + Current: current, + RPCAddr: rpcAddr, + Insecure: insecure, + Token: ellipsisToken, + }) } - fmt.Println(tw.Render()) + output := viper.GetString("output") + if err := printContexts(cmd, output, contexts); err != nil { + return err + } return nil }, } } +func printContexts(cmd *cobra.Command, output string, contexts []contextInfo) error { + switch output { + case "": + tw := table.NewWriter() + tw.Style().Options.DrawBorder = false + tw.Style().Options.SeparateColumns = false + tw.Style().Options.SeparateFooter = false + tw.Style().Options.SeparateHeader = false + tw.Style().Options.SeparateRows = false + + tw.AppendHeader(table.Row{"CURRENT", "RPC ADDR", "INSECURE", "TOKEN"}) + for _, ctx := range contexts { + tw.AppendRow(table.Row{ctx.Current, ctx.RPCAddr, ctx.Insecure, ctx.Token}) + } + cmd.Println(tw.Render()) + case "json": + marshalled, err := json.MarshalIndent(contexts, "", " ") + if err != nil { + return fmt.Errorf("marshal JSON: %w", err) + } + cmd.Println(string(marshalled)) + case "yaml": + marshalled, err := yaml.Marshal(contexts) + if err != nil { + return fmt.Errorf("marshal YAML: %w", err) + } + cmd.Println(string(marshalled)) + default: + return fmt.Errorf("unknown output format: %s", output) + } + + return nil +} + func init() { SubCmd.AddCommand(newListCommand()) } diff --git a/cmd/yorkie/delete_account.go b/cmd/yorkie/delete_account.go index 6e1f61273..96da991c4 100644 --- a/cmd/yorkie/delete_account.go +++ b/cmd/yorkie/delete_account.go @@ -61,7 +61,7 @@ func deleteAccountCmd() *cobra.Command { } if err := deleteAccount(conf, auth, rpcAddr, username, password); err != nil { - fmt.Println("Failed to delete account: ", err) + fmt.Println("delete account: ", err) } return nil diff --git a/cmd/yorkie/document/list.go b/cmd/yorkie/document/list.go index 71d886e87..feb59cf53 100644 --- a/cmd/yorkie/document/list.go +++ b/cmd/yorkie/document/list.go @@ -18,14 +18,18 @@ package document import ( "context" + "encoding/json" "errors" + "fmt" "time" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/spf13/viper" + "gopkg.in/yaml.v3" "github.com/yorkie-team/yorkie/admin" + "github.com/yorkie-team/yorkie/api/types" "github.com/yorkie-team/yorkie/cmd/yorkie/config" "github.com/yorkie-team/yorkie/pkg/units" ) @@ -68,36 +72,63 @@ func newListCommand() *cobra.Command { return err } - tw := table.NewWriter() - tw.Style().Options.DrawBorder = false - tw.Style().Options.SeparateColumns = false - tw.Style().Options.SeparateFooter = false - tw.Style().Options.SeparateHeader = false - tw.Style().Options.SeparateRows = false - tw.AppendHeader(table.Row{ - "ID", - "KEY", - "CREATED AT", - "ACCESSED AT", - "UPDATED AT", - "SNAPSHOT", - }) - for _, document := range documents { - tw.AppendRow(table.Row{ - document.ID, - document.Key, - units.HumanDuration(time.Now().UTC().Sub(document.CreatedAt)), - units.HumanDuration(time.Now().UTC().Sub(document.AccessedAt)), - units.HumanDuration(time.Now().UTC().Sub(document.UpdatedAt)), - document.Snapshot, - }) + output := viper.GetString("output") + if err := printDocuments(cmd, output, documents); err != nil { + return err } - cmd.Printf("%s\n", tw.Render()) + return nil }, } } +func printDocuments(cmd *cobra.Command, output string, documents []*types.DocumentSummary) error { + switch output { + case "": + tw := table.NewWriter() + tw.Style().Options.DrawBorder = false + tw.Style().Options.SeparateColumns = false + tw.Style().Options.SeparateFooter = false + tw.Style().Options.SeparateHeader = false + tw.Style().Options.SeparateRows = false + tw.AppendHeader(table.Row{ + "ID", + "KEY", + "CREATED AT", + "ACCESSED AT", + "UPDATED AT", + "SNAPSHOT", + }) + for _, document := range documents { + tw.AppendRow(table.Row{ + document.ID, + document.Key, + units.HumanDuration(time.Now().UTC().Sub(document.CreatedAt)), + units.HumanDuration(time.Now().UTC().Sub(document.AccessedAt)), + units.HumanDuration(time.Now().UTC().Sub(document.UpdatedAt)), + document.Snapshot, + }) + } + cmd.Printf("%s\n", tw.Render()) + case "json": + jsonOutput, err := json.MarshalIndent(documents, "", " ") + if err != nil { + return fmt.Errorf("marshal JSON: %w", err) + } + cmd.Println(string(jsonOutput)) + case "yaml": + yamlOutput, err := yaml.Marshal(documents) + if err != nil { + return fmt.Errorf("failed to marshal YAML: %w", err) + } + cmd.Println(string(yamlOutput)) + default: + return fmt.Errorf("unknown output format: %s", output) + } + + return nil +} + func init() { cmd := newListCommand() cmd.Flags().StringVar( diff --git a/cmd/yorkie/history.go b/cmd/yorkie/history.go index 73782bbe2..cdc7700a0 100644 --- a/cmd/yorkie/history.go +++ b/cmd/yorkie/history.go @@ -18,13 +18,17 @@ package main import ( "context" + "encoding/json" "errors" + "fmt" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/spf13/viper" + "gopkg.in/yaml.v3" "github.com/yorkie-team/yorkie/admin" + "github.com/yorkie-team/yorkie/api/types" "github.com/yorkie-team/yorkie/cmd/yorkie/config" "github.com/yorkie-team/yorkie/pkg/document/key" ) @@ -44,7 +48,6 @@ func newHistoryCmd() *cobra.Command { if len(args) != 2 { return errors.New("project name and document key are required") } - rpcAddr := viper.GetString("rpcAddr") auth, err := config.LoadAuth(rpcAddr) if err != nil { @@ -72,30 +75,57 @@ func newHistoryCmd() *cobra.Command { return err } - tw := table.NewWriter() - tw.Style().Options.DrawBorder = false - tw.Style().Options.SeparateColumns = false - tw.Style().Options.SeparateFooter = false - tw.Style().Options.SeparateHeader = false - tw.Style().Options.SeparateRows = false - tw.AppendHeader(table.Row{ - "SEQ", - "MESSAGE", - "SNAPSHOT", - }) - for _, change := range changes { - tw.AppendRow(table.Row{ - change.ID.ServerSeq(), - change.Message, - change.Snapshot, - }) + output := viper.GetString("output") + if err := printHistories(cmd, output, changes); err != nil { + return err } - cmd.Printf("%s\n", tw.Render()) + return nil }, } } +func printHistories(cmd *cobra.Command, output string, changes []*types.ChangeSummary) error { + switch output { + case DefaultOutput: + tw := table.NewWriter() + tw.Style().Options.DrawBorder = false + tw.Style().Options.SeparateColumns = false + tw.Style().Options.SeparateFooter = false + tw.Style().Options.SeparateHeader = false + tw.Style().Options.SeparateRows = false + tw.AppendHeader(table.Row{ + "SEQ", + "MESSAGE", + "SNAPSHOT", + }) + for _, change := range changes { + tw.AppendRow(table.Row{ + change.ID.ServerSeq(), + change.Message, + change.Snapshot, + }) + } + cmd.Printf("%s\n", tw.Render()) + case JSONOutput: + jsonOutput, err := json.MarshalIndent(changes, "", " ") + if err != nil { + return fmt.Errorf("marshal JSON: %w", err) + } + cmd.Println(string(jsonOutput)) + case YamlOutput: + yamlOutput, err := yaml.Marshal(changes) + if err != nil { + return fmt.Errorf("marshal YAML: %w", err) + } + cmd.Println(string(yamlOutput)) + default: + return fmt.Errorf("unknown output format: %s", output) + } + + return nil +} + func init() { cmd := newHistoryCmd() cmd.Flags().Int64Var( diff --git a/cmd/yorkie/output.go b/cmd/yorkie/output.go new file mode 100644 index 000000000..8b5f3a3f8 --- /dev/null +++ b/cmd/yorkie/output.go @@ -0,0 +1,7 @@ +package main + +const ( + DefaultOutput = "" // DefaultOutput is for table format + YamlOutput = "yaml" // YamlOutput is for yaml format + JSONOutput = "json" // JSONOutput is for json format +) diff --git a/cmd/yorkie/project/create.go b/cmd/yorkie/project/create.go index 5e1f0416b..6707ae45d 100644 --- a/cmd/yorkie/project/create.go +++ b/cmd/yorkie/project/create.go @@ -26,8 +26,10 @@ import ( "github.com/spf13/viper" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/status" + "gopkg.in/yaml.v3" "github.com/yorkie-team/yorkie/admin" + "github.com/yorkie-team/yorkie/api/types" "github.com/yorkie-team/yorkie/cmd/yorkie/config" ) @@ -72,18 +74,37 @@ func newCreateCommand() *cobra.Command { return err } - encoded, err := json.Marshal(project) - if err != nil { - return fmt.Errorf("marshal project: %w", err) + output := viper.GetString("output") + if err := printCreateProjectInfo(cmd, output, project); err != nil { + return err } - cmd.Println(string(encoded)) - return nil }, } } +func printCreateProjectInfo(cmd *cobra.Command, output string, project *types.Project) error { + switch output { + case JSONOutput, DefaultOutput: + encoded, err := json.Marshal(project) + if err != nil { + return fmt.Errorf("marshal JSON: %w", err) + } + cmd.Println(string(encoded)) + case YamlOutput: + marshalled, err := yaml.Marshal(project) + if err != nil { + return fmt.Errorf("marshal YAML: %w", err) + } + cmd.Println(string(marshalled)) + default: + return fmt.Errorf("unknown output format: %s", output) + } + + return nil +} + func init() { SubCmd.AddCommand(newCreateCommand()) } diff --git a/cmd/yorkie/project/list.go b/cmd/yorkie/project/list.go index 14a9ecbaf..a05f24d61 100644 --- a/cmd/yorkie/project/list.go +++ b/cmd/yorkie/project/list.go @@ -18,13 +18,17 @@ package project import ( "context" + "encoding/json" + "fmt" "time" "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/spf13/viper" + "gopkg.in/yaml.v3" "github.com/yorkie-team/yorkie/admin" + "github.com/yorkie-team/yorkie/api/types" "github.com/yorkie-team/yorkie/cmd/yorkie/config" "github.com/yorkie-team/yorkie/pkg/units" ) @@ -36,6 +40,7 @@ func newListCommand() *cobra.Command { PreRunE: config.Preload, RunE: func(cmd *cobra.Command, args []string) error { rpcAddr := viper.GetString("rpcAddr") + auth, err := config.LoadAuth(rpcAddr) if err != nil { return err @@ -55,39 +60,66 @@ func newListCommand() *cobra.Command { return err } - tw := table.NewWriter() - tw.Style().Options.DrawBorder = false - tw.Style().Options.SeparateColumns = false - tw.Style().Options.SeparateFooter = false - tw.Style().Options.SeparateHeader = false - tw.Style().Options.SeparateRows = false - tw.AppendHeader(table.Row{ - "NAME", - "PUBLIC KEY", - "SECRET KEY", - "AUTH WEBHOOK URL", - "AUTH WEBHOOK METHODS", - "CLIENT DEACTIVATE THRESHOLD", - "CREATED AT", - }) - for _, project := range projects { - tw.AppendRow(table.Row{ - project.Name, - project.PublicKey, - project.SecretKey, - project.AuthWebhookURL, - project.AuthWebhookMethods, - project.ClientDeactivateThreshold, - units.HumanDuration(time.Now().UTC().Sub(project.CreatedAt)), - }) + output := viper.GetString("output") + err2 := printProjects(cmd, output, projects) + if err2 != nil { + return err2 } - cmd.Printf("%s\n", tw.Render()) return nil }, } } +func printProjects(cmd *cobra.Command, output string, projects []*types.Project) error { + switch output { + case DefaultOutput: + tw := table.NewWriter() + tw.Style().Options.DrawBorder = false + tw.Style().Options.SeparateColumns = false + tw.Style().Options.SeparateFooter = false + tw.Style().Options.SeparateHeader = false + tw.Style().Options.SeparateRows = false + tw.AppendHeader(table.Row{ + "NAME", + "PUBLIC KEY", + "SECRET KEY", + "AUTH WEBHOOK URL", + "AUTH WEBHOOK METHODS", + "CLIENT DEACTIVATE THRESHOLD", + "CREATED AT", + }) + for _, project := range projects { + tw.AppendRow(table.Row{ + project.Name, + project.PublicKey, + project.SecretKey, + project.AuthWebhookURL, + project.AuthWebhookMethods, + project.ClientDeactivateThreshold, + units.HumanDuration(time.Now().UTC().Sub(project.CreatedAt)), + }) + } + cmd.Println(tw.Render()) + case JSONOutput: + jsonOutput, err := json.MarshalIndent(projects, "", " ") + if err != nil { + return fmt.Errorf("marshal JSON: %w", err) + } + cmd.Println(string(jsonOutput)) + case YamlOutput: + yamlOutput, err := yaml.Marshal(projects) + if err != nil { + return fmt.Errorf("marshal YAML: %w", err) + } + cmd.Println(string(yamlOutput)) + default: + return fmt.Errorf("unknown output format: %s", output) + } + + return nil +} + func init() { SubCmd.AddCommand(newListCommand()) } diff --git a/cmd/yorkie/project/project.go b/cmd/yorkie/project/project.go index 4e45ed7fa..9eec11816 100644 --- a/cmd/yorkie/project/project.go +++ b/cmd/yorkie/project/project.go @@ -26,3 +26,9 @@ var ( Short: "Manage projects", } ) + +const ( + DefaultOutput = "" // DefaultOutput is for table format + YamlOutput = "yaml" // YamlOutput is for yaml format + JSONOutput = "json" // JSONOutput is for json format +) diff --git a/cmd/yorkie/project/update.go b/cmd/yorkie/project/update.go index c26eb94be..9684e3900 100644 --- a/cmd/yorkie/project/update.go +++ b/cmd/yorkie/project/update.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/viper" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/status" + "gopkg.in/yaml.v3" "github.com/yorkie-team/yorkie/admin" "github.com/yorkie-team/yorkie/api/types" @@ -48,7 +49,6 @@ func newUpdateCommand() *cobra.Command { if len(args) != 1 { return errors.New("name is required") } - name := args[0] rpcAddr := viper.GetString("rpcAddr") @@ -109,18 +109,37 @@ func newUpdateCommand() *cobra.Command { return err } - encoded, err := json.Marshal(updated) - if err != nil { - return fmt.Errorf("marshal project: %w", err) + output := viper.GetString("output") + if err := printUpdateProjectInfo(cmd, output, updated); err != nil { + return err } - cmd.Println(string(encoded)) - return nil }, } } +func printUpdateProjectInfo(cmd *cobra.Command, output string, project *types.Project) error { + switch output { + case JSONOutput, DefaultOutput: + encoded, err := json.Marshal(project) + if err != nil { + return fmt.Errorf("marshal JSON: %w", err) + } + cmd.Println(string(encoded)) + case YamlOutput: + encoded, err := yaml.Marshal(project) + if err != nil { + return fmt.Errorf("marshal YAML: %w", err) + } + cmd.Println(string(encoded)) + default: + return fmt.Errorf("unknown output format: %s", output) + } + + return nil +} + func init() { cmd := newUpdateCommand() cmd.Flags().StringVar( diff --git a/cmd/yorkie/version.go b/cmd/yorkie/version.go index 7207f76db..5c8df95a2 100644 --- a/cmd/yorkie/version.go +++ b/cmd/yorkie/version.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "runtime" "connectrpc.com/connect" @@ -44,10 +45,6 @@ func newVersionCmd() *cobra.Command { Short: "Print the version number of Yorkie", PreRunE: config.Preload, RunE: func(cmd *cobra.Command, args []string) error { - if err := validateOutputOpts(); err != nil { - return err - } - info := types.VersionInfo{ ClientVersion: clientVersion(), } @@ -57,6 +54,7 @@ func newVersionCmd() *cobra.Command { info.ServerVersion, serverErr = fetchServerVersion() } + output := viper.GetString("output") if err := printVersionInfo(cmd, output, &info); err != nil { return err } @@ -119,7 +117,7 @@ func printServerError(cmd *cobra.Command, err error) { func printVersionInfo(cmd *cobra.Command, output string, versionInfo *types.VersionInfo) error { switch output { - case "": + case DefaultOutput: cmd.Printf("Yorkie Client: %s\n", versionInfo.ClientVersion.YorkieVersion) cmd.Printf("Go: %s\n", versionInfo.ClientVersion.GoVersion) cmd.Printf("Build Date: %s\n", versionInfo.ClientVersion.BuildDate) @@ -128,29 +126,20 @@ func printVersionInfo(cmd *cobra.Command, output string, versionInfo *types.Vers cmd.Printf("Go: %s\n", versionInfo.ServerVersion.GoVersion) cmd.Printf("Build Date: %s\n", versionInfo.ServerVersion.BuildDate) } - case "yaml": + case YamlOutput: marshalled, err := yaml.Marshal(versionInfo) if err != nil { - return errors.New("marshal YAML") + return fmt.Errorf("marshal YAML: %w", err) } cmd.Println(string(marshalled)) - case "json": + case JSONOutput: marshalled, err := json.MarshalIndent(versionInfo, "", " ") if err != nil { - return errors.New("marshal JSON") + return fmt.Errorf("marshal JSON: %w", err) } cmd.Println(string(marshalled)) default: - return errors.New("unknown output format") - } - - return nil -} - -// validateOutputOpts validates the output options. -func validateOutputOpts() error { - if output != "" && output != "yaml" && output != "json" { - return errors.New(`--output must be 'yaml' or 'json'`) + return fmt.Errorf("unknown output format: %s", output) } return nil @@ -166,16 +155,5 @@ func init() { "Shows client version only (no server required).", ) - // TODO(hackerwins): Output format should be configurable globally. - // So, we need to move this to the root command like `--rpc-addr` and - // apply it to all subcommands that print output. - cmd.Flags().StringVarP( - &output, - "output", - "o", - output, - "One of 'yaml' or 'json'.", - ) - rootCmd.AddCommand(cmd) }