From 5de73ec01239ca90f73715794c6ff5277d9642eb Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Wed, 24 Apr 2024 14:31:46 -0700 Subject: [PATCH] feat: output flag config cmd (#202) * Show plaintext response for config --list * Use map for output type * Refactor types * Organization * Remove extra type * 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 | 12 ++- 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/multiple_outputter.go | 49 ----------- internal/output/multiple_outputter_test.go | 59 ------------- internal/output/output.go | 61 +++++++++----- internal/output/output_test.go | 98 ++++++++++++++++++++++ internal/output/outputters.go | 42 ++++++++++ internal/output/plaintext_fns.go | 56 +++++++++++++ internal/output/singular_outputter.go | 49 ----------- internal/output/singular_outputter_test.go | 41 --------- 25 files changed, 382 insertions(+), 288 deletions(-) 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 f9779e62..831401d2 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,16 @@ func run() func(*cobra.Command, []string) error { return err } - if string(configJSON) == "{}" { - return nil + output, err := output.CmdOutputSingular( + viper.GetString(cliflags.OutputFlag), + configJSON, + output.ConfigPlaintextOutputFn, + ) + 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/cmd/environments/get.go b/cmd/environments/get.go index bc3366ef..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.NewSingularOutputterFn(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 18e54874..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.NewSingularOutputterFn(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 49912574..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.NewSingularOutputterFn(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 ed3ed815..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.NewSingularOutputterFn(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 19f92823..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.NewSingularOutputterFn(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 6190d4d7..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.NewSingularOutputterFn(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 90e7ea3a..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.NewSingularOutputterFn(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 aa0dac20..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.NewMultipleOutputterFn(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/multiple_outputter.go b/internal/output/multiple_outputter.go deleted file mode 100644 index 97e24505..00000000 --- a/internal/output/multiple_outputter.go +++ /dev/null @@ -1,49 +0,0 @@ -package output - -import ( - "encoding/json" - "fmt" -) - -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 -} - -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 -} - -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) -} diff --git a/internal/output/multiple_outputter_test.go b/internal/output/multiple_outputter_test.go deleted file mode 100644 index b22f449e..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.NewMultipleOutputterFn(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.NewMultipleOutputterFn(input), - ) - - require.NoError(t, err) - assert.Equal(t, expected, output) -} diff --git a/internal/output/output.go b/internal/output/output.go index eaea59e4..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" ) @@ -27,10 +27,8 @@ 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. -type resource struct { - Key string `json:"key"` - Name string `json:"name"` -} +// 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 // of resources. @@ -38,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 @@ -54,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 4c953c55..00000000 --- a/internal/output/singular_outputter.go +++ /dev/null @@ -1,49 +0,0 @@ -package output - -import ( - "encoding/json" - "fmt" -) - -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 -} - -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 -} - -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) -} diff --git a/internal/output/singular_outputter_test.go b/internal/output/singular_outputter_test.go deleted file mode 100644 index 81be51a1..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.NewSingularOutputterFn(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.NewSingularOutputterFn(input), - ) - - require.NoError(t, err) - assert.Equal(t, expected, output) -}