Skip to content
Open
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
10 changes: 8 additions & 2 deletions pkg/cmd/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type triggerCmd struct {
apiVersion string
skip []string
override []string
param []string
add []string
remove []string
raw string
Expand All @@ -46,7 +47,11 @@ needed to create the triggered event as well as the corresponding API objects.
ansi.Bold("Supported events:"),
fixtures.EventList(),
),
Example: `stripe trigger payment_intent.created`,
Example: `# Trigger a basic event
stripe trigger payment_intent.created

# Trigger an event that requires parameters
stripe trigger application_fee.created --param charge:transfer_data.destination=acct_123`,
Annotations: map[string]string{
AIAgentHelpAnnotationKey: " Use `--override` to customize event data, e.g. `--override customer:[email protected]`.\n" +
" Use `--skip` to skip specific steps in the trigger sequence.\n" +
Expand All @@ -58,6 +63,7 @@ needed to create the triggered event as well as the corresponding API objects.
tc.cmd.Flags().StringVar(&tc.stripeAccount, "stripe-account", "", "Set a header identifying the connected account")
tc.cmd.Flags().StringArrayVar(&tc.skip, "skip", []string{}, "Skip specific steps in the trigger")
tc.cmd.Flags().StringArrayVar(&tc.override, "override", []string{}, "Override params in the trigger")
tc.cmd.Flags().StringArrayVar(&tc.param, "param", []string{}, "Set required parameters (validated before execution)")
tc.cmd.Flags().StringArrayVar(&tc.add, "add", []string{}, "Add params to the trigger")
tc.cmd.Flags().StringArrayVar(&tc.remove, "remove", []string{}, "Remove params from the trigger")
tc.cmd.Flags().StringVar(&tc.raw, "raw", "", "Raw fixture in string format to replace all default fixtures")
Expand Down Expand Up @@ -91,7 +97,7 @@ func (tc *triggerCmd) runTriggerCmd(cmd *cobra.Command, args []string) error {

event := args[0]

_, err = fixtures.Trigger(cmd.Context(), event, tc.stripeAccount, tc.apiBaseURL, apiKey, tc.skip, tc.override, tc.add, tc.remove, tc.raw, tc.apiVersion, tc.edit)
_, err = fixtures.Trigger(cmd.Context(), event, tc.stripeAccount, tc.apiBaseURL, apiKey, tc.skip, tc.override, tc.param, tc.add, tc.remove, tc.raw, tc.apiVersion, tc.edit)
if err != nil {
return err
}
Expand Down
14 changes: 11 additions & 3 deletions pkg/fixtures/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@ import (
// SupportedVersions is the version number of the fixture template the CLI supports
const SupportedVersions = 0

// RequiredParam describes a parameter that must be provided via --param flag
type RequiredParam struct {
Name string `json:"name"` // Fixture path (e.g., "charge:transfer_data.destination")
Description string `json:"description"` // Human-readable description
Placeholder string `json:"placeholder"` // Placeholder value for error messages
}

// MetaFixture contains fixture metadata
type MetaFixture struct {
Version int `json:"template_version"`
ExcludeMetadata bool `json:"exclude_metadata"`
Aliases []string `json:"aliases,omitempty"`
Version int `json:"template_version"`
ExcludeMetadata bool `json:"exclude_metadata"`
Aliases []string `json:"aliases,omitempty"`
RequiredParams []RequiredParam `json:"required_params,omitempty"`
}

// FixtureData contains the whole fixture file
Expand Down
169 changes: 163 additions & 6 deletions pkg/fixtures/triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"
"sync"

"github.com/spf13/afero"

"github.com/stripe/stripe-cli/pkg/ansi"
"github.com/stripe/stripe-cli/pkg/stripe"
)

Expand Down Expand Up @@ -87,7 +89,7 @@
}

// BuildFromFixtureFile creates a new fixture struct for a file
func BuildFromFixtureFile(fs afero.Fs, apiKey, stripeAccount, apiBaseURL, jsonFile string, skip, override, add, remove []string, edit bool) (*Fixture, error) {
func BuildFromFixtureFile(fs afero.Fs, apiKey, stripeAccount, apiBaseURL, jsonFile string, skip, override, param, add, remove []string, edit bool) (*Fixture, error) {
fixture, err := NewFixtureFromFile(
fs,
apiKey,
Expand All @@ -104,6 +106,21 @@
return nil, err
}

// Validate required params before proceeding
if err := ValidateRequiredParams(fixture, jsonFile, param); err != nil {
return nil, err
}

// Merge params with overrides (params take precedence, same syntax)
mergedOverrides := make([]string, 0, len(override)+len(param))
mergedOverrides = append(mergedOverrides, override...)
mergedOverrides = append(mergedOverrides, param...)
if len(mergedOverrides) > 0 {
if err := fixture.Override(mergedOverrides); err != nil {
return nil, err
}
}

return fixture, nil
}

Expand All @@ -117,15 +134,71 @@
}

// EventList prints out a padded list of supported trigger events for printing the help file
// Events that require parameters show the --param syntax inline, vertically aligned
func EventList() string {
eventNames := EventNames()

// First pass: find the maximum event name length ONLY for events that require params
maxLength := 0
for _, event := range eventNames {
if file, ok := getEvents()[event]; ok {
params := getRequiredParamsForEvent(file)
if len(params) > 0 && len(event) > maxLength {
maxLength = len(event)
}
}
}

// Second pass: build the list with proper padding for vertical alignment
var eventList string
for _, event := range EventNames() {
eventList += fmt.Sprintf(" %s\n", event)
for _, event := range eventNames {
// Try to load fixture metadata to check for required params
if file, ok := getEvents()[event]; ok {
params := getRequiredParamsForEvent(file)
if len(params) > 0 {
// Show event with required params syntax, padded for vertical alignment
paramSyntax := ""
for i, param := range params {
if i > 0 {
paramSyntax += " "
}
paramSyntax += fmt.Sprintf("--param %s=<value>", param.Name)
}
// Pad event name to max length for vertical alignment
padding := maxLength - len(event)
eventList += fmt.Sprintf(" %s%s %s\n", event, strings.Repeat(" ", padding), paramSyntax)
} else {
eventList += fmt.Sprintf(" %s\n", event)
}
} else {
eventList += fmt.Sprintf(" %s\n", event)
}
}

return eventList
}

// getRequiredParamsForEvent loads fixture metadata and returns required params if any
func getRequiredParamsForEvent(fixtureFile string) []RequiredParam {
f, err := triggers.Open(fixtureFile)
if err != nil {
return nil
}
defer f.Close()

filedata, err := io.ReadAll(f)
if err != nil {
return nil
}

var fixtureData FixtureData
if err := json.Unmarshal(filedata, &fixtureData); err != nil {
return nil
}

return fixtureData.Meta.RequiredParams
}

// EventNames returns an array of all the event names
func EventNames() []string {
names := []string{}
Expand All @@ -139,7 +212,7 @@
}

// Trigger triggers a Stripe event.
func Trigger(ctx context.Context, event string, stripeAccount string, baseURL string, apiKey string, skip, override, add, remove []string, raw string, apiVersion string, edit bool) ([]string, error) {
func Trigger(ctx context.Context, event string, stripeAccount string, baseURL string, apiKey string, skip, override, param, add, remove []string, raw string, apiVersion string, edit bool) ([]string, error) {
var fixture *Fixture
var err error
fs := afero.NewOsFs()
Expand All @@ -152,7 +225,7 @@

if len(raw) == 0 {
if file, ok := getEvents()[event]; ok {
fixture, err = BuildFromFixtureFile(fs, apiKey, stripeAccount, baseURL, file, skip, override, add, remove, edit)
fixture, err = BuildFromFixtureFile(fs, apiKey, stripeAccount, baseURL, file, skip, override, param, add, remove, edit)
if err != nil {
return nil, err
}
Expand All @@ -162,7 +235,7 @@
return nil, fmt.Errorf("%s", fmt.Sprintf("The event `%s` is not supported by Stripe CLI. To trigger unsupported events, use the Stripe API or Dashboard to perform actions that lead to the event you want to trigger (for example, create a Customer to generate a `customer.created` event). You can also create a custom fixture: https://docs.stripe.com/cli/fixtures", event))
}

fixture, err = BuildFromFixtureFile(fs, apiKey, stripeAccount, baseURL, event, skip, override, add, remove, edit)
fixture, err = BuildFromFixtureFile(fs, apiKey, stripeAccount, baseURL, event, skip, override, param, add, remove, edit)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -190,3 +263,87 @@

return reversed
}

// ValidateRequiredParams checks if all required parameters specified in fixture metadata
// have been provided via the --param flag. Returns an actionable error if any are missing.
func ValidateRequiredParams(fixture *Fixture, jsonFile string, providedParams []string) error {
requiredParams := fixture.FixtureData.Meta.RequiredParams

// Look up event name from file path for error messages
eventName := reverseMap()[jsonFile]
if eventName == "" {
eventName = "<event>"
}

// If params were provided but none are required, show helpful error suggesting --override
if len(requiredParams) == 0 && len(providedParams) > 0 {
color := ansi.Color(os.Stdout)
return fmt.Errorf("%s\n\nThis trigger does not accept required parameters.\n\nIf you're trying to customize fixture values, use --override instead:\n stripe trigger <event> --override %s",
color.Red("✘ Unexpected parameters").String(),
providedParams[0]) // Show first param as example
}

if len(requiredParams) == 0 {
return nil // No required params, nothing to validate
}

// Parse provided params by fixture path
// Format: "fixtureName:path.to.field=value" (same as --override)
providedParamNames := make(map[string]bool)
for _, param := range providedParams {
parts := strings.SplitN(param, "=", 2)
if len(parts) != 2 {
color := ansi.Color(os.Stdout)
return fmt.Errorf("%s\n\nInvalid parameter format: %s\n\nParameters must use the format: fixtureName:path.to.field=value\n\nExample:\n --param charge:transfer_data.destination=acct_123",
color.Red("✘ Malformed parameter").String(),
param)
}
if parts[0] == "" {
color := ansi.Color(os.Stdout)
return fmt.Errorf("%s\n\nParameter name cannot be empty: %s\n\nParameters must use the format: fixtureName:path.to.field=value",
color.Red("✘ Malformed parameter").String(),
param)
}
if parts[1] == "" {
color := ansi.Color(os.Stdout)
return fmt.Errorf("%s\n\nParameter value cannot be empty: %s\n\nIf you want to set an empty value, use --override instead:\n --override %s=",
color.Red("✘ Malformed parameter").String(),
param,
parts[0])
}
providedParamNames[parts[0]] = true
}

// Check if all required params were provided
var missingParams []RequiredParam
for _, required := range requiredParams {
if !providedParamNames[required.Name] {
missingParams = append(missingParams, required)
}
}

if len(missingParams) > 0 {
// Build actionable error message
color := ansi.Color(os.Stdout)
var errorMsg strings.Builder

errorMsg.WriteString(color.Red("✘ Missing required parameters").String())
errorMsg.WriteString("\n")

for _, param := range missingParams {
errorMsg.WriteString(fmt.Sprintf("\n %s - %s\n", color.Bold(param.Name).String(), param.Description))

Check failure on line 334 in pkg/fixtures/triggers.go

View workflow job for this annotation

GitHub Actions / test (1.26.0, ubuntu-latest)

QF1012: Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)) (staticcheck)

Check failure on line 334 in pkg/fixtures/triggers.go

View workflow job for this annotation

GitHub Actions / test (1.26.0, ubuntu-latest)

QF1012: Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)) (staticcheck)
errorMsg.WriteString(" Example:\n\n")
errorMsg.WriteString(fmt.Sprintf(" stripe trigger %s \\\n", eventName))

Check failure on line 336 in pkg/fixtures/triggers.go

View workflow job for this annotation

GitHub Actions / test (1.26.0, ubuntu-latest)

QF1012: Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)) (staticcheck)

Check failure on line 336 in pkg/fixtures/triggers.go

View workflow job for this annotation

GitHub Actions / test (1.26.0, ubuntu-latest)

QF1012: Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)) (staticcheck)

placeholderValue := param.Placeholder
if placeholderValue == "" {
placeholderValue = "VALUE"
}
errorMsg.WriteString(fmt.Sprintf(" --param %s=%s\n", param.Name, placeholderValue))

Check failure on line 342 in pkg/fixtures/triggers.go

View workflow job for this annotation

GitHub Actions / test (1.26.0, ubuntu-latest)

QF1012: Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)) (staticcheck)

Check failure on line 342 in pkg/fixtures/triggers.go

View workflow job for this annotation

GitHub Actions / test (1.26.0, ubuntu-latest)

QF1012: Use fmt.Fprintf(...) instead of WriteString(fmt.Sprintf(...)) (staticcheck)
}

return fmt.Errorf("%s", errorMsg.String())
}

return nil
}
29 changes: 29 additions & 0 deletions pkg/fixtures/triggers/application_fee.created.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"_meta": {
"template_version": 0,
"required_params": [
{
"name": "charge:transfer_data.destination",
"description": "Connect account ID with transfers capability enabled",
"placeholder": "acct_1ABC234DEF567GHI"
}
]
},
"fixtures": [
{
"name": "charge",
"path": "/v1/charges",
"method": "post",
"params": {
"amount": 1000,
"currency": "usd",
"source": "tok_visa",
"description": "(created by Stripe CLI)",
"transfer_data": {
"destination": "{{CONNECT_ACCOUNT_ID}}"
},
"application_fee_amount": 100
}
}
]
}
43 changes: 43 additions & 0 deletions pkg/fixtures/triggers/application_fee.refund.updated.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"_meta": {
"template_version": 0,
"required_params": [
{
"name": "charge:transfer_data.destination",
"description": "Connect account ID with transfers capability enabled",
"placeholder": "acct_1ABC234DEF567GHI"
}
]
},
"fixtures": [
{
"name": "charge",
"path": "/v1/charges",
"method": "post",
"params": {
"amount": 1000,
"currency": "usd",
"source": "tok_visa",
"description": "(created by Stripe CLI)",
"transfer_data": {
"destination": "{{CONNECT_ACCOUNT_ID}}"
},
"application_fee_amount": 100
}
},
{
"name": "fee_refund",
"path": "/v1/application_fees/${charge:application_fee}/refunds",
"method": "post",
"params": {}
},
{
"name": "updated_fee_refund",
"path": "/v1/application_fees/${charge:application_fee}/refunds/${fee_refund:id}",
"method": "post",
"params": {
"metadata": { "order_id": "6735" }
}
}
]
}
Loading
Loading