Skip to content

Commit 8fae179

Browse files
committed
refactor: introduce RunContext and runCommand to eliminate command boilerplate
Add a thin command runner (command.go) that centralises client creation, output writer, and printer setup. All 22 API-backed commands now go through runCommand, removing duplicated newClient/newPrinter/writeResult calls. Convenience helpers PrintList, PrintTotal, and WriteResult on RunContext standardise pagination footers and success messages. Net effect: -108 lines across 9 files with zero behavioural changes.
1 parent fdd9a1a commit 8fae179

10 files changed

Lines changed: 481 additions & 524 deletions

File tree

internal/cli/change.go

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,47 +27,37 @@ func newChangeListCmd() *cobra.Command {
2727
Use: "list",
2828
Short: "List changes",
2929
RunE: func(cmd *cobra.Command, args []string) error {
30-
client, err := newClient()
31-
if err != nil {
32-
return err
33-
}
30+
return runCommand(cmd, args, func(ctx *RunContext) error {
31+
startTime, err := timeutil.Parse(since)
32+
if err != nil {
33+
return fmt.Errorf("invalid --since: %w", err)
34+
}
35+
endTime, err := timeutil.Parse(until)
36+
if err != nil {
37+
return fmt.Errorf("invalid --until: %w", err)
38+
}
3439

35-
startTime, err := timeutil.Parse(since)
36-
if err != nil {
37-
return fmt.Errorf("invalid --since: %w", err)
38-
}
39-
endTime, err := timeutil.Parse(until)
40-
if err != nil {
41-
return fmt.Errorf("invalid --until: %w", err)
42-
}
40+
result, err := ctx.Client.ListChanges(cmdContext(ctx.Cmd), &flashduty.ListChangesInput{
41+
ChannelID: channelID,
42+
StartTime: startTime,
43+
EndTime: endTime,
44+
Limit: limit,
45+
Page: page,
46+
})
47+
if err != nil {
48+
return err
49+
}
4350

44-
result, err := client.ListChanges(cmdContext(cmd), &flashduty.ListChangesInput{
45-
ChannelID: channelID,
46-
StartTime: startTime,
47-
EndTime: endTime,
48-
Limit: limit,
49-
Page: page,
50-
})
51-
if err != nil {
52-
return err
53-
}
54-
55-
cols := []output.Column{
56-
{Header: "ID", Field: func(v any) string { return v.(flashduty.Change).ChangeID }},
57-
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.Change).Title }},
58-
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.Change).Status }},
59-
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.Change).ChannelName }},
60-
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.Change).StartTime) }},
61-
}
51+
cols := []output.Column{
52+
{Header: "ID", Field: func(v any) string { return v.(flashduty.Change).ChangeID }},
53+
{Header: "TITLE", MaxWidth: 50, Field: func(v any) string { return v.(flashduty.Change).Title }},
54+
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.Change).Status }},
55+
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.Change).ChannelName }},
56+
{Header: "TIME", Field: func(v any) string { return output.FormatTime(v.(flashduty.Change).StartTime) }},
57+
}
6258

63-
p := newPrinter(cmd.OutOrStdout())
64-
if err := p.Print(result.Changes, cols); err != nil {
65-
return err
66-
}
67-
if !flagJSON {
68-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Showing %d results (page %d, total %d).\n", len(result.Changes), page, result.Total)
69-
}
70-
return nil
59+
return ctx.PrintList(result.Changes, cols, len(result.Changes), page, result.Total)
60+
})
7161
},
7262
}
7363

internal/cli/channel.go

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cli
22

33
import (
4-
"fmt"
54
"strconv"
65

76
flashduty "github.com/flashcatcloud/flashduty-sdk"
@@ -25,33 +24,23 @@ func newChannelListCmd() *cobra.Command {
2524
Use: "list",
2625
Short: "List channels",
2726
RunE: func(cmd *cobra.Command, args []string) error {
28-
client, err := newClient()
29-
if err != nil {
30-
return err
31-
}
32-
33-
result, err := client.ListChannels(cmdContext(cmd), &flashduty.ListChannelsInput{
34-
Name: name,
27+
return runCommand(cmd, args, func(ctx *RunContext) error {
28+
result, err := ctx.Client.ListChannels(cmdContext(ctx.Cmd), &flashduty.ListChannelsInput{
29+
Name: name,
30+
})
31+
if err != nil {
32+
return err
33+
}
34+
35+
cols := []output.Column{
36+
{Header: "ID", Field: func(v any) string { return strconv.FormatInt(v.(flashduty.ChannelInfo).ChannelID, 10) }},
37+
{Header: "NAME", Field: func(v any) string { return v.(flashduty.ChannelInfo).ChannelName }},
38+
{Header: "TEAM", Field: func(v any) string { return v.(flashduty.ChannelInfo).TeamName }},
39+
{Header: "CREATOR", Field: func(v any) string { return v.(flashduty.ChannelInfo).CreatorName }},
40+
}
41+
42+
return ctx.PrintTotal(result.Channels, cols, result.Total)
3543
})
36-
if err != nil {
37-
return err
38-
}
39-
40-
cols := []output.Column{
41-
{Header: "ID", Field: func(v any) string { return strconv.FormatInt(v.(flashduty.ChannelInfo).ChannelID, 10) }},
42-
{Header: "NAME", Field: func(v any) string { return v.(flashduty.ChannelInfo).ChannelName }},
43-
{Header: "TEAM", Field: func(v any) string { return v.(flashduty.ChannelInfo).TeamName }},
44-
{Header: "CREATOR", Field: func(v any) string { return v.(flashduty.ChannelInfo).CreatorName }},
45-
}
46-
47-
p := newPrinter(cmd.OutOrStdout())
48-
if err := p.Print(result.Channels, cols); err != nil {
49-
return err
50-
}
51-
if !flagJSON {
52-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Total: %d\n", result.Total)
53-
}
54-
return nil
5544
},
5645
}
5746

internal/cli/command.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/flashcatcloud/flashduty-cli/internal/output"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// RunContext provides helpers for command execution. It is created by
12+
// runCommand and passed to the command's handler function.
13+
type RunContext struct {
14+
Client flashdutyClient
15+
Cmd *cobra.Command
16+
Args []string
17+
Writer io.Writer
18+
Printer output.Printer
19+
JSON bool
20+
}
21+
22+
// runCommand creates a client and RunContext, then calls fn.
23+
// It centralises setup that every API-backed command repeats.
24+
func runCommand(cmd *cobra.Command, args []string, fn func(ctx *RunContext) error) error {
25+
client, err := newClient()
26+
if err != nil {
27+
return err
28+
}
29+
ctx := &RunContext{
30+
Client: client,
31+
Cmd: cmd,
32+
Args: args,
33+
Writer: cmd.OutOrStdout(),
34+
Printer: newPrinter(cmd.OutOrStdout()),
35+
JSON: flagJSON,
36+
}
37+
return fn(ctx)
38+
}
39+
40+
// PrintList prints items as a table and appends a "Showing N results (page P, total T)." footer.
41+
func (ctx *RunContext) PrintList(items any, cols []output.Column, count, page, total int) error {
42+
if err := ctx.Printer.Print(items, cols); err != nil {
43+
return err
44+
}
45+
if !ctx.JSON {
46+
_, _ = fmt.Fprintf(ctx.Writer, "Showing %d results (page %d, total %d).\n", count, page, total)
47+
}
48+
return nil
49+
}
50+
51+
// PrintTotal prints items as a table and appends a "Total: N" footer.
52+
func (ctx *RunContext) PrintTotal(items any, cols []output.Column, total int) error {
53+
if err := ctx.Printer.Print(items, cols); err != nil {
54+
return err
55+
}
56+
if !ctx.JSON {
57+
_, _ = fmt.Fprintf(ctx.Writer, "Total: %d\n", total)
58+
}
59+
return nil
60+
}
61+
62+
// WriteResult prints a success message as plain text or JSON.
63+
func (ctx *RunContext) WriteResult(message string) {
64+
writeResult(ctx.Writer, message)
65+
}

internal/cli/escalation_rule.go

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,39 +26,36 @@ func newEscalationRuleListCmd() *cobra.Command {
2626
Use: "list",
2727
Short: "List escalation rules for a channel",
2828
RunE: func(cmd *cobra.Command, args []string) error {
29-
client, err := newClient()
30-
if err != nil {
31-
return err
32-
}
29+
return runCommand(cmd, args, func(ctx *RunContext) error {
30+
// Resolve channel name to ID if needed
31+
if channelID == 0 && channelName != "" {
32+
resolved, err := resolveChannelID(ctx.Cmd, ctx.Client, channelName)
33+
if err != nil {
34+
return err
35+
}
36+
channelID = resolved
37+
}
38+
39+
if channelID == 0 {
40+
return fmt.Errorf("--channel or --channel-name is required")
41+
}
3342

34-
// Resolve channel name to ID if needed
35-
if channelID == 0 && channelName != "" {
36-
resolved, err := resolveChannelID(cmd, client, channelName)
43+
result, err := ctx.Client.ListEscalationRules(cmdContext(ctx.Cmd), channelID)
3744
if err != nil {
3845
return err
3946
}
40-
channelID = resolved
41-
}
4247

43-
if channelID == 0 {
44-
return fmt.Errorf("--channel or --channel-name is required")
45-
}
46-
47-
result, err := client.ListEscalationRules(cmdContext(cmd), channelID)
48-
if err != nil {
49-
return err
50-
}
51-
52-
cols := []output.Column{
53-
{Header: "ID", Field: func(v any) string { return v.(flashduty.EscalationRule).RuleID }},
54-
{Header: "NAME", Field: func(v any) string { return v.(flashduty.EscalationRule).RuleName }},
55-
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.EscalationRule).ChannelName }},
56-
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.EscalationRule).Status }},
57-
{Header: "PRIORITY", Field: func(v any) string { return strconv.Itoa(v.(flashduty.EscalationRule).Priority) }},
58-
{Header: "LAYERS", Field: func(v any) string { return strconv.Itoa(len(v.(flashduty.EscalationRule).Layers)) }},
59-
}
48+
cols := []output.Column{
49+
{Header: "ID", Field: func(v any) string { return v.(flashduty.EscalationRule).RuleID }},
50+
{Header: "NAME", Field: func(v any) string { return v.(flashduty.EscalationRule).RuleName }},
51+
{Header: "CHANNEL", Field: func(v any) string { return v.(flashduty.EscalationRule).ChannelName }},
52+
{Header: "STATUS", Field: func(v any) string { return v.(flashduty.EscalationRule).Status }},
53+
{Header: "PRIORITY", Field: func(v any) string { return strconv.Itoa(v.(flashduty.EscalationRule).Priority) }},
54+
{Header: "LAYERS", Field: func(v any) string { return strconv.Itoa(len(v.(flashduty.EscalationRule).Layers)) }},
55+
}
6056

61-
return newPrinter(cmd.OutOrStdout()).Print(result.Rules, cols)
57+
return ctx.Printer.Print(result.Rules, cols)
58+
})
6259
},
6360
}
6461

internal/cli/field.go

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cli
22

33
import (
4-
"fmt"
54
"strings"
65

76
flashduty "github.com/flashcatcloud/flashduty-sdk"
@@ -25,36 +24,26 @@ func newFieldListCmd() *cobra.Command {
2524
Use: "list",
2625
Short: "List custom fields",
2726
RunE: func(cmd *cobra.Command, args []string) error {
28-
client, err := newClient()
29-
if err != nil {
30-
return err
31-
}
32-
33-
result, err := client.ListFields(cmdContext(cmd), &flashduty.ListFieldsInput{
34-
FieldName: name,
27+
return runCommand(cmd, args, func(ctx *RunContext) error {
28+
result, err := ctx.Client.ListFields(cmdContext(ctx.Cmd), &flashduty.ListFieldsInput{
29+
FieldName: name,
30+
})
31+
if err != nil {
32+
return err
33+
}
34+
35+
cols := []output.Column{
36+
{Header: "ID", Field: func(v any) string { return v.(flashduty.FieldInfo).FieldID }},
37+
{Header: "NAME", Field: func(v any) string { return v.(flashduty.FieldInfo).FieldName }},
38+
{Header: "DISPLAY_NAME", Field: func(v any) string { return v.(flashduty.FieldInfo).DisplayName }},
39+
{Header: "TYPE", Field: func(v any) string { return v.(flashduty.FieldInfo).FieldType }},
40+
{Header: "OPTIONS", MaxWidth: 50, Field: func(v any) string {
41+
return strings.Join(v.(flashduty.FieldInfo).Options, ", ")
42+
}},
43+
}
44+
45+
return ctx.PrintTotal(result.Fields, cols, result.Total)
3546
})
36-
if err != nil {
37-
return err
38-
}
39-
40-
cols := []output.Column{
41-
{Header: "ID", Field: func(v any) string { return v.(flashduty.FieldInfo).FieldID }},
42-
{Header: "NAME", Field: func(v any) string { return v.(flashduty.FieldInfo).FieldName }},
43-
{Header: "DISPLAY_NAME", Field: func(v any) string { return v.(flashduty.FieldInfo).DisplayName }},
44-
{Header: "TYPE", Field: func(v any) string { return v.(flashduty.FieldInfo).FieldType }},
45-
{Header: "OPTIONS", MaxWidth: 50, Field: func(v any) string {
46-
return strings.Join(v.(flashduty.FieldInfo).Options, ", ")
47-
}},
48-
}
49-
50-
p := newPrinter(cmd.OutOrStdout())
51-
if err := p.Print(result.Fields, cols); err != nil {
52-
return err
53-
}
54-
if !flagJSON {
55-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Total: %d\n", result.Total)
56-
}
57-
return nil
5847
},
5948
}
6049

0 commit comments

Comments
 (0)