Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions internal/commands/checkouts/checkouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,6 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("create checkout: %w", err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, checkout)
}

if err := message.Success(appCtx.StatusOutput, "Checkout created"); err != nil {
return err
}
details := display.NewDetailsBuilder()
if checkout.ID != nil {
details.AddID(*checkout.ID)
Expand All @@ -252,7 +245,11 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error {
details.Add("Status", attribute.Styled(string(*checkout.Status)))
}
details.AddWhen(checkout.Description != nil && *checkout.Description != "", attribute.Attribute("Description", attribute.Styled(*checkout.Description)))
return details.Render(appCtx.Output)
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: checkout,
SuccessMessage: "Checkout created",
Details: details.Pairs(),
})
}

func deactivateCheckout(ctx context.Context, cmd *cli.Command) error {
Expand All @@ -269,13 +266,6 @@ func deactivateCheckout(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("deactivate checkout: %w", err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, checkout)
}

if err := message.Success(appCtx.StatusOutput, "Checkout deactivated"); err != nil {
return err
}
details := display.NewDetailsBuilder()
if checkout.ID != nil {
details.AddID(*checkout.ID)
Expand All @@ -289,7 +279,11 @@ func deactivateCheckout(ctx context.Context, cmd *cli.Command) error {
details.Add("Valid Until", attribute.Styled(util.TimeOrDash(appCtx, validUntil)))
}
}
return details.Render(appCtx.Output)
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: checkout,
SuccessMessage: "Checkout deactivated",
Details: details.Pairs(),
})
}

func getCheckout(ctx context.Context, cmd *cli.Command) error {
Expand Down
30 changes: 14 additions & 16 deletions internal/commands/customers/customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,13 @@ func createCustomer(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("create customer: %w", err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, customer)
}

if err := message.Success(appCtx.StatusOutput, "Customer created"); err != nil {
return err
}
return renderCustomer(appCtx.Output, customer)
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: customer,
SuccessMessage: "Customer created",
RenderHuman: func(w io.Writer) error {
return renderCustomer(w, customer)
},
})
}

func getCustomer(ctx context.Context, cmd *cli.Command) error {
Expand Down Expand Up @@ -197,14 +196,13 @@ func updateCustomer(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("update customer: %w", err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, customer)
}

if err := message.Success(appCtx.StatusOutput, "Customer updated"); err != nil {
return err
}
return renderCustomer(appCtx.Output, customer)
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: customer,
SuccessMessage: "Customer updated",
RenderHuman: func(w io.Writer) error {
return renderCustomer(w, customer)
},
})
}

func listPaymentInstruments(ctx context.Context, cmd *cli.Command) error {
Expand Down
30 changes: 12 additions & 18 deletions internal/commands/members/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,12 @@ func createMember(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("create member: %w", err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, response)
}

if err := message.Success(appCtx.StatusOutput, "Member created"); err != nil {
return err
}
return display.DataList(appCtx.Output, []attribute.KeyValue{
attribute.ID(response.ID),
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: response,
SuccessMessage: "Member created",
Details: []attribute.KeyValue{
attribute.ID(response.ID),
},
})
}

Expand All @@ -301,15 +298,12 @@ func inviteMember(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("invite member: %w", err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, response)
}

if err := message.Success(appCtx.StatusOutput, "Member invited"); err != nil {
return err
}
return display.DataList(appCtx.Output, []attribute.KeyValue{
attribute.ID(response.ID),
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: response,
SuccessMessage: "Member invited",
Details: []attribute.KeyValue{
attribute.ID(response.ID),
},
})
}

Expand Down
32 changes: 14 additions & 18 deletions internal/commands/readers/readers.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,19 +259,16 @@ func addReader(ctx context.Context, cmd *cli.Command) error {
return formatCreateReaderError(err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, reader)
}

if err := message.Success(appCtx.StatusOutput, "Reader created"); err != nil {
return err
}
return display.DataList(appCtx.Output, []attribute.KeyValue{
attribute.ID(string(reader.ID)),
attribute.Attribute("Name", attribute.Styled(string(reader.Name))),
attribute.Attribute("Status", attribute.Styled(string(reader.Status))),
attribute.Attribute("Model", attribute.Styled(string(reader.Device.Model))),
attribute.Attribute("Identifier", attribute.Styled(reader.Device.Identifier)),
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: reader,
SuccessMessage: "Reader created",
Details: []attribute.KeyValue{
attribute.ID(string(reader.ID)),
attribute.Attribute("Name", attribute.Styled(string(reader.Name))),
attribute.Attribute("Status", attribute.Styled(string(reader.Status))),
attribute.Attribute("Model", attribute.Styled(string(reader.Device.Model))),
attribute.Attribute("Identifier", attribute.Styled(reader.Device.Identifier)),
},
})
}

Expand Down Expand Up @@ -301,11 +298,10 @@ func deleteReader(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("delete reader: %w", err)
}

if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, map[string]string{"status": "deleted"})
}

return message.Success(appCtx.StatusOutput, "Reader deleted")
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: map[string]string{"status": "deleted"},
SuccessMessage: "Reader deleted",
})
}

func readerCheckout(ctx context.Context, cmd *cli.Command) error {
Expand Down
10 changes: 4 additions & 6 deletions internal/commands/transactions/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"github.com/sumup/sumup-cli/internal/currency"
"github.com/sumup/sumup-cli/internal/display"
"github.com/sumup/sumup-cli/internal/display/attribute"
"github.com/sumup/sumup-cli/internal/display/message"
)

func NewCommand() *cli.Command {
Expand Down Expand Up @@ -243,11 +242,10 @@ func renderTransactionDetails(appCtx *app.Context, transaction *sumup.Transactio
}

func renderRefundResult(appCtx *app.Context) error {
if appCtx.JSONOutput {
return display.PrintJSON(appCtx.Output, map[string]string{"status": "refunded"})
}

return message.Success(appCtx.StatusOutput, "Transaction refunded")
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
JSONValue: map[string]string{"status": "refunded"},
SuccessMessage: "Transaction refunded",
})
}

func transactionCardLabel(card *sumup.CardResponse) string {
Expand Down
35 changes: 35 additions & 0 deletions internal/display/mutation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package display

import (
"io"

"github.com/sumup/sumup-cli/internal/display/attribute"
"github.com/sumup/sumup-cli/internal/display/message"
)

// MutationResult describes the shared output flow for mutating commands.
type MutationResult struct {
JSONValue any
SuccessMessage string
Details []attribute.KeyValue
RenderHuman func(io.Writer) error
}

// RenderMutation renders a mutating command result consistently for JSON and human output.
func RenderMutation(output, statusOutput io.Writer, jsonOutput bool, result MutationResult) error {
if jsonOutput {
return PrintJSON(output, result.JSONValue)
}

if result.SuccessMessage != "" {
if err := message.Success(statusOutput, "%s", result.SuccessMessage); err != nil {
return err
}
}

if result.RenderHuman != nil {
return result.RenderHuman(output)
}

return DataList(output, result.Details)
}
77 changes: 77 additions & 0 deletions internal/display/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package display_test

import (
"bytes"
"errors"
"io"
"regexp"
"strings"
"testing"
Expand Down Expand Up @@ -64,6 +66,73 @@ func TestDetailsBuilder(t *testing.T) {
})
}

func TestRenderMutation(t *testing.T) {
t.Run("renders json output when requested", func(t *testing.T) {
var out bytes.Buffer
var status bytes.Buffer

err := display.RenderMutation(&out, &status, true, display.MutationResult{
JSONValue: map[string]string{"status": "ok"},
SuccessMessage: "Created",
Details: []attribute.KeyValue{
attribute.Attribute("Status", attribute.Styled("ok")),
},
})

require.NoError(t, err)
assert.Equal(t, normalizeOutput("{\n \"status\": \"ok\"\n}\n"), normalizeOutput(out.String()))
assert.Empty(t, status.String())
})

t.Run("renders success message and details in human mode", func(t *testing.T) {
var out bytes.Buffer
var status bytes.Buffer

err := display.RenderMutation(&out, &status, false, display.MutationResult{
SuccessMessage: "Created",
Details: []attribute.KeyValue{
attribute.Attribute("Status", attribute.Styled("ok")),
},
})

require.NoError(t, err)
assert.Contains(t, normalizeOutput(status.String()), "Created")
assert.Equal(t, normalizeOutput("Status: ok\n"), normalizeOutput(out.String()))
})

t.Run("prefers custom human renderer when provided", func(t *testing.T) {
var out bytes.Buffer
var status bytes.Buffer

err := display.RenderMutation(&out, &status, false, display.MutationResult{
SuccessMessage: "Updated",
Details: []attribute.KeyValue{
attribute.Attribute("Status", attribute.Styled("stale")),
},
RenderHuman: func(w io.Writer) error {
_, writeErr := w.Write([]byte("custom\n"))
return writeErr
},
})

require.NoError(t, err)
assert.Contains(t, normalizeOutput(status.String()), "Updated")
assert.Equal(t, "custom\n", out.String())
})

t.Run("returns status writer errors", func(t *testing.T) {
out := &bytes.Buffer{}
status := failWriter{}

err := display.RenderMutation(out, status, false, display.MutationResult{
SuccessMessage: "Created",
})

require.Error(t, err)
assert.ErrorIs(t, err, errWriteFailed)
})
}

func TestRenderTable(t *testing.T) {
t.Run("renders title and row content", func(t *testing.T) {
var out bytes.Buffer
Expand Down Expand Up @@ -108,6 +177,14 @@ func TestRenderTableWithOptions(t *testing.T) {

var ansiPattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)

var errWriteFailed = errors.New("write failed")

type failWriter struct{}

func (failWriter) Write(_ []byte) (int, error) {
return 0, errWriteFailed
}

func normalizeOutput(value string) string {
value = ansiPattern.ReplaceAllString(value, "")
lines := strings.Split(strings.ReplaceAll(value, "\r\n", "\n"), "\n")
Expand Down