diff --git a/pkg/cmd/consumer/list/list.go b/pkg/cmd/consumer/list/list.go index 75b40c3..f57460e 100644 --- a/pkg/cmd/consumer/list/list.go +++ b/pkg/cmd/consumer/list/list.go @@ -40,6 +40,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") return actionRun(opts) @@ -95,7 +98,7 @@ func actionRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { return cmdutil.NewExporter(opts.Output, opts.IO.Out).Write(resp.List) } diff --git a/pkg/cmd/context/list/list.go b/pkg/cmd/context/list/list.go index daa1823..1c460ce 100644 --- a/pkg/cmd/context/list/list.go +++ b/pkg/cmd/context/list/list.go @@ -30,6 +30,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } return listRun(opts, f) }, } @@ -49,7 +52,7 @@ func listRun(opts *Options, f *cmd.Factory) error { return nil } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { type contextJSON struct { Name string `json:"name"` Server string `json:"server"` diff --git a/pkg/cmd/credential/list/list.go b/pkg/cmd/credential/list/list.go index b90fda6..2c2f3d4 100644 --- a/pkg/cmd/credential/list/list.go +++ b/pkg/cmd/credential/list/list.go @@ -34,6 +34,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Consumer, _ = c.Flags().GetString("consumer") opts.Label, _ = c.Flags().GetString("label") @@ -98,7 +101,7 @@ func actionRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { return cmdutil.NewExporter(opts.Output, opts.IO.Out).Write(resp.List) } diff --git a/pkg/cmd/gateway-group/list/list.go b/pkg/cmd/gateway-group/list/list.go index 5365599..5c9cee3 100644 --- a/pkg/cmd/gateway-group/list/list.go +++ b/pkg/cmd/gateway-group/list/list.go @@ -36,6 +36,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.Label, _ = c.Flags().GetString("label") return listRun(opts) }, @@ -66,7 +69,7 @@ func listRun(opts *Options) error { return fmt.Errorf("failed to list gateway groups: %s", cmdutil.FormatAPIError(err)) } - if opts.Output == "json" || opts.Output == "yaml" { + if cmdutil.IsStructuredOutput(opts.Output) { exp := cmdutil.NewExporter(opts.Output, opts.IO.Out) var result api.ListResponse[api.GatewayGroup] if err := json.Unmarshal(body, &result); err != nil { diff --git a/pkg/cmd/global-rule/list/list.go b/pkg/cmd/global-rule/list/list.go index 8bfc373..cc47e8e 100644 --- a/pkg/cmd/global-rule/list/list.go +++ b/pkg/cmd/global-rule/list/list.go @@ -34,6 +34,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") return listRun(opts) @@ -88,7 +91,7 @@ func listRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) return exporter.Write(resp.List) } diff --git a/pkg/cmd/plugin/list/list.go b/pkg/cmd/plugin/list/list.go index f1c344b..8ed6139 100644 --- a/pkg/cmd/plugin/list/list.go +++ b/pkg/cmd/plugin/list/list.go @@ -36,6 +36,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") return actionRun(opts) }, @@ -74,8 +77,8 @@ func actionRun(opts *Options) error { return fmt.Errorf("failed to parse plugin list response: %w", err) } - if opts.Output == "json" { - return cmdutil.NewExporter("json", opts.IO.Out).Write(plugins) + if cmdutil.IsStructuredOutput(opts.Output) { + return cmdutil.NewExporter(opts.Output, opts.IO.Out).Write(plugins) } for _, name := range plugins { diff --git a/pkg/cmd/proto/list/list.go b/pkg/cmd/proto/list/list.go index f0782d7..8ae569d 100644 --- a/pkg/cmd/proto/list/list.go +++ b/pkg/cmd/proto/list/list.go @@ -33,6 +33,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") return actionRun(opts) @@ -88,7 +91,7 @@ func actionRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) return exporter.Write(resp.List) } diff --git a/pkg/cmd/route/list/list.go b/pkg/cmd/route/list/list.go index f677493..282a75b 100644 --- a/pkg/cmd/route/list/list.go +++ b/pkg/cmd/route/list/list.go @@ -35,6 +35,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") opts.ServiceID, _ = c.Flags().GetString("service-id") @@ -85,7 +88,7 @@ func actionRun(opts *Options) error { routes = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) return exporter.Write(routes) } diff --git a/pkg/cmd/secret/list/list.go b/pkg/cmd/secret/list/list.go index 4f7ee75..4a5b66f 100644 --- a/pkg/cmd/secret/list/list.go +++ b/pkg/cmd/secret/list/list.go @@ -33,6 +33,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") return actionRun(opts) @@ -88,7 +91,7 @@ func actionRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) return exporter.Write(api.RedactSecrets(resp.List)) } diff --git a/pkg/cmd/service/list/list.go b/pkg/cmd/service/list/list.go index 1cd0b9e..36456c3 100644 --- a/pkg/cmd/service/list/list.go +++ b/pkg/cmd/service/list/list.go @@ -33,6 +33,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") return actionRun(opts) @@ -87,7 +90,7 @@ func actionRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) return exporter.Write(resp.List) } diff --git a/pkg/cmd/ssl/list/list.go b/pkg/cmd/ssl/list/list.go index 80b0704..f526d2b 100644 --- a/pkg/cmd/ssl/list/list.go +++ b/pkg/cmd/ssl/list/list.go @@ -40,6 +40,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") return actionRun(opts) @@ -95,7 +98,7 @@ func actionRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { return cmdutil.NewExporter(opts.Output, opts.IO.Out).Write(api.RedactSSLs(resp.List)) } diff --git a/pkg/cmd/stream-route/list/list.go b/pkg/cmd/stream-route/list/list.go index 26b4e18..543a17b 100644 --- a/pkg/cmd/stream-route/list/list.go +++ b/pkg/cmd/stream-route/list/list.go @@ -34,6 +34,9 @@ func NewCmd(f *cmd.Factory) *cobra.Command { Args: cobra.NoArgs, RunE: func(c *cobra.Command, args []string) error { opts.Output, _ = c.Flags().GetString("output") + if err := cmdutil.ValidateOutputFormat(opts.Output); err != nil { + return err + } opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") opts.Label, _ = c.Flags().GetString("label") opts.ServiceID, _ = c.Flags().GetString("service-id") @@ -95,7 +98,7 @@ func actionRun(opts *Options) error { resp.List = filtered } - if opts.Output != "" { + if cmdutil.IsStructuredOutput(opts.Output) { exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) return exporter.Write(resp.List) } diff --git a/pkg/cmdutil/exporter.go b/pkg/cmdutil/exporter.go index 9b6d895..04d6acc 100644 --- a/pkg/cmdutil/exporter.go +++ b/pkg/cmdutil/exporter.go @@ -8,6 +8,30 @@ import ( "gopkg.in/yaml.v3" ) +// ValidateOutputFormat returns nil when the given --output value is one of +// the four shapes the CLI claims to support: empty (default table), "table", +// "json", "yaml". Anything else (typos, abbreviations, unknown formats) is +// rejected with a clear message listing the valid set. +// +// Every command that accepts `-o`/`--output` should call this before +// dispatching, so a typo like `-o jzon` fails fast with a helpful error +// instead of silently falling back to table rendering. +func ValidateOutputFormat(format string) error { + switch format { + case "", "table", "json", "yaml": + return nil + default: + return fmt.Errorf("unsupported output format: %q (valid: table, json, yaml)", format) + } +} + +// IsStructuredOutput returns true when the format should be rendered through +// the JSON/YAML exporter, false when the command should fall through to its +// own table renderer. Pairs with ValidateOutputFormat. +func IsStructuredOutput(format string) bool { + return format == "json" || format == "yaml" +} + // Exporter formats and writes data to the given writer. type Exporter struct { format string diff --git a/pkg/cmdutil/exporter_test.go b/pkg/cmdutil/exporter_test.go index eef8a99..81c0c8b 100644 --- a/pkg/cmdutil/exporter_test.go +++ b/pkg/cmdutil/exporter_test.go @@ -61,3 +61,35 @@ func TestExporter_WriteAPIResponse_InvalidJSON_ReturnsError(t *testing.T) { t.Fatalf("error did not mention decoding: %v", err) } } + +func TestValidateOutputFormat(t *testing.T) { + for _, ok := range []string{"", "table", "json", "yaml"} { + if err := ValidateOutputFormat(ok); err != nil { + t.Errorf("expected %q to be accepted, got %v", ok, err) + } + } + for _, bad := range []string{"jzon", "yml", "TABLE", "csv", "totally-not-a-format"} { + err := ValidateOutputFormat(bad) + if err == nil { + t.Errorf("expected %q to be rejected", bad) + continue + } + if !strings.Contains(err.Error(), "valid: table, json, yaml") { + t.Errorf("error for %q should list the valid set, got: %v", bad, err) + } + } +} + +func TestIsStructuredOutput(t *testing.T) { + cases := map[string]bool{ + "": false, + "table": false, + "json": true, + "yaml": true, + } + for format, want := range cases { + if got := IsStructuredOutput(format); got != want { + t.Errorf("IsStructuredOutput(%q) = %v, want %v", format, got, want) + } + } +}