diff --git a/cmd/environments/get.go b/cmd/environments/get.go index e169adc4..0005788d 100644 --- a/cmd/environments/get.go +++ b/cmd/environments/get.go @@ -76,11 +76,7 @@ func runGet( return errors.NewError(output) } - output, err := output.CmdOutputSingular( - viper.GetString(cliflags.OutputFlag), - response, - output.SingularPlaintextOutputFn, - ) + output, err := output.CmdOutput("get", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/cmd/flags/create.go b/cmd/flags/create.go index aaabd9e0..60618678 100644 --- a/cmd/flags/create.go +++ b/cmd/flags/create.go @@ -81,11 +81,7 @@ func runCreate(client flags.Client) func(*cobra.Command, []string) error { return errors.NewError(output) } - output, err := output.CmdOutputCreate( - viper.GetString(cliflags.OutputFlag), - response, - output.SingularPlaintextOutputFn, - ) + output, err := output.CmdOutput("create", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/cmd/flags/get.go b/cmd/flags/get.go index 77912d51..cc1a906c 100644 --- a/cmd/flags/get.go +++ b/cmd/flags/get.go @@ -79,11 +79,7 @@ func runGet(client flags.Client) func(*cobra.Command, []string) error { return errors.NewError(output) } - output, err := output.CmdOutputSingular( - viper.GetString(cliflags.OutputFlag), - response, - output.SingularPlaintextOutputFn, - ) + output, err := output.CmdOutput("get", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/cmd/flags/update.go b/cmd/flags/update.go index 5c8bae11..b3f62bcf 100644 --- a/cmd/flags/update.go +++ b/cmd/flags/update.go @@ -150,11 +150,7 @@ func runUpdate(client flags.Client) func(*cobra.Command, []string) error { return errors.NewError(output) } - output, err := output.CmdOutputUpdate( - viper.GetString(cliflags.OutputFlag), - response, - output.SingularPlaintextOutputFn, - ) + output, err := output.CmdOutput("update", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/cmd/members/create.go b/cmd/members/create.go index 154c06d8..3c2be7f6 100644 --- a/cmd/members/create.go +++ b/cmd/members/create.go @@ -65,11 +65,7 @@ func runCreate(client members.Client) func(*cobra.Command, []string) error { return errors.NewError(output) } - output, err := output.CmdOutputSingular( - viper.GetString(cliflags.OutputFlag), - response, - output.SingularPlaintextOutputFn, - ) + output, err := output.CmdOutput("create", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/cmd/members/invite.go b/cmd/members/invite.go index 450d6412..1dd3b385 100644 --- a/cmd/members/invite.go +++ b/cmd/members/invite.go @@ -74,11 +74,7 @@ func runInvite(client members.Client) func(*cobra.Command, []string) error { return errors.NewError(output) } - output, err := output.CmdOutputMultiple( - viper.GetString(cliflags.OutputFlag), - response, - output.MultipleEmailPlaintextOutputFn, - ) + output, err := output.CmdOutput("create", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/cmd/projects/create.go b/cmd/projects/create.go index 408984fa..2f4d83f6 100644 --- a/cmd/projects/create.go +++ b/cmd/projects/create.go @@ -71,11 +71,7 @@ func runCreate(client projects.Client) func(*cobra.Command, []string) error { return errors.NewError(output) } - output, err := output.CmdOutputSingular( - viper.GetString(cliflags.OutputFlag), - response, - output.SingularPlaintextOutputFn, - ) + output, err := output.CmdOutput("create", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/cmd/projects/list.go b/cmd/projects/list.go index 4a038995..666f32e3 100644 --- a/cmd/projects/list.go +++ b/cmd/projects/list.go @@ -46,11 +46,7 @@ func runList(client projects.Client) func(*cobra.Command, []string) error { return errors.NewError(output) } - output, err := output.CmdOutputMultiple( - viper.GetString(cliflags.OutputFlag), - response, - output.MultiplePlaintextOutputFn, - ) + output, err := output.CmdOutput("list", viper.GetString(cliflags.OutputFlag), response) if err != nil { return errors.NewError(err.Error()) } diff --git a/internal/members/members.go b/internal/members/members.go index 7ef553c7..4ca6de29 100644 --- a/internal/members/members.go +++ b/internal/members/members.go @@ -42,10 +42,10 @@ func (c MembersClient) Create(ctx context.Context, accessToken string, baseURI s if err != nil { return nil, errors.NewLDAPIError(err) } - memberJson, err := json.Marshal(members.Items) + membersJson, err := json.Marshal(members) if err != nil { return nil, err } - return memberJson, nil + return membersJson, nil } diff --git a/internal/output/output.go b/internal/output/output.go index c9bc51c0..04ee0717 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -60,10 +60,6 @@ type resources struct { Items []resource `json:"items"` } -// 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) { @@ -73,67 +69,19 @@ func CmdOutputSingular(outputKind string, input []byte, fn PlaintextOutputFn) (s return "", err } - return outputFromKind(outputKind, "", SingularOutputter{ - outputFn: fn, - resource: r, - resourceJSON: input, - }) -} - -// CmdOutputCreate builds a command response based on the flag the user provided and the shape of -// the input with a successfully created message. The expected shape is a single JSON object. -func CmdOutputCreate(outputKind string, input []byte, fn PlaintextOutputFn) (string, error) { - return cmdOutputWithMessage(outputKind, "Successfully created ", input, fn) -} - -// CmdOutputUpdate builds a command response based on the flag the user provided and the shape of -// the input with a successfully created message. The expected shape is a single JSON object. -func CmdOutputUpdate(outputKind string, input []byte, fn PlaintextOutputFn) (string, error) { - return cmdOutputWithMessage(outputKind, "Successfully updated ", input, fn) -} - -func cmdOutputWithMessage(outputKind string, message string, input []byte, fn PlaintextOutputFn) (string, error) { - var r resource - err := json.Unmarshal(input, &r) - if err != nil { - return "", err - } - - return outputFromKind(outputKind, message, SingularOutputter{ + 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, additional string, o Outputter) (string, error) { +func outputFromKind(outputKind string, o Outputter) (string, error) { switch outputKind { case "json": return o.JSON(), nil case "plaintext": - return additional + o.String(), nil + return o.String(), nil } return "", ErrInvalidOutputKind diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 430e491f..12d58dee 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -64,100 +64,3 @@ func TestCmdOutputSingular(t *testing.T) { }) } } - -func TestCmdOutput_WithSuccessMessage(t *testing.T) { - tests := map[string]struct { - expected string - fn func(string, []byte, output.PlaintextOutputFn) (string, error) - input string - outputKind string - }{ - "when creating with json": { - expected: `{ - "key": "test-key", - "name": "test-name" - }`, - fn: output.CmdOutputCreate, - input: `{ - "key": "test-key", - "name": "test-name" - }`, - outputKind: "json", - }, - "when creating with plaintext": { - expected: "Successfully created test-name (test-key)", - fn: output.CmdOutputCreate, - input: `{ - "key": "test-key", - "name": "test-name" - }`, - outputKind: "plaintext", - }, - "when updating with json": { - expected: `{ - "key": "test-key", - "name": "test-name" - }`, - fn: output.CmdOutputUpdate, - input: `{ - "key": "test-key", - "name": "test-name" - }`, - outputKind: "json", - }, - "when updating with plaintext": { - expected: "Successfully updated test-name (test-key)", - fn: output.CmdOutputUpdate, - input: `{ - "key": "test-key", - "name": "test-name" - }`, - outputKind: "plaintext", - }, - } - for name, tt := range tests { - tt := tt - t.Run(name, func(t *testing.T) { - output, err := tt.fn( - tt.outputKind, - []byte(tt.input), - output.SingularPlaintextOutputFn, - ) - - require.NoError(t, err) - assert.Equal(t, tt.expected, output) - }) - } -} - -func TestCmdOutputMultiple(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/resource_output.go b/internal/output/resource_output.go new file mode 100644 index 00000000..f4882ac8 --- /dev/null +++ b/internal/output/resource_output.go @@ -0,0 +1,68 @@ +package output + +import ( + "encoding/json" + "fmt" + "strings" +) + +// CmdOutput returns a response from a resource create action formatted based on the +// output flag along with an optional message based on the action. +func CmdOutput(action string, outputKind string, input []byte) (string, error) { + if outputKind == "json" { + return string(input), nil + } + + var ( + maybeResource resource + maybeResources resources + isMultipleResponse bool + ) + + // unmarshal either a singular resource or a list of them + err := json.Unmarshal(input, &maybeResource) + _, isMultipleResponse = maybeResource["items"] + if err != nil || isMultipleResponse { + err := json.Unmarshal(input, &maybeResources) + if err != nil { + return "", err + } + } + + var successMessage string + switch action { + case "create": + successMessage = "Successfully created" + case "delete": + successMessage = "Successfully deleted" + case "update": + successMessage = "Successfully updated" + default: + // no success message + } + + if isMultipleResponse { + // the response could have various properties we want to show + outputFn := MultiplePlaintextOutputFn + if _, ok := maybeResources.Items[0]["email"]; ok { + outputFn = MultipleEmailPlaintextOutputFn + } + + items := make([]string, 0, len(maybeResources.Items)) + for _, i := range maybeResources.Items { + items = append(items, outputFn(i)) + } + + return plaintextOutput("\n"+strings.Join(items, "\n"), successMessage), nil + } + + return plaintextOutput(SingularPlaintextOutputFn(maybeResource), successMessage), nil +} + +func plaintextOutput(out string, successMessage string) string { + if successMessage != "" { + return fmt.Sprintf("%s %s", successMessage, out) + } + + return out +} diff --git a/internal/output/resource_output_test.go b/internal/output/resource_output_test.go new file mode 100644 index 00000000..e4a7e561 --- /dev/null +++ b/internal/output/resource_output_test.go @@ -0,0 +1,172 @@ +package output_test + +import ( + "ldcli/internal/output" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCmdOutput(t *testing.T) { + t.Run("when creating a resource", func(t *testing.T) { + input := `{ + "key": "test-key", + "name": "test-name", + "other": "other-value" + }` + + t.Run("with json output", func(t *testing.T) { + t.Run("returns the JSON", func(t *testing.T) { + result, err := output.CmdOutput("create", "json", []byte(input)) + + require.NoError(t, err) + assert.JSONEq(t, input, result) + }) + }) + + t.Run("with plaintext output", func(t *testing.T) { + t.Run("returns a success message", func(t *testing.T) { + expected := "Successfully created test-name (test-key)" + + result, err := output.CmdOutput("create", "plaintext", []byte(input)) + + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) + }) + + t.Run("when creating multiple resources", func(t *testing.T) { + input := `{ + "items": [ + { + "key": "test-key", + "name": "test-name", + "other": "other-value" + } + ] + }` + + t.Run("with json output", func(t *testing.T) { + t.Run("returns the JSON", func(t *testing.T) { + result, err := output.CmdOutput("create", "json", []byte(input)) + + require.NoError(t, err) + assert.JSONEq(t, input, result) + }) + }) + + t.Run("with plaintext output", func(t *testing.T) { + t.Run("returns a success message", func(t *testing.T) { + expected := "Successfully created \n* test-name (test-key)" + + result, err := output.CmdOutput("create", "plaintext", []byte(input)) + + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) + }) + + t.Run("when creating multiple with an email instead of a key", func(t *testing.T) { + input := `{ + "items": [ + { + "_id": "test-id", + "email": "test-email", + "other": "other-value" + } + ] + }` + + t.Run("with json output", func(t *testing.T) { + t.Run("returns the JSON", func(t *testing.T) { + result, err := output.CmdOutput("create", "json", []byte(input)) + + require.NoError(t, err) + assert.JSONEq(t, input, result) + }) + }) + + t.Run("with plaintext output", func(t *testing.T) { + t.Run("returns a success message", func(t *testing.T) { + expected := "Successfully created \n* test-email (test-id)" + + result, err := output.CmdOutput("create", "plaintext", []byte(input)) + + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) + }) + + t.Run("when deleting a resource", func(t *testing.T) { + input := `{ + "key": "test-key", + "name": "test-name" + }` + + t.Run("with json output", func(t *testing.T) { + t.Run("does not return anything", func(t *testing.T) { + result, err := output.CmdOutput("delete", "json", []byte("")) + + require.NoError(t, err) + assert.Equal(t, "", result) + }) + }) + + t.Run("with plaintext output", func(t *testing.T) { + t.Run("with a key and name", func(t *testing.T) { + t.Run("returns a success message", func(t *testing.T) { + expected := "Successfully deleted test-name (test-key)" + + result, err := output.CmdOutput("delete", "plaintext", []byte(input)) + + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) + + t.Run("with a key", func(t *testing.T) { + t.Skip() + t.Run("returns a success message", func(t *testing.T) { + expected := "Successfully deleted test-key" + + result, err := output.CmdOutput("delete", "plaintext", []byte(input)) + + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) + }) + }) + + t.Run("when updating a resource", func(t *testing.T) { + input := `{ + "key": "test-key", + "name": "test-name", + "other": "other-value" + }` + + t.Run("with json output", func(t *testing.T) { + t.Run("returns the JSON", func(t *testing.T) { + result, err := output.CmdOutput("update", "json", []byte(input)) + + require.NoError(t, err) + assert.JSONEq(t, input, result) + }) + }) + + t.Run("with plaintext output", func(t *testing.T) { + t.Run("returns a success message", func(t *testing.T) { + expected := "Successfully updated test-name (test-key)" + + result, err := output.CmdOutput("update", "plaintext", []byte(input)) + + require.NoError(t, err) + assert.Equal(t, expected, result) + }) + }) + }) +}