Skip to content

Commit

Permalink
support v2 openapi (#1298)
Browse files Browse the repository at this point in the history
* wip

* pretty print v2 response

* update makefile

* update test

* fix lint errors

* wip

* remove unused method

* fix import format

* wip

* wip

* fix lint errors

* fix test

* fix test

* add timeout option to golangci linter

---------

Co-authored-by: etsai-stripe <[email protected]>
  • Loading branch information
etsai2-stripe and etsai-stripe authored Feb 5, 2025
1 parent 0f10695 commit 8e75730
Show file tree
Hide file tree
Showing 26 changed files with 1,925 additions and 228 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
uses: golangci/golangci-lint-action@v6
with:
version: v1.63.4
args: --timeout=3m
- name: Run Tests
run: make ci
shell: bash
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,9 @@ protoc-gen-plugin:
@echo "Successfully compiled proto files for plugins"
.PHONY: protoc-plugin

resource:
./scripts/sync-openapi-v2.sh
@echo "✨ Successfully built Stripe CLI with latest API resources."
.PHONY: resource

.DEFAULT_GOAL := build
1 change: 1 addition & 0 deletions api/ZOOLANDER_SHA
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
325e40328b716cb6292a159d884b6b051ee3f7c1
1,144 changes: 1,144 additions & 0 deletions api/openapi-spec/spec3.v2.sdk.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/stripe/stripe-cli

go 1.22.0

toolchain go1.23.5

require (
github.com/BurntSushi/toml v1.2.0
github.com/briandowns/spinner v1.19.0
Expand Down
2 changes: 1 addition & 1 deletion pkg/ansi/ansi.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func ColorizeJSON(json string, darkStyle bool, w io.Writer) string {
style = darkTerminalStyle
}

return string(pretty.Color([]byte(json), style))
return string(pretty.Color(pretty.Pretty([]byte(json)), style))
}

// ColorizeStatus returns a colorized number for HTTP status code
Expand Down
205 changes: 151 additions & 54 deletions pkg/cmd/resource/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"os"
"reflect"
"regexp"
"strings"

Expand Down Expand Up @@ -34,10 +35,10 @@ type OperationCmd struct {
Path string
URLParams []string

stringFlags map[string]*string
arrayFlags map[string]*[]string

data []string
stringFlags map[string]*string
arrayFlags map[string]*[]string
integerFlags map[string]*int
boolFlags map[string]*bool
}

func (oc *OperationCmd) runOperationCmd(cmd *cobra.Command, args []string) error {
Expand All @@ -51,52 +52,16 @@ func (oc *OperationCmd) runOperationCmd(cmd *cobra.Command, args []string) error
}

path := formatURL(oc.Path, args)
requestParams := make(map[string]interface{})
oc.addStringRequestParams(requestParams)
oc.addIntRequestParams(requestParams)
oc.addBoolRequestParams(requestParams)

flagParams := make([]string, 0)

for stringProp, stringVal := range oc.stringFlags {
// only include fields explicitly set by the user to avoid conflicts between e.g. account_balance, balance
if oc.Cmd.Flags().Changed(stringProp) {
paramName := strings.ReplaceAll(stringProp, "-", "_")
if strings.Contains(paramName, ".") {
fullParam := constructParamFromDot(paramName)
flagParams = append(flagParams, fmt.Sprintf("%s=%s", fullParam, *stringVal))
} else {
flagParams = append(flagParams, fmt.Sprintf("%s=%s", paramName, *stringVal))
}
}
}

for arrayProp, arrayVal := range oc.arrayFlags {
// only include fields explicitly set by the user to avoid conflicts between e.g. account_balance, balance
if oc.Cmd.Flags().Changed(arrayProp) {
paramName := strings.ReplaceAll(arrayProp, "-", "_")
for _, arrayItem := range *arrayVal {
if strings.Contains(paramName, ".") {
fullParam := constructParamFromDot(paramName)
flagParams = append(flagParams, fmt.Sprintf("%s[]=%s", fullParam, arrayItem))
} else {
flagParams = append(flagParams, fmt.Sprintf("%s[]=%s", paramName, arrayItem))
}
}
}
}

for _, datum := range oc.data {
split := strings.SplitN(datum, "=", 2)
if len(split) < 2 {
return fmt.Errorf("Invalid data argument: %s", datum)
}

if _, ok := oc.stringFlags[split[0]]; ok {
return fmt.Errorf("Flag \"%s\" already set", split[0])
}

flagParams = append(flagParams, datum)
err = oc.addArrayRequestParams(requestParams)
if err != nil {
return err
}

oc.Parameters.AppendData(flagParams)

if oc.HTTPVerb == http.MethodDelete {
// display account information and confirm whether user wants to proceed
var mode = "Test"
Expand Down Expand Up @@ -127,12 +92,12 @@ func (oc *OperationCmd) runOperationCmd(cmd *cobra.Command, args []string) error
}

// if confirmation is provided, make the request
_, err = oc.MakeRequest(cmd.Context(), apiKey, path, &oc.Parameters, false, nil)
_, err = oc.MakeRequest(cmd.Context(), apiKey, path, &oc.Parameters, requestParams, false, nil)

return err
}
// else
_, err = oc.MakeRequest(cmd.Context(), apiKey, path, &oc.Parameters, false, nil)
_, err = oc.MakeRequest(cmd.Context(), apiKey, path, &oc.Parameters, requestParams, false, nil)
return err
}

Expand Down Expand Up @@ -161,7 +126,8 @@ func NewUnsupportedV2BillingOperationCmd(parentCmd *cobra.Command, name string,
}

// NewOperationCmd returns a new OperationCmd.
func NewOperationCmd(parentCmd *cobra.Command, name, path, httpVerb string, propFlags map[string]string, cfg *config.Config) *OperationCmd {
func NewOperationCmd(parentCmd *cobra.Command, name, path, httpVerb string,
propFlags map[string]string, cfg *config.Config) *OperationCmd {
urlParams := extractURLParams(path)
httpVerb = strings.ToUpper(httpVerb)
operationCmd := &OperationCmd{
Expand All @@ -174,8 +140,10 @@ func NewOperationCmd(parentCmd *cobra.Command, name, path, httpVerb string, prop
Path: path,
URLParams: urlParams,

stringFlags: make(map[string]*string),
arrayFlags: make(map[string]*[]string),
arrayFlags: make(map[string]*[]string),
stringFlags: make(map[string]*string),
integerFlags: make(map[string]*int),
boolFlags: make(map[string]*bool),
}
cmd := &cobra.Command{
Use: name,
Expand All @@ -188,10 +156,17 @@ func NewOperationCmd(parentCmd *cobra.Command, name, path, httpVerb string, prop
// it's ok to treat all flags as string flags because we don't send any default flag values to the API
// i.e. "account_balance" default is "" not 0 but this is ok
flagName := strings.ReplaceAll(prop, "_", "-")
if propType == "array" {

switch propType {
case "array":
operationCmd.arrayFlags[flagName] = cmd.Flags().StringArray(flagName, []string{}, "")
} else {
case "string":
operationCmd.stringFlags[flagName] = cmd.Flags().String(flagName, "", "")
case "integer":
operationCmd.integerFlags[flagName] = cmd.Flags().Int(flagName, -1, "")
case "boolean":
operationCmd.boolFlags[flagName] = cmd.Flags().Bool(flagName, false, "")
default:
}
cmd.Flags().SetAnnotation(flagName, "request", []string{"true"})
}
Expand Down Expand Up @@ -299,3 +274,125 @@ func constructParamFromDot(dotParam string) string {

return param
}

func (oc *OperationCmd) addStringRequestParams(requestParams map[string]interface{}) {
for stringProp, stringVal := range oc.stringFlags {
// only include fields explicitly set by the user to avoid conflicts between e.g. account_balance, balance
if oc.Cmd.Flags().Changed(stringProp) {
paramName := getParamName(stringProp)
if strings.Contains(paramName, ".") {
constructedNestedStringParams(requestParams, strings.Split(paramName, "."), stringVal)
} else {
requestParams[paramName] = *stringVal
}
}
}
}

func (oc *OperationCmd) addIntRequestParams(requestParams map[string]interface{}) {
for intProp, intVal := range oc.integerFlags {
if oc.Cmd.Flags().Changed(intProp) {
paramName := getParamName(intProp)
if strings.Contains(paramName, ".") {
constructedNestedIntParams(requestParams, strings.Split(paramName, "."), intVal)
} else {
requestParams[paramName] = *intVal
}
}
}
}

func (oc *OperationCmd) addBoolRequestParams(requestParams map[string]interface{}) {
for boolProp, boolVal := range oc.boolFlags {
if oc.Cmd.Flags().Changed(boolProp) {
paramName := getParamName(boolProp)
if strings.Contains(paramName, ".") {
constructedNestedBoolParams(requestParams, strings.Split(paramName, "."), boolVal)
} else {
requestParams[paramName] = *boolVal
}
}
}
}

func (oc *OperationCmd) addArrayRequestParams(requestParams map[string]interface{}) error {
for arrayProp, arrayVal := range oc.arrayFlags {
// only include fields explicitly set by the user to avoid conflicts between e.g. account_balance, balance
if oc.Cmd.Flags().Changed(arrayProp) {
paramName := getParamName(arrayProp)
for _, arrayItem := range *arrayVal {
if _, ok := requestParams[paramName]; !ok {
requestParams[paramName] = make([]interface{}, 0)
}
switch v := reflect.ValueOf(requestParams[paramName]); v.Kind() {
case reflect.Array, reflect.Slice:
requestParams[paramName] = append(requestParams[paramName].([]interface{}), arrayItem)
default:
return fmt.Errorf("array parameter flag %s has conflict with another non-array parameter flag", paramName)
}
}
}
}
return nil
}

func constructedNestedStringParams(params map[string]interface{}, paramKeys []string, stringVal *string) {
if len(paramKeys) == 0 {
return
}

field := paramKeys[0]

if len(paramKeys) == 1 {
params[field] = *stringVal
return
}

if _, ok := params[field]; !ok {
params[field] = make(map[string]interface{}, 0)
}

constructedNestedStringParams(params[field].(map[string]interface{}), paramKeys[1:], stringVal)
}

func constructedNestedIntParams(params map[string]interface{}, paramKeys []string, intVal *int) {
if len(paramKeys) == 0 {
return
}

field := paramKeys[0]

if len(paramKeys) == 1 {
params[field] = *intVal
return
}

if _, ok := params[field]; !ok {
params[field] = make(map[string]interface{}, 0)
}

constructedNestedIntParams(params[field].(map[string]interface{}), paramKeys[1:], intVal)
}

func constructedNestedBoolParams(params map[string]interface{}, paramKeys []string, boolVal *bool) {
if len(paramKeys) == 0 {
return
}

field := paramKeys[0]

if len(paramKeys) == 1 {
params[field] = *boolVal
return
}

if _, ok := params[field]; !ok {
params[field] = make(map[string]interface{}, 0)
}

constructedNestedBoolParams(params[field].(map[string]interface{}), paramKeys[1:], boolVal)
}

func getParamName(prop string) string {
return strings.ReplaceAll(prop, "-", "_")
}
72 changes: 72 additions & 0 deletions pkg/cmd/resources_cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import (
)

func addAllResourcesCmds(rootCmd *cobra.Command) {
v1root := rootCmd
addV1ResourcesCmds(v1root)
v2root := resource.NewNamespaceCmd(rootCmd, "v2").Cmd
addV2ResourcesCmds(v2root)
}

func addV1ResourcesCmds(rootCmd *cobra.Command) {
// Namespace commands
_ = resource.NewNamespaceCmd(rootCmd, "")
nsAppsCmd := resource.NewNamespaceCmd(rootCmd, "apps")
Expand Down Expand Up @@ -5147,3 +5154,68 @@ func addAllResourcesCmds(rootCmd *cobra.Command) {
}, &Config)
resource.NewOperationCmd(rTreasuryTransactionsCmd.Cmd, "retrieve", "/v1/treasury/transactions/{id}", http.MethodGet, map[string]string{}, &Config)
}

func addV2ResourcesCmds(rootCmd *cobra.Command) {
// Namespace commands
_ = resource.NewNamespaceCmd(rootCmd, "")
nsBillingCmd := resource.NewNamespaceCmd(rootCmd, "billing")
nsCoreCmd := resource.NewNamespaceCmd(rootCmd, "core")

// Resource commands
rBillingMeterEventAdjustmentsCmd := resource.NewResourceCmd(nsBillingCmd.Cmd, "meter_event_adjustments")
rBillingMeterEventSessionsCmd := resource.NewResourceCmd(nsBillingCmd.Cmd, "meter_event_sessions")
rBillingMeterEventsCmd := resource.NewResourceCmd(nsBillingCmd.Cmd, "meter_events")
rCoreEventDestinationsCmd := resource.NewResourceCmd(nsCoreCmd.Cmd, "event_destinations")
rCoreEventsCmd := resource.NewResourceCmd(nsCoreCmd.Cmd, "events")

// Operation commands
resource.NewOperationCmd(rBillingMeterEventAdjustmentsCmd.Cmd, "create", "/v2/billing/meter_event_adjustments", http.MethodPost, map[string]string{
"cancel.identifier": "string",
"event_name": "string",
"type": "string",
}, &Config)
resource.NewOperationCmd(rBillingMeterEventSessionsCmd.Cmd, "create", "/v2/billing/meter_event_session", http.MethodPost, map[string]string{}, &Config)
resource.NewOperationCmd(rBillingMeterEventsCmd.Cmd, "create", "/v2/billing/meter_events", http.MethodPost, map[string]string{
"event_name": "string",
"identifier": "string",
"timestamp": "string",
}, &Config)
resource.NewOperationCmd(rCoreEventDestinationsCmd.Cmd, "create", "/v2/core/event_destinations", http.MethodPost, map[string]string{
"amazon_eventbridge.aws_account_id": "string",
"amazon_eventbridge.aws_region": "string",
"description": "string",
"enabled_events": "array",
"event_payload": "string",
"events_from": "array",
"include": "array",
"name": "string",
"snapshot_api_version": "string",
"type": "string",
"webhook_endpoint.url": "string",
}, &Config)
resource.NewOperationCmd(rCoreEventDestinationsCmd.Cmd, "delete", "/v2/core/event_destinations/{id}", http.MethodDelete, map[string]string{}, &Config)
resource.NewOperationCmd(rCoreEventDestinationsCmd.Cmd, "disable", "/v2/core/event_destinations/{id}/disable", http.MethodPost, map[string]string{}, &Config)
resource.NewOperationCmd(rCoreEventDestinationsCmd.Cmd, "enable", "/v2/core/event_destinations/{id}/enable", http.MethodPost, map[string]string{}, &Config)
resource.NewOperationCmd(rCoreEventDestinationsCmd.Cmd, "list", "/v2/core/event_destinations", http.MethodGet, map[string]string{
"include": "array",
"limit": "integer",
"page": "string",
}, &Config)
resource.NewOperationCmd(rCoreEventDestinationsCmd.Cmd, "retrieve", "/v2/core/event_destinations/{id}", http.MethodGet, map[string]string{
"include": "array",
}, &Config)
resource.NewOperationCmd(rCoreEventDestinationsCmd.Cmd, "update", "/v2/core/event_destinations/{id}", http.MethodPost, map[string]string{
"description": "string",
"enabled_events": "array",
"include": "array",
"name": "string",
"webhook_endpoint.url": "string",
}, &Config)
resource.NewOperationCmd(rCoreEventsCmd.Cmd, "list", "/v2/core/events", http.MethodGet, map[string]string{
"limit": "integer",
"object_id": "string",
"page": "string",
}, &Config)
resource.NewOperationCmd(rCoreEventsCmd.Cmd, "ping", "/v2/core/event_destinations/{id}/ping", http.MethodPost, map[string]string{}, &Config)
resource.NewOperationCmd(rCoreEventsCmd.Cmd, "retrieve", "/v2/core/events/{id}", http.MethodGet, map[string]string{}, &Config)
}
Loading

0 comments on commit 8e75730

Please sign in to comment.