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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ Run the generator after updating `sumup-go`:
make generate
```

[`internal/commands/operations.go`](internal/commands/operations.go) maps CLI
command paths to generated OpenAPI operation IDs. Tests enforce parity between
the pinned SDK, the generated catalog, and the CLI command tree, so an SDK
upgrade fails CI until every new endpoint has a corresponding command.
Each API leaf binds its generated OpenAPI operation ID directly in the command
definition. Tests enforce a one-to-one relationship between the pinned SDK,
the generated catalog, and the CLI command tree, so an SDK upgrade fails CI
until every new endpoint has exactly one corresponding command.

### Developer portal code samples

Expand Down
7 changes: 5 additions & 2 deletions internal/apicommands/apicommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ func Lookup(operationID string) (Operation, bool) {
return Operation{}, false
}

// Bind records which OpenAPI operation a CLI command exposes.
func Bind(command *cli.Command, operationID string) {
// Bind records which OpenAPI operation a CLI command exposes and returns the
// command so the binding can live next to the command definition.
func Bind(operationID string, command *cli.Command) *cli.Command {
if command == nil {
panic("cannot bind an OpenAPI operation to a nil command")
}
Expand All @@ -64,6 +65,8 @@ func Bind(command *cli.Command, operationID string) {
command.Metadata = make(map[string]any)
}
command.Metadata[operationIDMetadataKey] = operationID

return command
}

// OperationID returns the OpenAPI operation ID bound to a CLI command.
Expand Down
28 changes: 6 additions & 22 deletions internal/codesamples/codesamples.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@ type sampleFlag struct {

var argumentPattern = regexp.MustCompile(`[<\[]([a-z0-9-]+)[>\]]`)

// canonicalCommandPaths resolves operations exposed through more than one CLI
// command. CreateMerchantMember is also used by the convenience invite flow,
// while the full create command is the canonical portal example.
var canonicalCommandPaths = map[string]string{
"CreateMerchantMember": "members create",
}

// optionalSampleFlags adds a representative field where an otherwise valid
// command would not show what a create or update request changes.
var optionalSampleFlags = map[string]map[string]string{
Expand Down Expand Up @@ -140,7 +133,7 @@ func Generate(cliVersion string) (*Catalog, error) {
commandsByOperation := boundCommandsByOperation(commands.All())
samples := make([]Sample, 0, len(apicommands.Operations))
for _, operation := range apicommands.Operations {
command, err := canonicalCommand(operation.ID, commandsByOperation[operation.ID])
command, err := commandForOperation(operation.ID, commandsByOperation[operation.ID])
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -199,29 +192,20 @@ func boundCommandsByOperation(resourceCommands []*cli.Command) map[string][]boun
return result
}

func canonicalCommand(operationID string, candidates []boundCommand) (boundCommand, error) {
func commandForOperation(operationID string, candidates []boundCommand) (boundCommand, error) {
switch len(candidates) {
case 0:
return boundCommand{}, fmt.Errorf("OpenAPI operation %q has no CLI command", operationID)
case 1:
return candidates[0], nil
}

preferredPath, ok := canonicalCommandPaths[operationID]
if !ok {
paths := make([]string, 0, len(candidates))
for _, candidate := range candidates {
paths = append(paths, candidate.path)
}
slices.Sort(paths)
return boundCommand{}, fmt.Errorf("OpenAPI operation %q has multiple CLI commands (%s); select a canonical command", operationID, strings.Join(paths, ", "))
}
paths := make([]string, 0, len(candidates))
for _, candidate := range candidates {
if candidate.path == preferredPath {
return candidate, nil
}
paths = append(paths, candidate.path)
}
return boundCommand{}, fmt.Errorf("canonical CLI command %q for OpenAPI operation %q does not exist", preferredPath, operationID)
slices.Sort(paths)
return boundCommand{}, fmt.Errorf("OpenAPI operation %q has multiple CLI commands (%s)", operationID, strings.Join(paths, ", "))
}

func renderCommand(bound boundCommand) (string, error) {
Expand Down
8 changes: 4 additions & 4 deletions internal/codesamples/codesamples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestGeneratedInvocationsReachAPITransport(t *testing.T) {
t.Run(operation.ID, func(t *testing.T) {
resourceCommands := commands.All()
commandsByOperation := boundCommandsByOperation(resourceCommands)
bound, err := canonicalCommand(operation.ID, commandsByOperation[operation.ID])
bound, err := commandForOperation(operation.ID, commandsByOperation[operation.ID])
require.NoError(t, err)
invocation, err := buildInvocation(bound)
require.NoError(t, err)
Expand Down Expand Up @@ -135,12 +135,12 @@ func TestGeneratedInvocationsReachAPITransport(t *testing.T) {
}
}

func TestCanonicalCommandRequiresExplicitDuplicateSelection(t *testing.T) {
func TestCommandForOperationRejectsDuplicates(t *testing.T) {
t.Parallel()

_, err := canonicalCommand("ExampleOperation", []boundCommand{{path: "examples first"}, {path: "examples second"}})
_, err := commandForOperation("ExampleOperation", []boundCommand{{path: "examples first"}, {path: "examples second"}})

require.EqualError(t, err, `OpenAPI operation "ExampleOperation" has multiple CLI commands (examples first, examples second); select a canonical command`)
require.EqualError(t, err, `OpenAPI operation "ExampleOperation" has multiple CLI commands (examples first, examples second)`)
}

func sampleByID(t *testing.T, samples []Sample, id string) Sample {
Expand Down
33 changes: 17 additions & 16 deletions internal/commands/checkouts/checkouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/sumup/sumup-go/datetime"
"github.com/sumup/sumup-go/nullable"

"github.com/sumup/sumup-cli/internal/apicommands"
"github.com/sumup/sumup-cli/internal/app"
"github.com/sumup/sumup-cli/internal/commands/util"
"github.com/sumup/sumup-cli/internal/currency"
Expand All @@ -26,7 +27,7 @@ func NewCommand() *cli.Command {
Name: "checkouts",
Usage: "Commands related to hosted sumup.",
Commands: []*cli.Command{
{
apicommands.Bind("ListCheckouts", &cli.Command{
Name: "list",
Usage: "List checkout resources.",
Action: listCheckouts,
Expand All @@ -36,8 +37,8 @@ func NewCommand() *cli.Command {
Usage: "Filter results by checkout reference.",
},
},
},
{
}),
apicommands.Bind("CreateCheckout", &cli.Command{
Name: "create",
Usage: "Create a new checkout resource.",
Description: `Examples:
Expand Down Expand Up @@ -94,14 +95,14 @@ func NewCommand() *cli.Command {
Usage: "Enable the SumUp-hosted checkout page and return its URL.",
},
},
},
{
}),
apicommands.Bind("DeactivateCheckout", &cli.Command{
Name: "deactivate",
Usage: "Deactivate a checkout by ID.",
Action: deactivateCheckout,
ArgsUsage: "<checkout-id>",
},
{
}),
apicommands.Bind("CreateApplePaySession", &cli.Command{
Name: "apple-pay-session",
Usage: "Create an Apple Pay merchant session for a checkout.",
Action: createApplePaySession,
Expand All @@ -118,14 +119,14 @@ func NewCommand() *cli.Command {
Required: true,
},
},
},
{
}),
apicommands.Bind("GetCheckout", &cli.Command{
Name: "get",
Usage: "Get a checkout by ID.",
Action: getCheckout,
ArgsUsage: "<checkout-id>",
},
{
}),
apicommands.Bind("UpdateCheckout", &cli.Command{
Name: "update",
Usage: "Update a checkout by ID.",
Description: `Examples:
Expand Down Expand Up @@ -163,8 +164,8 @@ func NewCommand() *cli.Command {
Usage: "Clear the checkout expiration timestamp.",
},
},
},
{
}),
apicommands.Bind("GetPaymentMethods", &cli.Command{
Name: "payment-methods",
Usage: "List available payment methods for a merchant.",
Action: listPaymentMethods,
Expand All @@ -183,8 +184,8 @@ func NewCommand() *cli.Command {
Usage: fmt.Sprintf("Optional currency filter. Supported: %s", strings.Join(currency.Supported(), ", ")),
},
},
},
{
}),
apicommands.Bind("ProcessCheckout", &cli.Command{
Name: "process",
Usage: "Process a checkout.",
Action: processCheckout,
Expand All @@ -205,7 +206,7 @@ func NewCommand() *cli.Command {
&cli.StringFlag{Name: "tax-id", Usage: "Customer tax ID."},
&cli.StringFlag{Name: "birth-date", Usage: "Customer birth date in YYYY-MM-DD format."},
},
},
}),
},
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

// All returns the list of resource commands exposed by the CLI.
func All() []*cli.Command {
return bindOpenAPIOperations([]*cli.Command{
return []*cli.Command{
checkouts.NewCommand(),
context.NewCommand(),
customers.NewCommand(),
Expand All @@ -32,5 +32,5 @@ func All() []*cli.Command {
roles.NewCommand(),
transactions.NewCommand(),
version.NewCommand(),
})
}
}
21 changes: 11 additions & 10 deletions internal/commands/customers/customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
sumup "github.com/sumup/sumup-go"
"github.com/sumup/sumup-go/datetime"

"github.com/sumup/sumup-cli/internal/apicommands"
"github.com/sumup/sumup-cli/internal/app"
"github.com/sumup/sumup-cli/internal/commands/util"
"github.com/sumup/sumup-cli/internal/display"
Expand All @@ -24,41 +25,41 @@ func NewCommand() *cli.Command {
Name: "customers",
Usage: "Commands for managing customers.",
Commands: []*cli.Command{
{
apicommands.Bind("CreateCustomer", &cli.Command{
Name: "create",
Usage: "Create a customer.",
Action: createCustomer,
Flags: customerDetailsFlags(),
},
{
}),
apicommands.Bind("GetCustomer", &cli.Command{
Name: "get",
Usage: "Get a customer by ID.",
Action: getCustomer,
ArgsUsage: "<customer-id>",
},
{
}),
apicommands.Bind("UpdateCustomer", &cli.Command{
Name: "update",
Usage: "Update customer details.",
Action: updateCustomer,
ArgsUsage: "<customer-id>",
Flags: customerDetailsFlags(),
},
}),
{
Name: "payment-instruments",
Usage: "Manage stored payment instruments for a customer.",
Commands: []*cli.Command{
{
apicommands.Bind("ListPaymentInstruments", &cli.Command{
Name: "list",
Usage: "List stored payment instruments for a customer.",
Action: listPaymentInstruments,
ArgsUsage: "<customer-id>",
},
{
}),
apicommands.Bind("DeactivatePaymentInstrument", &cli.Command{
Name: "deactivate",
Usage: "Deactivate a stored payment instrument.",
Action: deactivatePaymentInstrument,
ArgsUsage: "<customer-id> <token>",
},
}),
},
},
},
Expand Down
Loading