Skip to content

Commit 82c1cb6

Browse files
committed
feat(display): add shared mutation renderer
Introduce a shared mutation output helper for the common JSON or success plus details flow. Refactor representative commands to use the new renderer and cover the helper with display-level tests.
1 parent 15b78bd commit 82c1cb6

File tree

7 files changed

+166
-74
lines changed

7 files changed

+166
-74
lines changed

internal/commands/checkouts/checkouts.go

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,6 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error {
236236
return fmt.Errorf("create checkout: %w", err)
237237
}
238238

239-
if appCtx.JSONOutput {
240-
return display.PrintJSON(appCtx.Output, checkout)
241-
}
242-
243-
if err := message.Success(appCtx.StatusOutput, "Checkout created"); err != nil {
244-
return err
245-
}
246239
details := make([]attribute.KeyValue, 0, 5)
247240
if checkout.ID != nil {
248241
details = append(details, attribute.ID(*checkout.ID))
@@ -255,7 +248,11 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error {
255248
if checkout.Description != nil && *checkout.Description != "" {
256249
details = append(details, attribute.Attribute("Description", attribute.Styled(*checkout.Description)))
257250
}
258-
return display.DataList(appCtx.Output, details)
251+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
252+
JSONValue: checkout,
253+
SuccessMessage: "Checkout created",
254+
Details: details,
255+
})
259256
}
260257

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

275-
if appCtx.JSONOutput {
276-
return display.PrintJSON(appCtx.Output, checkout)
277-
}
278-
279-
if err := message.Success(appCtx.StatusOutput, "Checkout deactivated"); err != nil {
280-
return err
281-
}
282272
details := make([]attribute.KeyValue, 0, 4)
283273
if checkout.ID != nil {
284274
details = append(details, attribute.ID(*checkout.ID))
@@ -292,7 +282,11 @@ func deactivateCheckout(ctx context.Context, cmd *cli.Command) error {
292282
details = append(details, attribute.Attribute("Valid Until", attribute.Styled(util.TimeOrDash(appCtx, validUntil))))
293283
}
294284
}
295-
return display.DataList(appCtx.Output, details)
285+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
286+
JSONValue: checkout,
287+
SuccessMessage: "Checkout deactivated",
288+
Details: details,
289+
})
296290
}
297291

298292
func getCheckout(ctx context.Context, cmd *cli.Command) error {

internal/commands/customers/customers.go

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,13 @@ func createCustomer(ctx context.Context, cmd *cli.Command) error {
137137
return fmt.Errorf("create customer: %w", err)
138138
}
139139

140-
if appCtx.JSONOutput {
141-
return display.PrintJSON(appCtx.Output, customer)
142-
}
143-
144-
if err := message.Success(appCtx.StatusOutput, "Customer created"); err != nil {
145-
return err
146-
}
147-
return renderCustomer(appCtx.Output, customer)
140+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
141+
JSONValue: customer,
142+
SuccessMessage: "Customer created",
143+
RenderHuman: func(w io.Writer) error {
144+
return renderCustomer(w, customer)
145+
},
146+
})
148147
}
149148

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

200-
if appCtx.JSONOutput {
201-
return display.PrintJSON(appCtx.Output, customer)
202-
}
203-
204-
if err := message.Success(appCtx.StatusOutput, "Customer updated"); err != nil {
205-
return err
206-
}
207-
return renderCustomer(appCtx.Output, customer)
199+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
200+
JSONValue: customer,
201+
SuccessMessage: "Customer updated",
202+
RenderHuman: func(w io.Writer) error {
203+
return renderCustomer(w, customer)
204+
},
205+
})
208206
}
209207

210208
func listPaymentInstruments(ctx context.Context, cmd *cli.Command) error {

internal/commands/members/members.go

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,12 @@ func createMember(ctx context.Context, cmd *cli.Command) error {
269269
return fmt.Errorf("create member: %w", err)
270270
}
271271

272-
if appCtx.JSONOutput {
273-
return display.PrintJSON(appCtx.Output, response)
274-
}
275-
276-
if err := message.Success(appCtx.StatusOutput, "Member created"); err != nil {
277-
return err
278-
}
279-
return display.DataList(appCtx.Output, []attribute.KeyValue{
280-
attribute.ID(response.ID),
272+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
273+
JSONValue: response,
274+
SuccessMessage: "Member created",
275+
Details: []attribute.KeyValue{
276+
attribute.ID(response.ID),
277+
},
281278
})
282279
}
283280

@@ -302,15 +299,12 @@ func inviteMember(ctx context.Context, cmd *cli.Command) error {
302299
return fmt.Errorf("invite member: %w", err)
303300
}
304301

305-
if appCtx.JSONOutput {
306-
return display.PrintJSON(appCtx.Output, response)
307-
}
308-
309-
if err := message.Success(appCtx.StatusOutput, "Member invited"); err != nil {
310-
return err
311-
}
312-
return display.DataList(appCtx.Output, []attribute.KeyValue{
313-
attribute.ID(response.ID),
302+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
303+
JSONValue: response,
304+
SuccessMessage: "Member invited",
305+
Details: []attribute.KeyValue{
306+
attribute.ID(response.ID),
307+
},
314308
})
315309
}
316310

internal/commands/readers/readers.go

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -260,19 +260,16 @@ func addReader(ctx context.Context, cmd *cli.Command) error {
260260
return formatCreateReaderError(err)
261261
}
262262

263-
if appCtx.JSONOutput {
264-
return display.PrintJSON(appCtx.Output, reader)
265-
}
266-
267-
if err := message.Success(appCtx.StatusOutput, "Reader created"); err != nil {
268-
return err
269-
}
270-
return display.DataList(appCtx.Output, []attribute.KeyValue{
271-
attribute.ID(string(reader.ID)),
272-
attribute.Attribute("Name", attribute.Styled(string(reader.Name))),
273-
attribute.Attribute("Status", attribute.Styled(string(reader.Status))),
274-
attribute.Attribute("Model", attribute.Styled(string(reader.Device.Model))),
275-
attribute.Attribute("Identifier", attribute.Styled(reader.Device.Identifier)),
263+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
264+
JSONValue: reader,
265+
SuccessMessage: "Reader created",
266+
Details: []attribute.KeyValue{
267+
attribute.ID(string(reader.ID)),
268+
attribute.Attribute("Name", attribute.Styled(string(reader.Name))),
269+
attribute.Attribute("Status", attribute.Styled(string(reader.Status))),
270+
attribute.Attribute("Model", attribute.Styled(string(reader.Device.Model))),
271+
attribute.Attribute("Identifier", attribute.Styled(reader.Device.Identifier)),
272+
},
276273
})
277274
}
278275

@@ -302,11 +299,10 @@ func deleteReader(ctx context.Context, cmd *cli.Command) error {
302299
return fmt.Errorf("delete reader: %w", err)
303300
}
304301

305-
if appCtx.JSONOutput {
306-
return display.PrintJSON(appCtx.Output, map[string]string{"status": "deleted"})
307-
}
308-
309-
return message.Success(appCtx.StatusOutput, "Reader deleted")
302+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
303+
JSONValue: map[string]string{"status": "deleted"},
304+
SuccessMessage: "Reader deleted",
305+
})
310306
}
311307

312308
func readerCheckout(ctx context.Context, cmd *cli.Command) error {

internal/commands/transactions/transactions.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/sumup/sumup-cli/internal/currency"
1616
"github.com/sumup/sumup-cli/internal/display"
1717
"github.com/sumup/sumup-cli/internal/display/attribute"
18-
"github.com/sumup/sumup-cli/internal/display/message"
1918
)
2019

2120
func NewCommand() *cli.Command {
@@ -244,11 +243,10 @@ func renderTransactionDetails(appCtx *app.Context, transaction *sumup.Transactio
244243
}
245244

246245
func renderRefundResult(appCtx *app.Context) error {
247-
if appCtx.JSONOutput {
248-
return display.PrintJSON(appCtx.Output, map[string]string{"status": "refunded"})
249-
}
250-
251-
return message.Success(appCtx.StatusOutput, "Transaction refunded")
246+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
247+
JSONValue: map[string]string{"status": "refunded"},
248+
SuccessMessage: "Transaction refunded",
249+
})
252250
}
253251

254252
func transactionCardLabel(card *sumup.CardResponse) string {

internal/display/mutation.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package display
2+
3+
import (
4+
"io"
5+
6+
"github.com/sumup/sumup-cli/internal/display/attribute"
7+
"github.com/sumup/sumup-cli/internal/display/message"
8+
)
9+
10+
// MutationResult describes the shared output flow for mutating commands.
11+
type MutationResult struct {
12+
JSONValue any
13+
SuccessMessage string
14+
Details []attribute.KeyValue
15+
RenderHuman func(io.Writer) error
16+
}
17+
18+
// RenderMutation renders a mutating command result consistently for JSON and human output.
19+
func RenderMutation(output, statusOutput io.Writer, jsonOutput bool, result MutationResult) error {
20+
if jsonOutput {
21+
return PrintJSON(output, result.JSONValue)
22+
}
23+
24+
if result.SuccessMessage != "" {
25+
if err := message.Success(statusOutput, "%s", result.SuccessMessage); err != nil {
26+
return err
27+
}
28+
}
29+
30+
if result.RenderHuman != nil {
31+
return result.RenderHuman(output)
32+
}
33+
34+
return DataList(output, result.Details)
35+
}

internal/display/output_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package display_test
22

33
import (
44
"bytes"
5+
"errors"
6+
"io"
57
"regexp"
68
"strings"
79
"testing"
@@ -37,6 +39,73 @@ func TestDataList(t *testing.T) {
3739
})
3840
}
3941

42+
func TestRenderMutation(t *testing.T) {
43+
t.Run("renders json output when requested", func(t *testing.T) {
44+
var out bytes.Buffer
45+
var status bytes.Buffer
46+
47+
err := display.RenderMutation(&out, &status, true, display.MutationResult{
48+
JSONValue: map[string]string{"status": "ok"},
49+
SuccessMessage: "Created",
50+
Details: []attribute.KeyValue{
51+
attribute.Attribute("Status", attribute.Styled("ok")),
52+
},
53+
})
54+
55+
require.NoError(t, err)
56+
assert.Equal(t, normalizeOutput("{\n \"status\": \"ok\"\n}\n"), normalizeOutput(out.String()))
57+
assert.Empty(t, status.String())
58+
})
59+
60+
t.Run("renders success message and details in human mode", func(t *testing.T) {
61+
var out bytes.Buffer
62+
var status bytes.Buffer
63+
64+
err := display.RenderMutation(&out, &status, false, display.MutationResult{
65+
SuccessMessage: "Created",
66+
Details: []attribute.KeyValue{
67+
attribute.Attribute("Status", attribute.Styled("ok")),
68+
},
69+
})
70+
71+
require.NoError(t, err)
72+
assert.Contains(t, normalizeOutput(status.String()), "Created")
73+
assert.Equal(t, normalizeOutput("Status: ok\n"), normalizeOutput(out.String()))
74+
})
75+
76+
t.Run("prefers custom human renderer when provided", func(t *testing.T) {
77+
var out bytes.Buffer
78+
var status bytes.Buffer
79+
80+
err := display.RenderMutation(&out, &status, false, display.MutationResult{
81+
SuccessMessage: "Updated",
82+
Details: []attribute.KeyValue{
83+
attribute.Attribute("Status", attribute.Styled("stale")),
84+
},
85+
RenderHuman: func(w io.Writer) error {
86+
_, writeErr := w.Write([]byte("custom\n"))
87+
return writeErr
88+
},
89+
})
90+
91+
require.NoError(t, err)
92+
assert.Contains(t, normalizeOutput(status.String()), "Updated")
93+
assert.Equal(t, "custom\n", out.String())
94+
})
95+
96+
t.Run("returns status writer errors", func(t *testing.T) {
97+
out := &bytes.Buffer{}
98+
status := failWriter{}
99+
100+
err := display.RenderMutation(out, status, false, display.MutationResult{
101+
SuccessMessage: "Created",
102+
})
103+
104+
require.Error(t, err)
105+
assert.ErrorIs(t, err, errWriteFailed)
106+
})
107+
}
108+
40109
func TestRenderTable(t *testing.T) {
41110
t.Run("renders title and row content", func(t *testing.T) {
42111
var out bytes.Buffer
@@ -65,6 +134,14 @@ func TestRenderTableWithOptionsSupportsEmptyTextWithoutTitle(t *testing.T) {
65134

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

137+
var errWriteFailed = errors.New("write failed")
138+
139+
type failWriter struct{}
140+
141+
func (failWriter) Write(_ []byte) (int, error) {
142+
return 0, errWriteFailed
143+
}
144+
68145
func normalizeOutput(value string) string {
69146
value = ansiPattern.ReplaceAllString(value, "")
70147
lines := strings.Split(strings.ReplaceAll(value, "\r\n", "\n"), "\n")

0 commit comments

Comments
 (0)