From a12c04b8b572d299974bbfde0a743aed4811d8bc Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Tue, 23 Apr 2024 14:54:31 -0700 Subject: [PATCH 1/6] Show plaintext response for config --list --- cmd/config/config.go | 11 +++-- internal/output/config_outputter.go | 54 ++++++++++++++++++++++++ internal/output/config_outputter_test.go | 39 +++++++++++++++++ internal/output/multiple_outputter.go | 2 +- internal/output/output.go | 4 +- internal/output/singular_outputter.go | 2 +- 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 internal/output/config_outputter.go create mode 100644 internal/output/config_outputter_test.go diff --git a/cmd/config/config.go b/cmd/config/config.go index f9779e62..a161cf50 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -13,6 +13,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/internal/analytics" "ldcli/internal/config" + "ldcli/internal/output" ) const ( @@ -63,11 +64,15 @@ func run() func(*cobra.Command, []string) error { return err } - if string(configJSON) == "{}" { - return nil + output, err := output.CmdOutput( + viper.GetString(cliflags.OutputFlag), + output.NewConfigOutputterFn(configJSON), + ) + if err != nil { + return err } - fmt.Fprint(cmd.OutOrStdout(), string(configJSON)+"\n") + fmt.Fprintf(cmd.OutOrStdout(), output+"\n") case viper.GetBool(SetFlag): // flag needs two arguments: a key and value if len(args)%2 != 0 { diff --git a/internal/output/config_outputter.go b/internal/output/config_outputter.go new file mode 100644 index 00000000..07aaa7bd --- /dev/null +++ b/internal/output/config_outputter.go @@ -0,0 +1,54 @@ +package output + +import ( + "encoding/json" + "fmt" + "strings" +) + +var configPlaintextOutputFn = func(r configResource) string { + lst := make([]string, 0) + for k, v := range r { + lst = append(lst, fmt.Sprintf("%s: %s", k, v)) + } + + return strings.Join(lst, "\n") +} + +func NewConfigOutputterFn(input []byte) configOutputterFn { + return configOutputterFn{ + input: input, + } +} + +type configOutputterFn struct { + input []byte +} + +func (o configOutputterFn) New() (Outputter, error) { + var r configResource + err := json.Unmarshal(o.input, &r) + if err != nil { + return ConfigOutputter{}, err + } + + return ConfigOutputter{ + outputFn: configPlaintextOutputFn, + resource: r, + resourceJSON: o.input, + }, nil +} + +type ConfigOutputter struct { + outputFn PlaintextOutputFn[configResource] + resource configResource + resourceJSON []byte +} + +func (o ConfigOutputter) JSON() string { + return string(o.resourceJSON) +} + +func (o ConfigOutputter) String() string { + return formatColl([]configResource{o.resource}, o.outputFn) +} diff --git a/internal/output/config_outputter_test.go b/internal/output/config_outputter_test.go new file mode 100644 index 00000000..eaf35657 --- /dev/null +++ b/internal/output/config_outputter_test.go @@ -0,0 +1,39 @@ +package output_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "ldcli/internal/output" +) + +func TestConfigOutputter_JSON(t *testing.T) { + input := []byte(`{ + "access-token": "test-access-token", + "base-uri": "test-base-uri" + }`) + output, err := output.CmdOutput( + "json", + output.NewConfigOutputterFn(input), + ) + + require.NoError(t, err) + assert.JSONEq(t, output, string(input)) +} + +func TestConfigOutputter_String(t *testing.T) { + input := []byte(`{ + "access-token": "test-access-token", + "base-uri": "test-base-uri" + }`) + expected := "access-token: test-access-token\nbase-uri: test-base-uri" + output, err := output.CmdOutput( + "plaintext", + output.NewConfigOutputterFn(input), + ) + + require.NoError(t, err) + assert.Equal(t, expected, output) +} diff --git a/internal/output/multiple_outputter.go b/internal/output/multiple_outputter.go index 97e24505..19b4b3c2 100644 --- a/internal/output/multiple_outputter.go +++ b/internal/output/multiple_outputter.go @@ -35,7 +35,7 @@ func (o multipleOutputterFn) New() (Outputter, error) { } type MultipleOutputter struct { - outputFn PlaintextOutputFn + outputFn PlaintextOutputFn[resource] resources resources resourceJSON []byte } diff --git a/internal/output/output.go b/internal/output/output.go index eaea59e4..098ef5ce 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -23,7 +23,7 @@ type OutputterFn interface { } // PlaintextOutputFn represents the various ways to output a resource or resources. -type PlaintextOutputFn func(resource) string +type PlaintextOutputFn[T any] func(t T) string // resource is the subset of data we need to display a command's plain text response for a single // resource. @@ -38,6 +38,8 @@ type resources struct { Items []resource `json:"items"` } +type configResource map[string]string + // CmdOutput returns a command's response as a string formatted based on the user's requested type. func CmdOutput(outputKind string, outputter OutputterFn) (string, error) { o, err := outputter.New() diff --git a/internal/output/singular_outputter.go b/internal/output/singular_outputter.go index 4c953c55..42055efe 100644 --- a/internal/output/singular_outputter.go +++ b/internal/output/singular_outputter.go @@ -35,7 +35,7 @@ func (o singularOutputterFn) New() (Outputter, error) { } type SingularOutputter struct { - outputFn PlaintextOutputFn + outputFn PlaintextOutputFn[resource] resource resource resourceJSON []byte } From d4ee04c145dce24e47e64bd57404487fd0f7861e Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Tue, 23 Apr 2024 15:01:58 -0700 Subject: [PATCH 2/6] Use map for output type --- internal/output/config_outputter.go | 11 +++++++++-- internal/output/output.go | 2 +- internal/output/singular_outputter.go | 12 ++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/output/config_outputter.go b/internal/output/config_outputter.go index 07aaa7bd..58d349a6 100644 --- a/internal/output/config_outputter.go +++ b/internal/output/config_outputter.go @@ -3,13 +3,20 @@ package output import ( "encoding/json" "fmt" + "sort" "strings" ) var configPlaintextOutputFn = func(r configResource) string { + keys := make([]string, 0) + for k := range r { + keys = append(keys, k) + } + sort.Strings(keys) + lst := make([]string, 0) - for k, v := range r { - lst = append(lst, fmt.Sprintf("%s: %s", k, v)) + for _, k := range keys { + lst = append(lst, fmt.Sprintf("%s: %s", k, r[k])) } return strings.Join(lst, "\n") diff --git a/internal/output/output.go b/internal/output/output.go index 098ef5ce..a2eb7ddc 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -38,7 +38,7 @@ type resources struct { Items []resource `json:"items"` } -type configResource map[string]string +type configResource map[string]interface{} // CmdOutput returns a command's response as a string formatted based on the user's requested type. func CmdOutput(outputKind string, outputter OutputterFn) (string, error) { diff --git a/internal/output/singular_outputter.go b/internal/output/singular_outputter.go index 42055efe..3d8f50dd 100644 --- a/internal/output/singular_outputter.go +++ b/internal/output/singular_outputter.go @@ -5,8 +5,8 @@ import ( "fmt" ) -var singularPlaintextOutputFn = func(r resource) string { - return fmt.Sprintf("%s (%s)", r.Name, r.Key) +var singularPlaintextOutputFn = func(r configResource) string { + return fmt.Sprintf("%s (%s)", r["name"], r["key"]) } // TODO: rename this to be "cleaner"? -- NewSingularOutput() @@ -21,7 +21,7 @@ type singularOutputterFn struct { } func (o singularOutputterFn) New() (Outputter, error) { - var r resource + var r configResource err := json.Unmarshal(o.input, &r) if err != nil { return SingularOutputter{}, err @@ -35,8 +35,8 @@ func (o singularOutputterFn) New() (Outputter, error) { } type SingularOutputter struct { - outputFn PlaintextOutputFn[resource] - resource resource + outputFn PlaintextOutputFn[configResource] + resource configResource resourceJSON []byte } @@ -45,5 +45,5 @@ func (o SingularOutputter) JSON() string { } func (o SingularOutputter) String() string { - return formatColl([]resource{o.resource}, o.outputFn) + return formatColl([]configResource{o.resource}, o.outputFn) } From 6e62d33de6b34646719cdfc3ce74d2c180af9cbb Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Tue, 23 Apr 2024 15:06:35 -0700 Subject: [PATCH 3/6] Refactor types --- internal/output/config_outputter.go | 10 +++++----- internal/output/multiple_outputter.go | 2 +- internal/output/output.go | 7 +------ internal/output/singular_outputter.go | 10 +++++----- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/internal/output/config_outputter.go b/internal/output/config_outputter.go index 58d349a6..385d93bf 100644 --- a/internal/output/config_outputter.go +++ b/internal/output/config_outputter.go @@ -7,7 +7,7 @@ import ( "strings" ) -var configPlaintextOutputFn = func(r configResource) string { +var configPlaintextOutputFn = func(r resource) string { keys := make([]string, 0) for k := range r { keys = append(keys, k) @@ -33,7 +33,7 @@ type configOutputterFn struct { } func (o configOutputterFn) New() (Outputter, error) { - var r configResource + var r resource err := json.Unmarshal(o.input, &r) if err != nil { return ConfigOutputter{}, err @@ -47,8 +47,8 @@ func (o configOutputterFn) New() (Outputter, error) { } type ConfigOutputter struct { - outputFn PlaintextOutputFn[configResource] - resource configResource + outputFn PlaintextOutputFn[resource] + resource resource resourceJSON []byte } @@ -57,5 +57,5 @@ func (o ConfigOutputter) JSON() string { } func (o ConfigOutputter) String() string { - return formatColl([]configResource{o.resource}, o.outputFn) + return formatColl([]resource{o.resource}, o.outputFn) } diff --git a/internal/output/multiple_outputter.go b/internal/output/multiple_outputter.go index 19b4b3c2..efcb1656 100644 --- a/internal/output/multiple_outputter.go +++ b/internal/output/multiple_outputter.go @@ -6,7 +6,7 @@ import ( ) var multiplePlaintextOutputFn = func(r resource) string { - return fmt.Sprintf("* %s (%s)", r.Name, r.Key) + return fmt.Sprintf("* %s (%s)", r["name"], r["key"]) } // TODO: rename this to be "cleaner"? -- NewMultipleOutput() diff --git a/internal/output/output.go b/internal/output/output.go index a2eb7ddc..0f6892be 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -27,10 +27,7 @@ type PlaintextOutputFn[T any] func(t T) string // resource is the subset of data we need to display a command's plain text response for a single // resource. -type resource struct { - Key string `json:"key"` - Name string `json:"name"` -} +type resource map[string]interface{} // resources is the subset of data we need to display a command's plain text response for a list // of resources. @@ -38,8 +35,6 @@ type resources struct { Items []resource `json:"items"` } -type configResource map[string]interface{} - // CmdOutput returns a command's response as a string formatted based on the user's requested type. func CmdOutput(outputKind string, outputter OutputterFn) (string, error) { o, err := outputter.New() diff --git a/internal/output/singular_outputter.go b/internal/output/singular_outputter.go index 3d8f50dd..d4999d53 100644 --- a/internal/output/singular_outputter.go +++ b/internal/output/singular_outputter.go @@ -5,7 +5,7 @@ import ( "fmt" ) -var singularPlaintextOutputFn = func(r configResource) string { +var singularPlaintextOutputFn = func(r resource) string { return fmt.Sprintf("%s (%s)", r["name"], r["key"]) } @@ -21,7 +21,7 @@ type singularOutputterFn struct { } func (o singularOutputterFn) New() (Outputter, error) { - var r configResource + var r resource err := json.Unmarshal(o.input, &r) if err != nil { return SingularOutputter{}, err @@ -35,8 +35,8 @@ func (o singularOutputterFn) New() (Outputter, error) { } type SingularOutputter struct { - outputFn PlaintextOutputFn[configResource] - resource configResource + outputFn PlaintextOutputFn[resource] + resource resource resourceJSON []byte } @@ -45,5 +45,5 @@ func (o SingularOutputter) JSON() string { } func (o SingularOutputter) String() string { - return formatColl([]configResource{o.resource}, o.outputFn) + return formatColl([]resource{o.resource}, o.outputFn) } From 065ebb32fe39826e5a36316e3ff34d48f73ad784 Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Tue, 23 Apr 2024 15:12:58 -0700 Subject: [PATCH 4/6] Organization --- cmd/config/config.go | 2 +- cmd/environments/get.go | 2 +- cmd/flags/create.go | 2 +- cmd/flags/get.go | 2 +- cmd/flags/update.go | 2 +- cmd/members/create.go | 2 +- cmd/members/invite.go | 2 +- cmd/projects/create.go | 2 +- cmd/projects/list.go | 2 +- internal/output/config_outputter.go | 12 ++++++------ internal/output/config_outputter_test.go | 4 ++-- internal/output/multiple_outputter.go | 13 ++++++------- internal/output/multiple_outputter_test.go | 4 ++-- internal/output/singular_outputter.go | 13 ++++++------- internal/output/singular_outputter_test.go | 4 ++-- 15 files changed, 33 insertions(+), 35 deletions(-) diff --git a/cmd/config/config.go b/cmd/config/config.go index a161cf50..2f62d73d 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -66,7 +66,7 @@ func run() func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewConfigOutputterFn(configJSON), + output.NewConfigOutput(configJSON), ) if err != nil { return err diff --git a/cmd/environments/get.go b/cmd/environments/get.go index bc3366ef..08245463 100644 --- a/cmd/environments/get.go +++ b/cmd/environments/get.go @@ -68,7 +68,7 @@ func runGet( output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutputterFn(response), + output.NewSingularOutput(response), ) if err != nil { return err diff --git a/cmd/flags/create.go b/cmd/flags/create.go index 18e54874..9f8c60c6 100644 --- a/cmd/flags/create.go +++ b/cmd/flags/create.go @@ -77,7 +77,7 @@ func runCreate(client flags.Client) func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutputterFn(response), + output.NewSingularOutput(response), ) if err != nil { return err diff --git a/cmd/flags/get.go b/cmd/flags/get.go index 49912574..ab1aef78 100644 --- a/cmd/flags/get.go +++ b/cmd/flags/get.go @@ -76,7 +76,7 @@ func runGet(client flags.Client) func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutputterFn(response), + output.NewSingularOutput(response), ) if err != nil { return err diff --git a/cmd/flags/update.go b/cmd/flags/update.go index ed3ed815..c61180c4 100644 --- a/cmd/flags/update.go +++ b/cmd/flags/update.go @@ -147,7 +147,7 @@ func runUpdate(client flags.Client) func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutputterFn(response), + output.NewSingularOutput(response), ) if err != nil { return err diff --git a/cmd/members/create.go b/cmd/members/create.go index 19f92823..a1860706 100644 --- a/cmd/members/create.go +++ b/cmd/members/create.go @@ -57,7 +57,7 @@ func runCreate(client members.Client) func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutputterFn(response), + output.NewSingularOutput(response), ) if err != nil { return err diff --git a/cmd/members/invite.go b/cmd/members/invite.go index 6190d4d7..70c56e6e 100644 --- a/cmd/members/invite.go +++ b/cmd/members/invite.go @@ -66,7 +66,7 @@ func runInvite(client members.Client) func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutputterFn(response), + output.NewSingularOutput(response), ) if err != nil { return err diff --git a/cmd/projects/create.go b/cmd/projects/create.go index 90e7ea3a..681632a7 100644 --- a/cmd/projects/create.go +++ b/cmd/projects/create.go @@ -63,7 +63,7 @@ func runCreate(client projects.Client) func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutputterFn(response), + output.NewSingularOutput(response), ) if err != nil { return err diff --git a/cmd/projects/list.go b/cmd/projects/list.go index aa0dac20..625a5c02 100644 --- a/cmd/projects/list.go +++ b/cmd/projects/list.go @@ -38,7 +38,7 @@ func runList(client projects.Client) func(*cobra.Command, []string) error { output, err := output.CmdOutput( viper.GetString(cliflags.OutputFlag), - output.NewMultipleOutputterFn(response), + output.NewMultipleOutput(response), ) if err != nil { return err diff --git a/internal/output/config_outputter.go b/internal/output/config_outputter.go index 385d93bf..4f5d75ca 100644 --- a/internal/output/config_outputter.go +++ b/internal/output/config_outputter.go @@ -22,12 +22,6 @@ var configPlaintextOutputFn = func(r resource) string { return strings.Join(lst, "\n") } -func NewConfigOutputterFn(input []byte) configOutputterFn { - return configOutputterFn{ - input: input, - } -} - type configOutputterFn struct { input []byte } @@ -46,6 +40,12 @@ func (o configOutputterFn) New() (Outputter, error) { }, nil } +func NewConfigOutput(input []byte) configOutputterFn { + return configOutputterFn{ + input: input, + } +} + type ConfigOutputter struct { outputFn PlaintextOutputFn[resource] resource resource diff --git a/internal/output/config_outputter_test.go b/internal/output/config_outputter_test.go index eaf35657..c0b159fe 100644 --- a/internal/output/config_outputter_test.go +++ b/internal/output/config_outputter_test.go @@ -16,7 +16,7 @@ func TestConfigOutputter_JSON(t *testing.T) { }`) output, err := output.CmdOutput( "json", - output.NewConfigOutputterFn(input), + output.NewConfigOutput(input), ) require.NoError(t, err) @@ -31,7 +31,7 @@ func TestConfigOutputter_String(t *testing.T) { expected := "access-token: test-access-token\nbase-uri: test-base-uri" output, err := output.CmdOutput( "plaintext", - output.NewConfigOutputterFn(input), + output.NewConfigOutput(input), ) require.NoError(t, err) diff --git a/internal/output/multiple_outputter.go b/internal/output/multiple_outputter.go index efcb1656..f78882ba 100644 --- a/internal/output/multiple_outputter.go +++ b/internal/output/multiple_outputter.go @@ -9,13 +9,6 @@ var multiplePlaintextOutputFn = func(r resource) string { return fmt.Sprintf("* %s (%s)", r["name"], r["key"]) } -// TODO: rename this to be "cleaner"? -- NewMultipleOutput() -func NewMultipleOutputterFn(input []byte) multipleOutputterFn { - return multipleOutputterFn{ - input: input, - } -} - type multipleOutputterFn struct { input []byte } @@ -34,6 +27,12 @@ func (o multipleOutputterFn) New() (Outputter, error) { }, nil } +func NewMultipleOutput(input []byte) multipleOutputterFn { + return multipleOutputterFn{ + input: input, + } +} + type MultipleOutputter struct { outputFn PlaintextOutputFn[resource] resources resources diff --git a/internal/output/multiple_outputter_test.go b/internal/output/multiple_outputter_test.go index b22f449e..fdcc610e 100644 --- a/internal/output/multiple_outputter_test.go +++ b/internal/output/multiple_outputter_test.go @@ -26,7 +26,7 @@ func TestMultipleOutputter_JSON(t *testing.T) { }`) output, err := output.CmdOutput( "json", - output.NewMultipleOutputterFn(input), + output.NewMultipleOutput(input), ) require.NoError(t, err) @@ -51,7 +51,7 @@ func TestMultipleOutputter_String(t *testing.T) { expected := "* test-name1 (test-key1)\n* test-name2 (test-key2)" output, err := output.CmdOutput( "plaintext", - output.NewMultipleOutputterFn(input), + output.NewMultipleOutput(input), ) require.NoError(t, err) diff --git a/internal/output/singular_outputter.go b/internal/output/singular_outputter.go index d4999d53..d1cd7bd7 100644 --- a/internal/output/singular_outputter.go +++ b/internal/output/singular_outputter.go @@ -9,13 +9,6 @@ var singularPlaintextOutputFn = func(r resource) string { return fmt.Sprintf("%s (%s)", r["name"], r["key"]) } -// TODO: rename this to be "cleaner"? -- NewSingularOutput() -func NewSingularOutputterFn(input []byte) singularOutputterFn { - return singularOutputterFn{ - input: input, - } -} - type singularOutputterFn struct { input []byte } @@ -34,6 +27,12 @@ func (o singularOutputterFn) New() (Outputter, error) { }, nil } +func NewSingularOutput(input []byte) singularOutputterFn { + return singularOutputterFn{ + input: input, + } +} + type SingularOutputter struct { outputFn PlaintextOutputFn[resource] resource resource diff --git a/internal/output/singular_outputter_test.go b/internal/output/singular_outputter_test.go index 81be51a1..6283b00c 100644 --- a/internal/output/singular_outputter_test.go +++ b/internal/output/singular_outputter_test.go @@ -17,7 +17,7 @@ func TestSingularOutputter_JSON(t *testing.T) { }`) output, err := output.CmdOutput( "json", - output.NewSingularOutputterFn(input), + output.NewSingularOutput(input), ) require.NoError(t, err) @@ -33,7 +33,7 @@ func TestSingularOutputter_String(t *testing.T) { expected := "test-name (test-key)" output, err := output.CmdOutput( "plaintext", - output.NewSingularOutputterFn(input), + output.NewSingularOutput(input), ) require.NoError(t, err) From 4623108792be95751560212bc5bd7a2e936bf759 Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Tue, 23 Apr 2024 15:23:19 -0700 Subject: [PATCH 5/6] Remove extra type --- internal/output/config_outputter.go | 21 +++++---------------- internal/output/multiple_outputter.go | 2 ++ internal/output/output.go | 1 + internal/output/singular_outputter.go | 2 ++ 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/internal/output/config_outputter.go b/internal/output/config_outputter.go index 4f5d75ca..59414667 100644 --- a/internal/output/config_outputter.go +++ b/internal/output/config_outputter.go @@ -7,6 +7,8 @@ import ( "strings" ) +// configPlaintextOutputFn converts the resource to plain text specifically for data from the +// config file. var configPlaintextOutputFn = func(r resource) string { keys := make([]string, 0) for k := range r { @@ -26,14 +28,15 @@ type configOutputterFn struct { input []byte } +// New unmarshals a single config resource and wires up a particular plain text output function. func (o configOutputterFn) New() (Outputter, error) { var r resource err := json.Unmarshal(o.input, &r) if err != nil { - return ConfigOutputter{}, err + return SingularOutputter{}, err } - return ConfigOutputter{ + return SingularOutputter{ outputFn: configPlaintextOutputFn, resource: r, resourceJSON: o.input, @@ -45,17 +48,3 @@ func NewConfigOutput(input []byte) configOutputterFn { input: input, } } - -type ConfigOutputter struct { - outputFn PlaintextOutputFn[resource] - resource resource - resourceJSON []byte -} - -func (o ConfigOutputter) JSON() string { - return string(o.resourceJSON) -} - -func (o ConfigOutputter) String() string { - return formatColl([]resource{o.resource}, o.outputFn) -} diff --git a/internal/output/multiple_outputter.go b/internal/output/multiple_outputter.go index f78882ba..0041d834 100644 --- a/internal/output/multiple_outputter.go +++ b/internal/output/multiple_outputter.go @@ -5,6 +5,7 @@ import ( "fmt" ) +// multiplePlaintextOutputFn converts the resource to plain text based on its name and key in a list. var multiplePlaintextOutputFn = func(r resource) string { return fmt.Sprintf("* %s (%s)", r["name"], r["key"]) } @@ -13,6 +14,7 @@ type multipleOutputterFn struct { input []byte } +// New unmarshals multiple resources and wires up a particular plain text output function. func (o multipleOutputterFn) New() (Outputter, error) { var r resources err := json.Unmarshal(o.input, &r) diff --git a/internal/output/output.go b/internal/output/output.go index 0f6892be..81482f5c 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -27,6 +27,7 @@ type PlaintextOutputFn[T any] func(t T) string // resource is the subset of data we need to display a command's plain text response for a single // resource. +// We're trading off type safety for easy of use instead of defining a type for each expected resource. type resource map[string]interface{} // resources is the subset of data we need to display a command's plain text response for a list diff --git a/internal/output/singular_outputter.go b/internal/output/singular_outputter.go index d1cd7bd7..3f8f3c78 100644 --- a/internal/output/singular_outputter.go +++ b/internal/output/singular_outputter.go @@ -5,6 +5,7 @@ import ( "fmt" ) +// singularPlaintextOutputFn converts the resource to plain text based on its name and key. var singularPlaintextOutputFn = func(r resource) string { return fmt.Sprintf("%s (%s)", r["name"], r["key"]) } @@ -13,6 +14,7 @@ type singularOutputterFn struct { input []byte } +// New unmarshals a single resource and wires up a particular plain text output function. func (o singularOutputterFn) New() (Outputter, error) { var r resource err := json.Unmarshal(o.input, &r) From 7ce4b06c3a2733ae98fc5522afe8a82d8359b012 Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Wed, 24 Apr 2024 14:30:58 -0700 Subject: [PATCH 6/6] feat: output flag errors (#204) * Add plain text error handling * Output flag errors as plain text or JSON * Remove unused code * Update all commands to return plaintext or JSON * Refactor outputter * Backfill tests * Reorganize * Refactor * Remove comments * Renamed functions --- cmd/config/config.go | 5 +- cmd/environments/get.go | 19 ++++- cmd/environments/get_test.go | 3 +- cmd/flags/create.go | 23 +++-- cmd/flags/create_test.go | 2 +- cmd/flags/get.go | 24 ++++-- cmd/flags/get_test.go | 3 +- cmd/flags/update.go | 24 ++++-- cmd/flags/update_test.go | 4 +- cmd/members/create.go | 21 +++-- cmd/members/create_test.go | 2 +- cmd/members/invite.go | 19 ++++- cmd/members/invite_test.go | 3 +- cmd/projects/create.go | 19 ++++- cmd/projects/create_test.go | 14 ++-- cmd/projects/list.go | 19 ++++- cmd/projects/list_test.go | 4 +- internal/output/config_outputter.go | 50 ----------- internal/output/config_outputter_test.go | 39 --------- internal/output/multiple_outputter.go | 50 ----------- internal/output/multiple_outputter_test.go | 59 ------------- internal/output/output.go | 57 +++++++++---- internal/output/output_test.go | 98 ++++++++++++++++++++++ internal/output/outputters.go | 42 ++++++++++ internal/output/plaintext_fns.go | 56 +++++++++++++ internal/output/singular_outputter.go | 50 ----------- internal/output/singular_outputter_test.go | 41 --------- 27 files changed, 375 insertions(+), 375 deletions(-) delete mode 100644 internal/output/config_outputter.go delete mode 100644 internal/output/config_outputter_test.go delete mode 100644 internal/output/multiple_outputter.go delete mode 100644 internal/output/multiple_outputter_test.go create mode 100644 internal/output/output_test.go create mode 100644 internal/output/outputters.go create mode 100644 internal/output/plaintext_fns.go delete mode 100644 internal/output/singular_outputter.go delete mode 100644 internal/output/singular_outputter_test.go diff --git a/cmd/config/config.go b/cmd/config/config.go index 2f62d73d..831401d2 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -64,9 +64,10 @@ func run() func(*cobra.Command, []string) error { return err } - output, err := output.CmdOutput( + output, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - output.NewConfigOutput(configJSON), + configJSON, + output.ConfigPlaintextOutputFn, ) if err != nil { return err diff --git a/cmd/environments/get.go b/cmd/environments/get.go index 08245463..e169adc4 100644 --- a/cmd/environments/get.go +++ b/cmd/environments/get.go @@ -10,6 +10,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" "ldcli/internal/environments" + "ldcli/internal/errors" "ldcli/internal/output" ) @@ -63,15 +64,25 @@ func runGet( viper.GetString(cliflags.ProjectFlag), ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutput(response), + response, + output.SingularPlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/environments/get_test.go b/cmd/environments/get_test.go index 4c0a2dd3..7c7c4ce2 100644 --- a/cmd/environments/get_test.go +++ b/cmd/environments/get_test.go @@ -72,7 +72,7 @@ func TestGet(t *testing.T) { client := environments.MockClient{} client. On("Get", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ EnvironmentsClient: &client, } @@ -80,7 +80,6 @@ func TestGet(t *testing.T) { "environments", "get", "--access-token", "testAccessToken", "--base-uri", "http://test.com", - "--output", "json", "--environment", "test-env", "--project", "test-proj", } diff --git a/cmd/flags/create.go b/cmd/flags/create.go index 9f8c60c6..e9180e1d 100644 --- a/cmd/flags/create.go +++ b/cmd/flags/create.go @@ -10,6 +10,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/errors" "ldcli/internal/flags" "ldcli/internal/output" ) @@ -53,10 +54,6 @@ type inputData struct { func runCreate(client flags.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - // rebind flags used in other subcommands - _ = viper.BindPFlag(cliflags.DataFlag, cmd.Flags().Lookup(cliflags.DataFlag)) - _ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag)) - var data inputData err := json.Unmarshal([]byte(viper.GetString(cliflags.DataFlag)), &data) if err != nil { @@ -72,15 +69,25 @@ func runCreate(client flags.Client) func(*cobra.Command, []string) error { viper.GetString(cliflags.ProjectFlag), ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutput(response), + response, + output.SingularPlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/flags/create_test.go b/cmd/flags/create_test.go index 9518b573..799ee4aa 100644 --- a/cmd/flags/create_test.go +++ b/cmd/flags/create_test.go @@ -72,7 +72,7 @@ func TestCreate(t *testing.T) { client := flags.MockClient{} client. On("Create", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ FlagsClient: &client, } diff --git a/cmd/flags/get.go b/cmd/flags/get.go index ab1aef78..77912d51 100644 --- a/cmd/flags/get.go +++ b/cmd/flags/get.go @@ -9,6 +9,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/errors" "ldcli/internal/flags" "ldcli/internal/output" ) @@ -57,11 +58,6 @@ func NewGetCmd(client flags.Client) (*cobra.Command, error) { func runGet(client flags.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - // rebind flags used in other subcommands - _ = viper.BindPFlag(cliflags.FlagFlag, cmd.Flags().Lookup(cliflags.FlagFlag)) - _ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag)) - _ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag)) - response, err := client.Get( context.Background(), viper.GetString(cliflags.AccessTokenFlag), @@ -71,15 +67,25 @@ func runGet(client flags.Client) func(*cobra.Command, []string) error { viper.GetString(cliflags.EnvironmentFlag), ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutput(response), + response, + output.SingularPlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/flags/get_test.go b/cmd/flags/get_test.go index 11527792..ee65f641 100644 --- a/cmd/flags/get_test.go +++ b/cmd/flags/get_test.go @@ -73,7 +73,7 @@ func TestGet(t *testing.T) { client := flags.MockClient{} client. On("Get", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ FlagsClient: &client, } @@ -81,7 +81,6 @@ func TestGet(t *testing.T) { "flags", "get", "--access-token", "testAccessToken", "--base-uri", "http://test.com", - "--output", "json", "--flag", "test-key", "--project", "test-proj-key", "--environment", "test-env-key", diff --git a/cmd/flags/update.go b/cmd/flags/update.go index c61180c4..f2275cfe 100644 --- a/cmd/flags/update.go +++ b/cmd/flags/update.go @@ -10,6 +10,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/errors" "ldcli/internal/flags" "ldcli/internal/output" ) @@ -116,11 +117,6 @@ func setToggleCommandFlags(cmd *cobra.Command) (*cobra.Command, error) { func runUpdate(client flags.Client) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { - // rebind flags used in other subcommands - _ = viper.BindPFlag(cliflags.DataFlag, cmd.Flags().Lookup(cliflags.DataFlag)) - _ = viper.BindPFlag(cliflags.ProjectFlag, cmd.Flags().Lookup(cliflags.ProjectFlag)) - _ = viper.BindPFlag(cliflags.FlagFlag, cmd.Flags().Lookup(cliflags.FlagFlag)) - var patch []flags.UpdateInput if cmd.CalledAs() == "toggle-on" || cmd.CalledAs() == "toggle-off" { _ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag)) @@ -142,15 +138,25 @@ func runUpdate(client flags.Client) func(*cobra.Command, []string) error { patch, ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutput(response), + response, + output.SingularPlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/flags/update_test.go b/cmd/flags/update_test.go index 14937239..70a1fde3 100644 --- a/cmd/flags/update_test.go +++ b/cmd/flags/update_test.go @@ -80,7 +80,7 @@ func TestUpdate(t *testing.T) { client := flags.MockClient{} client. On("Update", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ FlagsClient: &client, } @@ -207,7 +207,7 @@ func TestToggle(t *testing.T) { client := flags.MockClient{} client. On("Update", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ FlagsClient: &client, } diff --git a/cmd/members/create.go b/cmd/members/create.go index a1860706..154c06d8 100644 --- a/cmd/members/create.go +++ b/cmd/members/create.go @@ -10,6 +10,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/errors" "ldcli/internal/members" "ldcli/internal/output" ) @@ -42,7 +43,7 @@ func runCreate(client members.Client) func(*cobra.Command, []string) error { // TODO: why does viper.GetString(cliflags.DataFlag) not work? err := json.Unmarshal([]byte(cmd.Flags().Lookup(cliflags.DataFlag).Value.String()), &data) if err != nil { - return err + return errors.NewError(err.Error()) } response, err := client.Create( @@ -52,15 +53,25 @@ func runCreate(client members.Client) func(*cobra.Command, []string) error { data, ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutput(response), + response, + output.SingularPlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/members/create_test.go b/cmd/members/create_test.go index 0605eb46..feb2efcb 100644 --- a/cmd/members/create_test.go +++ b/cmd/members/create_test.go @@ -76,7 +76,7 @@ func TestCreate(t *testing.T) { client := members.MockClient{} client. On("Create", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ MembersClient: &client, } diff --git a/cmd/members/invite.go b/cmd/members/invite.go index 70c56e6e..450d6412 100644 --- a/cmd/members/invite.go +++ b/cmd/members/invite.go @@ -9,6 +9,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/errors" "ldcli/internal/members" "ldcli/internal/output" ) @@ -61,15 +62,25 @@ func runInvite(client members.Client) func(*cobra.Command, []string) error { memberInputs, ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputMultiple( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutput(response), + response, + output.MultipleEmailPlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/members/invite_test.go b/cmd/members/invite_test.go index 85dac256..c98197d4 100644 --- a/cmd/members/invite_test.go +++ b/cmd/members/invite_test.go @@ -77,7 +77,7 @@ func TestInvite(t *testing.T) { client := members.MockClient{} client. On("Create", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ MembersClient: &client, } @@ -86,7 +86,6 @@ func TestInvite(t *testing.T) { "invite", "--access-token", "testAccessToken", "--base-uri", "http://test.com", - "--output", "json", "-e", `testemail1@test.com,testemail2@test.com`, } diff --git a/cmd/projects/create.go b/cmd/projects/create.go index 681632a7..408984fa 100644 --- a/cmd/projects/create.go +++ b/cmd/projects/create.go @@ -10,6 +10,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/errors" "ldcli/internal/output" "ldcli/internal/projects" ) @@ -58,15 +59,25 @@ func runCreate(client projects.Client) func(*cobra.Command, []string) error { data.Key, ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputSingular( viper.GetString(cliflags.OutputFlag), - output.NewSingularOutput(response), + response, + output.SingularPlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/projects/create_test.go b/cmd/projects/create_test.go index 0be76c9b..06cdeb10 100644 --- a/cmd/projects/create_test.go +++ b/cmd/projects/create_test.go @@ -77,19 +77,15 @@ func TestCreate(t *testing.T) { client := projects.MockClient{} client. On("Create", mockArgs...). - Return([]byte(`{}`), errors.NewError("An error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ ProjectsClient: &client, } args := []string{ - "projects", - "create", - "--access-token", - "testAccessToken", - "--base-uri", - "http://test.com", - "-d", - `{"key": "test-key", "name": "test-name"}`, + "projects", "create", + "--access-token", "testAccessToken", + "--base-uri", "http://test.com", + "-d", `{"key": "test-key", "name": "test-name"}`, } _, err := cmd.CallCmd(t, clients, &analytics.NoopClient{}, args) diff --git a/cmd/projects/list.go b/cmd/projects/list.go index 625a5c02..4a038995 100644 --- a/cmd/projects/list.go +++ b/cmd/projects/list.go @@ -9,6 +9,7 @@ import ( "ldcli/cmd/cliflags" "ldcli/cmd/validators" + "ldcli/internal/errors" "ldcli/internal/output" "ldcli/internal/projects" ) @@ -33,15 +34,25 @@ func runList(client projects.Client) func(*cobra.Command, []string) error { viper.GetString(cliflags.BaseURIFlag), ) if err != nil { - return err + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + []byte(err.Error()), + output.ErrorPlaintextOutputFn, + ) + if err != nil { + return errors.NewError(err.Error()) + } + + return errors.NewError(output) } - output, err := output.CmdOutput( + output, err := output.CmdOutputMultiple( viper.GetString(cliflags.OutputFlag), - output.NewMultipleOutput(response), + response, + output.MultiplePlaintextOutputFn, ) if err != nil { - return err + return errors.NewError(err.Error()) } fmt.Fprintf(cmd.OutOrStdout(), output+"\n") diff --git a/cmd/projects/list_test.go b/cmd/projects/list_test.go index 5b072adf..d0ec5381 100644 --- a/cmd/projects/list_test.go +++ b/cmd/projects/list_test.go @@ -74,7 +74,7 @@ func TestList(t *testing.T) { client := projects.MockClient{} client. On("List", mockArgs...). - Return([]byte(`{}`), errors.NewError("an error")) + Return([]byte(`{}`), errors.NewError(`{"message": "An error"}`)) clients := cmd.APIClients{ ProjectsClient: &client, } @@ -86,7 +86,7 @@ func TestList(t *testing.T) { _, err := cmd.CallCmd(t, clients, &analytics.NoopClient{}, args) - require.EqualError(t, err, "an error") + require.EqualError(t, err, "An error") }) t.Run("with missing required flags is an error", func(t *testing.T) { diff --git a/internal/output/config_outputter.go b/internal/output/config_outputter.go deleted file mode 100644 index 59414667..00000000 --- a/internal/output/config_outputter.go +++ /dev/null @@ -1,50 +0,0 @@ -package output - -import ( - "encoding/json" - "fmt" - "sort" - "strings" -) - -// configPlaintextOutputFn converts the resource to plain text specifically for data from the -// config file. -var configPlaintextOutputFn = func(r resource) string { - keys := make([]string, 0) - for k := range r { - keys = append(keys, k) - } - sort.Strings(keys) - - lst := make([]string, 0) - for _, k := range keys { - lst = append(lst, fmt.Sprintf("%s: %s", k, r[k])) - } - - return strings.Join(lst, "\n") -} - -type configOutputterFn struct { - input []byte -} - -// New unmarshals a single config resource and wires up a particular plain text output function. -func (o configOutputterFn) New() (Outputter, error) { - var r resource - err := json.Unmarshal(o.input, &r) - if err != nil { - return SingularOutputter{}, err - } - - return SingularOutputter{ - outputFn: configPlaintextOutputFn, - resource: r, - resourceJSON: o.input, - }, nil -} - -func NewConfigOutput(input []byte) configOutputterFn { - return configOutputterFn{ - input: input, - } -} diff --git a/internal/output/config_outputter_test.go b/internal/output/config_outputter_test.go deleted file mode 100644 index c0b159fe..00000000 --- a/internal/output/config_outputter_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package output_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "ldcli/internal/output" -) - -func TestConfigOutputter_JSON(t *testing.T) { - input := []byte(`{ - "access-token": "test-access-token", - "base-uri": "test-base-uri" - }`) - output, err := output.CmdOutput( - "json", - output.NewConfigOutput(input), - ) - - require.NoError(t, err) - assert.JSONEq(t, output, string(input)) -} - -func TestConfigOutputter_String(t *testing.T) { - input := []byte(`{ - "access-token": "test-access-token", - "base-uri": "test-base-uri" - }`) - expected := "access-token: test-access-token\nbase-uri: test-base-uri" - output, err := output.CmdOutput( - "plaintext", - output.NewConfigOutput(input), - ) - - require.NoError(t, err) - assert.Equal(t, expected, output) -} diff --git a/internal/output/multiple_outputter.go b/internal/output/multiple_outputter.go deleted file mode 100644 index 0041d834..00000000 --- a/internal/output/multiple_outputter.go +++ /dev/null @@ -1,50 +0,0 @@ -package output - -import ( - "encoding/json" - "fmt" -) - -// multiplePlaintextOutputFn converts the resource to plain text based on its name and key in a list. -var multiplePlaintextOutputFn = func(r resource) string { - return fmt.Sprintf("* %s (%s)", r["name"], r["key"]) -} - -type multipleOutputterFn struct { - input []byte -} - -// New unmarshals multiple resources and wires up a particular plain text output function. -func (o multipleOutputterFn) New() (Outputter, error) { - var r resources - err := json.Unmarshal(o.input, &r) - if err != nil { - return MultipleOutputter{}, err - } - - return MultipleOutputter{ - outputFn: multiplePlaintextOutputFn, - resources: r, - resourceJSON: o.input, - }, nil -} - -func NewMultipleOutput(input []byte) multipleOutputterFn { - return multipleOutputterFn{ - input: input, - } -} - -type MultipleOutputter struct { - outputFn PlaintextOutputFn[resource] - resources resources - resourceJSON []byte -} - -func (o MultipleOutputter) JSON() string { - return string(o.resourceJSON) -} - -func (o MultipleOutputter) String() string { - return formatColl(o.resources.Items, o.outputFn) -} diff --git a/internal/output/multiple_outputter_test.go b/internal/output/multiple_outputter_test.go deleted file mode 100644 index fdcc610e..00000000 --- a/internal/output/multiple_outputter_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package output_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "ldcli/internal/output" -) - -func TestMultipleOutputter_JSON(t *testing.T) { - input := []byte(`{ - "items": [ - { - "key": "test-key1", - "name": "test-name1", - "other": "another-value2" - }, - { - "key": "test-key2", - "name": "test-name2", - "other": "another-value2" - } - ] - }`) - output, err := output.CmdOutput( - "json", - output.NewMultipleOutput(input), - ) - - require.NoError(t, err) - assert.JSONEq(t, output, string(input)) -} - -func TestMultipleOutputter_String(t *testing.T) { - input := []byte(`{ - "items": [ - { - "key": "test-key1", - "name": "test-name1", - "other": "another-value2" - }, - { - "key": "test-key2", - "name": "test-name2", - "other": "another-value2" - } - ] - }`) - expected := "* test-name1 (test-key1)\n* test-name2 (test-key2)" - output, err := output.CmdOutput( - "plaintext", - output.NewMultipleOutput(input), - ) - - require.NoError(t, err) - assert.Equal(t, expected, output) -} diff --git a/internal/output/output.go b/internal/output/output.go index 81482f5c..4d9e188e 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -1,7 +1,7 @@ package output import ( - "strings" + "encoding/json" "ldcli/internal/errors" ) @@ -23,7 +23,7 @@ type OutputterFn interface { } // PlaintextOutputFn represents the various ways to output a resource or resources. -type PlaintextOutputFn[T any] func(t T) string +type PlaintextOutputFn func(resource) string // resource is the subset of data we need to display a command's plain text response for a single // resource. @@ -36,13 +36,49 @@ type resources struct { Items []resource `json:"items"` } -// CmdOutput returns a command's response as a string formatted based on the user's requested type. -func CmdOutput(outputKind string, outputter OutputterFn) (string, error) { - o, err := outputter.New() +// resourcesBare is for responses that return a list of resources at the top level of the response, +// not as a value of an "items" property. +type resourcesBare []resource + +// CmdOutputSingular builds a command response based on the flag the user provided and the shape of +// the input. The expected shape is a single JSON object. +func CmdOutputSingular(outputKind string, input []byte, fn PlaintextOutputFn) (string, error) { + var r resource + err := json.Unmarshal(input, &r) if err != nil { return "", err } + return outputFromKind(outputKind, SingularOutputter{ + outputFn: fn, + resource: r, + resourceJSON: input, + }) +} + +// CmdOutputMultiple builds a command response based on the flag the user provided and the shape of +// the input. The expected shape is a list of JSON objects. +func CmdOutputMultiple(outputKind string, input []byte, fn PlaintextOutputFn) (string, error) { + var r resources + err := json.Unmarshal(input, &r) + if err != nil { + // sometimes a response doesn't include each item in an "items" property + var rr resourcesBare + err := json.Unmarshal(input, &rr) + if err != nil { + return "", err + } + r.Items = rr + } + + return outputFromKind(outputKind, MultipleOutputter{ + outputFn: fn, + resources: r, + resourceJSON: input, + }) +} + +func outputFromKind(outputKind string, o Outputter) (string, error) { switch outputKind { case "json": return o.JSON(), nil @@ -52,14 +88,3 @@ func CmdOutput(outputKind string, outputter OutputterFn) (string, error) { return "", ErrInvalidOutputKind } - -// FormatColl applies a formatting function to every element in the collection and returns it as a -// string. -func formatColl[T any](coll []T, formatFn func(T) string) string { - lst := make([]string, 0, len(coll)) - for _, c := range coll { - lst = append(lst, formatFn(c)) - } - - return strings.Join(lst, "\n") -} diff --git a/internal/output/output_test.go b/internal/output/output_test.go new file mode 100644 index 00000000..5a16069f --- /dev/null +++ b/internal/output/output_test.go @@ -0,0 +1,98 @@ +package output_test + +import ( + "ldcli/internal/output" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCmdOutputResource(t *testing.T) { + tests := map[string]struct { + expected string + fn output.PlaintextOutputFn + input string + }{ + "with config file data": { + expected: "key: value\nkey2: value2", + fn: output.ConfigPlaintextOutputFn, + input: `{"key": "value", "key2": "value2"}`, + }, + "with an error with a code and message": { + expected: "test-message (code: test-code)", + fn: output.ErrorPlaintextOutputFn, + input: `{"code": "test-code", "message": "test-message"}`, + }, + "with an error with only a code": { + expected: "an error occurred (code: test-code)", + fn: output.ErrorPlaintextOutputFn, + input: `{"code": "test-code", "message": ""}`, + }, + "with an error with only a message": { + expected: "test-message", + fn: output.ErrorPlaintextOutputFn, + input: `{"message": "test-message"}`, + }, + "with an error without a code or message": { + expected: "unknown error occurred", + fn: output.ErrorPlaintextOutputFn, + input: `{"message": ""}`, + }, + "with an error without a response body": { + expected: "unknown error occurred", + fn: output.ErrorPlaintextOutputFn, + input: `{}`, + }, + "with a singular resource": { + expected: "test-name (test-key)", + fn: output.SingularPlaintextOutputFn, + input: `{"key": "test-key", "name": "test-name"}`, + }, + } + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + output, err := output.CmdOutputSingular( + "plaintext", + []byte(tt.input), + tt.fn, + ) + + require.NoError(t, err) + assert.Equal(t, tt.expected, output) + }) + } +} + +func TestCmdOutputResources(t *testing.T) { + tests := map[string]struct { + expected string + fn output.PlaintextOutputFn + input string + }{ + "with multiple emails not as items property": { + expected: "* test-email1 (test-id1)\n* test-email2 (test-id2)", + fn: output.MultipleEmailPlaintextOutputFn, + input: `[{"_id": "test-id1", "email": "test-email1"}, {"_id": "test-id2", "email": "test-email2"}]`, + }, + "with multiple items": { + expected: "* test-name1 (test-key1)\n* test-name2 (test-key2)", + fn: output.MultiplePlaintextOutputFn, + input: `{"items": [{"key": "test-key1", "name": "test-name1"}, {"key": "test-key2", "name": "test-name2"}]}`, + }, + } + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + output, err := output.CmdOutputMultiple( + "plaintext", + []byte(tt.input), + tt.fn, + ) + + require.NoError(t, err) + assert.Equal(t, tt.expected, output) + }) + } +} diff --git a/internal/output/outputters.go b/internal/output/outputters.go new file mode 100644 index 00000000..a45e0467 --- /dev/null +++ b/internal/output/outputters.go @@ -0,0 +1,42 @@ +package output + +import "strings" + +type MultipleOutputter struct { + outputFn PlaintextOutputFn + resources resources + resourceJSON []byte +} + +func (o MultipleOutputter) JSON() string { + return string(o.resourceJSON) +} + +func (o MultipleOutputter) String() string { + return formatColl(o.resources.Items, o.outputFn) +} + +type SingularOutputter struct { + outputFn PlaintextOutputFn + resource resource + resourceJSON []byte +} + +func (o SingularOutputter) JSON() string { + return string(o.resourceJSON) +} + +func (o SingularOutputter) String() string { + return formatColl([]resource{o.resource}, o.outputFn) +} + +// formatColl applies a formatting function to every element in the collection and returns it as a +// string. +func formatColl[T any](coll []T, formatFn func(T) string) string { + lst := make([]string, 0, len(coll)) + for _, c := range coll { + lst = append(lst, formatFn(c)) + } + + return strings.Join(lst, "\n") +} diff --git a/internal/output/plaintext_fns.go b/internal/output/plaintext_fns.go new file mode 100644 index 00000000..af1c8232 --- /dev/null +++ b/internal/output/plaintext_fns.go @@ -0,0 +1,56 @@ +package output + +import ( + "fmt" + "sort" + "strings" +) + +// ConfigPlaintextOutputFn converts the resource to plain text specifically for data from the +// config file. +var ConfigPlaintextOutputFn = func(r resource) string { + keys := make([]string, 0) + for k := range r { + keys = append(keys, k) + } + sort.Strings(keys) + + lst := make([]string, 0) + for _, k := range keys { + lst = append(lst, fmt.Sprintf("%s: %s", k, r[k])) + } + + return strings.Join(lst, "\n") +} + +// ErrorPlaintextOutputFn converts the resource to plain text specifically for data from the +// error file. +// An error response could have a code and message or just a message. It's also possible that +// there isn't either property. +var ErrorPlaintextOutputFn = func(r resource) string { + switch { + case r["code"] == nil && (r["message"] == "" || r["message"] == nil): + return "unknown error occurred" + case r["code"] == nil: + return r["message"].(string) + case r["message"] == "": + return fmt.Sprintf("an error occurred (code: %s)", r["code"]) + default: + return fmt.Sprintf("%s (code: %s)", r["message"], r["code"]) + } +} + +// MultipleEmailPlaintextOutputFn converts the resource to plain text specifically for member data. +var MultipleEmailPlaintextOutputFn = func(r resource) string { + return fmt.Sprintf("* %s (%s)", r["email"], r["_id"]) +} + +// MultiplePlaintextOutputFn converts the resource to plain text based on its name and key in a list. +var MultiplePlaintextOutputFn = func(r resource) string { + return fmt.Sprintf("* %s (%s)", r["name"], r["key"]) +} + +// SingularPlaintextOutputFn converts the resource to plain text based on its name and key. +var SingularPlaintextOutputFn = func(r resource) string { + return fmt.Sprintf("%s (%s)", r["name"], r["key"]) +} diff --git a/internal/output/singular_outputter.go b/internal/output/singular_outputter.go deleted file mode 100644 index 3f8f3c78..00000000 --- a/internal/output/singular_outputter.go +++ /dev/null @@ -1,50 +0,0 @@ -package output - -import ( - "encoding/json" - "fmt" -) - -// singularPlaintextOutputFn converts the resource to plain text based on its name and key. -var singularPlaintextOutputFn = func(r resource) string { - return fmt.Sprintf("%s (%s)", r["name"], r["key"]) -} - -type singularOutputterFn struct { - input []byte -} - -// New unmarshals a single resource and wires up a particular plain text output function. -func (o singularOutputterFn) New() (Outputter, error) { - var r resource - err := json.Unmarshal(o.input, &r) - if err != nil { - return SingularOutputter{}, err - } - - return SingularOutputter{ - outputFn: singularPlaintextOutputFn, - resource: r, - resourceJSON: o.input, - }, nil -} - -func NewSingularOutput(input []byte) singularOutputterFn { - return singularOutputterFn{ - input: input, - } -} - -type SingularOutputter struct { - outputFn PlaintextOutputFn[resource] - resource resource - resourceJSON []byte -} - -func (o SingularOutputter) JSON() string { - return string(o.resourceJSON) -} - -func (o SingularOutputter) String() string { - return formatColl([]resource{o.resource}, o.outputFn) -} diff --git a/internal/output/singular_outputter_test.go b/internal/output/singular_outputter_test.go deleted file mode 100644 index 6283b00c..00000000 --- a/internal/output/singular_outputter_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package output_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "ldcli/internal/output" -) - -func TestSingularOutputter_JSON(t *testing.T) { - input := []byte(`{ - "key": "test-key", - "name": "test-name", - "other": "another-value" - }`) - output, err := output.CmdOutput( - "json", - output.NewSingularOutput(input), - ) - - require.NoError(t, err) - assert.JSONEq(t, output, string(input)) -} - -func TestSingularOutputter_String(t *testing.T) { - input := []byte(`{ - "key": "test-key", - "name": "test-name", - "other": "another-value" - }`) - expected := "test-name (test-key)" - output, err := output.CmdOutput( - "plaintext", - output.NewSingularOutput(input), - ) - - require.NoError(t, err) - assert.Equal(t, expected, output) -}