Skip to content

Commit 3f13341

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 6bcdee8 commit 3f13341

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
@@ -235,13 +235,6 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error {
235235
return fmt.Errorf("create checkout: %w", err)
236236
}
237237

238-
if appCtx.JSONOutput {
239-
return display.PrintJSON(appCtx.Output, checkout)
240-
}
241-
242-
if err := message.Success(appCtx.StatusOutput, "Checkout created"); err != nil {
243-
return err
244-
}
245238
details := display.NewDetailsBuilder()
246239
if checkout.ID != nil {
247240
details.AddID(*checkout.ID)
@@ -252,7 +245,11 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error {
252245
details.Add("Status", attribute.Styled(string(*checkout.Status)))
253246
}
254247
details.AddWhen(checkout.Description != nil && *checkout.Description != "", attribute.Attribute("Description", attribute.Styled(*checkout.Description)))
255-
return details.Render(appCtx.Output)
248+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
249+
JSONValue: checkout,
250+
SuccessMessage: "Checkout created",
251+
Details: details.Pairs(),
252+
})
256253
}
257254

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

272-
if appCtx.JSONOutput {
273-
return display.PrintJSON(appCtx.Output, checkout)
274-
}
275-
276-
if err := message.Success(appCtx.StatusOutput, "Checkout deactivated"); err != nil {
277-
return err
278-
}
279269
details := display.NewDetailsBuilder()
280270
if checkout.ID != nil {
281271
details.AddID(*checkout.ID)
@@ -289,7 +279,11 @@ func deactivateCheckout(ctx context.Context, cmd *cli.Command) error {
289279
details.Add("Valid Until", attribute.Styled(util.TimeOrDash(appCtx, validUntil)))
290280
}
291281
}
292-
return details.Render(appCtx.Output)
282+
return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{
283+
JSONValue: checkout,
284+
SuccessMessage: "Checkout deactivated",
285+
Details: details.Pairs(),
286+
})
293287
}
294288

295289
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
@@ -268,15 +268,12 @@ func createMember(ctx context.Context, cmd *cli.Command) error {
268268
return fmt.Errorf("create member: %w", err)
269269
}
270270

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

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

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

internal/commands/readers/readers.go

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

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

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

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

311307
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 {
@@ -243,11 +242,10 @@ func renderTransactionDetails(appCtx *app.Context, transaction *sumup.Transactio
243242
}
244243

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

253251
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"
@@ -64,6 +66,73 @@ func TestDetailsBuilder(t *testing.T) {
6466
})
6567
}
6668

69+
func TestRenderMutation(t *testing.T) {
70+
t.Run("renders json output when requested", func(t *testing.T) {
71+
var out bytes.Buffer
72+
var status bytes.Buffer
73+
74+
err := display.RenderMutation(&out, &status, true, display.MutationResult{
75+
JSONValue: map[string]string{"status": "ok"},
76+
SuccessMessage: "Created",
77+
Details: []attribute.KeyValue{
78+
attribute.Attribute("Status", attribute.Styled("ok")),
79+
},
80+
})
81+
82+
require.NoError(t, err)
83+
assert.Equal(t, normalizeOutput("{\n \"status\": \"ok\"\n}\n"), normalizeOutput(out.String()))
84+
assert.Empty(t, status.String())
85+
})
86+
87+
t.Run("renders success message and details in human mode", func(t *testing.T) {
88+
var out bytes.Buffer
89+
var status bytes.Buffer
90+
91+
err := display.RenderMutation(&out, &status, false, display.MutationResult{
92+
SuccessMessage: "Created",
93+
Details: []attribute.KeyValue{
94+
attribute.Attribute("Status", attribute.Styled("ok")),
95+
},
96+
})
97+
98+
require.NoError(t, err)
99+
assert.Contains(t, normalizeOutput(status.String()), "Created")
100+
assert.Equal(t, normalizeOutput("Status: ok\n"), normalizeOutput(out.String()))
101+
})
102+
103+
t.Run("prefers custom human renderer when provided", func(t *testing.T) {
104+
var out bytes.Buffer
105+
var status bytes.Buffer
106+
107+
err := display.RenderMutation(&out, &status, false, display.MutationResult{
108+
SuccessMessage: "Updated",
109+
Details: []attribute.KeyValue{
110+
attribute.Attribute("Status", attribute.Styled("stale")),
111+
},
112+
RenderHuman: func(w io.Writer) error {
113+
_, writeErr := w.Write([]byte("custom\n"))
114+
return writeErr
115+
},
116+
})
117+
118+
require.NoError(t, err)
119+
assert.Contains(t, normalizeOutput(status.String()), "Updated")
120+
assert.Equal(t, "custom\n", out.String())
121+
})
122+
123+
t.Run("returns status writer errors", func(t *testing.T) {
124+
out := &bytes.Buffer{}
125+
status := failWriter{}
126+
127+
err := display.RenderMutation(out, status, false, display.MutationResult{
128+
SuccessMessage: "Created",
129+
})
130+
131+
require.Error(t, err)
132+
assert.ErrorIs(t, err, errWriteFailed)
133+
})
134+
}
135+
67136
func TestRenderTable(t *testing.T) {
68137
t.Run("renders title and row content", func(t *testing.T) {
69138
var out bytes.Buffer
@@ -108,6 +177,14 @@ func TestRenderTableWithOptions(t *testing.T) {
108177

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

180+
var errWriteFailed = errors.New("write failed")
181+
182+
type failWriter struct{}
183+
184+
func (failWriter) Write(_ []byte) (int, error) {
185+
return 0, errWriteFailed
186+
}
187+
111188
func normalizeOutput(value string) string {
112189
value = ansiPattern.ReplaceAllString(value, "")
113190
lines := strings.Split(strings.ReplaceAll(value, "\r\n", "\n"), "\n")

0 commit comments

Comments
 (0)