diff --git a/.changeset/swift-otters-wander.md b/.changeset/swift-otters-wander.md new file mode 100644 index 0000000000..96e58d1f27 --- /dev/null +++ b/.changeset/swift-otters-wander.md @@ -0,0 +1,6 @@ +--- +'@redocly/openapi-core': minor +'@redocly/cli': minor +--- + +Added the `spec-parameters-in-by-context` Arazzo rule, which validates that a parameter's `in` field is specified when the parent workflow, step, success action, or failure action does not reference a `workflowId`. diff --git a/.changeset/thin-aliens-accept.md b/.changeset/thin-aliens-accept.md new file mode 100644 index 0000000000..2bf346aee3 --- /dev/null +++ b/.changeset/thin-aliens-accept.md @@ -0,0 +1,7 @@ +--- +'@redocly/openapi-core': minor +'@redocly/respect-core': minor +'@redocly/cli': minor +--- + +Extended success and failure action objects to accept a `parameters` property that maps to workflow inputs. diff --git a/docs/@v2/rules/arazzo/spec-parameters-in-by-context.md b/docs/@v2/rules/arazzo/spec-parameters-in-by-context.md new file mode 100644 index 0000000000..136fee1622 --- /dev/null +++ b/docs/@v2/rules/arazzo/spec-parameters-in-by-context.md @@ -0,0 +1,122 @@ +# spec-parameters-in-by-context + +Validates how the `in` field is used on parameters based on the parent context. + +| Arazzo | Compatibility | +| ------ | ------------- | +| 1.x | ✅ | + +## Design principles + +The `in` field on an Arazzo parameter is not a required property — omitting it carries semantics. +When a step references a `workflowId`, a parameter with no `in` field is mapped to the referenced workflow's inputs. +When `in` is specified, the parameter is sent at that request location (`header`, `query`, `path`, or `cookie`) against the targeted operation. + +This rule enforces the following: + +- For a step that does not reference a `workflowId` (for example, one using `operationId`, `operationPath`, or `x-operation`), and for parameters defined at the workflow level, `in` must be specified on each inline parameter. +- Parameters on success and failure actions are only valid when the action references a `workflowId` — these parameters map to the referenced workflow's inputs and the spec states that `in` MUST NOT be used on them (see the [Success Action Object](https://spec.openapis.org/arazzo/latest.html#success-action-object) and [Failure Action Object](https://spec.openapis.org/arazzo/latest.html#failure-action-object)). + +## Configuration + +| Option | Type | Description | +| -------- | ------ | ------------------------------------------------------- | +| severity | string | Possible values: `off`, `warn`, `error`. Default `off`. | + +An example configuration: + +```yaml +rules: + spec-parameters-in-by-context: error +``` + +## Examples + +Given the following configuration: + +```yaml +rules: + spec-parameters-in-by-context: error +``` + +Example of a **correct** step referencing an `operationId` (each parameter declares `in`): + +```yaml +# Correct example - operationId +workflows: + - workflowId: get-museum-hours + steps: + - stepId: list-hours + operationId: listMuseumHours + parameters: + - in: query + name: startDate + value: '2024-01-01' +``` + +Example of a **correct** step referencing a `workflowId` (parameters omit `in` and are mapped to the referenced workflow's inputs): + +```yaml +# Correct example - workflowId +workflows: + - workflowId: buy-tickets + steps: + - stepId: reuse-hours-workflow + workflowId: get-museum-hours + parameters: + - name: startDate + value: '2024-01-01' +``` + +Example of a **correct** success action transferring to another workflow with mapped parameters: + +```yaml +# Correct example - success action +workflows: + - workflowId: buy-tickets + steps: + - stepId: purchase + operationId: createTicket + onSuccess: + - name: continue-to-hours + type: goto + workflowId: get-museum-hours + parameters: + - name: startDate + value: '2024-01-01' +``` + +Example of an **incorrect** step referencing an `operationId` while omitting `in`: + +```yaml +# Incorrect example - operationId without `in` +workflows: + - workflowId: get-museum-hours + steps: + - stepId: list-hours + operationId: listMuseumHours + parameters: + - name: startDate + value: '2024-01-01' +``` + +Example of an **incorrect** success action defining `parameters` without referencing a `workflowId`: + +```yaml +# Incorrect example - action without workflowId +workflows: + - workflowId: buy-tickets + steps: + - stepId: purchase + operationId: createTicket + onSuccess: + - name: end-with-params + type: end + parameters: + - name: startDate + value: '2024-01-01' +``` + +## Resources + +- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/arazzo/spec-parameters-in-by-context.ts) diff --git a/docs/@v2/rules/built-in-rules.md b/docs/@v2/rules/built-in-rules.md index 5e52a7a058..a16a9166d2 100644 --- a/docs/@v2/rules/built-in-rules.md +++ b/docs/@v2/rules/built-in-rules.md @@ -136,6 +136,7 @@ Within the Arazzo family of rules, there are rules for the main Arazzo specifica - [requestBody-replacements-unique](./arazzo/requestBody-replacements-unique.md): the `replacements` of the `requestBody` object must be unique - [sourceDescription-name-unique](./arazzo/sourceDescription-name-unique.md): the `name` property of the `sourceDescription` object must be unique across all source descriptions - [sourceDescription-type](./arazzo/sourceDescription-type.md): the `type` property of the `sourceDescription` object must be either `openapi` or `arazzo` +- [spec-parameters-in-by-context](./arazzo/spec-parameters-in-by-context.md): the parameter `in` field must be specified or omitted based on whether the parent step or action references a `workflowId` - [spec-step-mutually-exclusive-fields](./arazzo/spec-step-mutually-exclusive-fields.md): a step must use only one of its mutually exclusive operation fields - [stepId-unique](./arazzo/stepId-unique.md): the `stepId` must be unique amongst all steps described in the workflow - [step-onFailure-unique](./arazzo/step-onFailure-unique.md): the `onFailure` actions of the `step` object must be unique diff --git a/docs/@v2/v2.sidebars.yaml b/docs/@v2/v2.sidebars.yaml index 954aad8145..ac1934edc9 100644 --- a/docs/@v2/v2.sidebars.yaml +++ b/docs/@v2/v2.sidebars.yaml @@ -175,6 +175,7 @@ - page: rules/arazzo/requestBody-replacements-unique.md - page: rules/arazzo/sourceDescription-name-unique.md - page: rules/arazzo/sourceDescription-type.md + - page: rules/arazzo/spec-parameters-in-by-context.md - page: rules/arazzo/spec-step-mutually-exclusive-fields.md - page: rules/arazzo/stepId-unique.md - page: rules/arazzo/step-onFailure-unique.md diff --git a/packages/core/src/__tests__/__snapshots__/redocly-yaml.test.ts.snap b/packages/core/src/__tests__/__snapshots__/redocly-yaml.test.ts.snap index 1741e529b7..0ad1248f2d 100644 --- a/packages/core/src/__tests__/__snapshots__/redocly-yaml.test.ts.snap +++ b/packages/core/src/__tests__/__snapshots__/redocly-yaml.test.ts.snap @@ -1396,6 +1396,8 @@ exports[`createConfigTypes > matches snapshot for the default config schema 1`] "OpenAPISourceDescription", "ArazzoSourceDescription", "Parameters", + "ActionParameters", + "ActionParameter", "ReusableObject", "Workflows", "Workflow", diff --git a/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap b/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap index 7606619204..fbee6e1a36 100644 --- a/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +++ b/packages/core/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap @@ -19,6 +19,7 @@ exports[`resolveConfig > should ignore minimal from the root and read local file "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "warn", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", @@ -45,6 +46,7 @@ exports[`resolveConfig > should ignore minimal from the root and read local file "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", @@ -442,6 +444,7 @@ exports[`resolveConfig > should resolve extends with local file config which con "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "warn", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", @@ -468,6 +471,7 @@ exports[`resolveConfig > should resolve extends with local file config which con "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", diff --git a/packages/core/src/config/__tests__/load.test.ts b/packages/core/src/config/__tests__/load.test.ts index 20a957169c..93637e41a6 100644 --- a/packages/core/src/config/__tests__/load.test.ts +++ b/packages/core/src/config/__tests__/load.test.ts @@ -142,6 +142,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "off", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", @@ -168,6 +169,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", @@ -502,6 +504,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "warn", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", @@ -528,6 +531,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", @@ -867,6 +871,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "off", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", @@ -893,6 +898,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", @@ -1316,6 +1322,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "off", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", @@ -1342,6 +1349,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", @@ -1676,6 +1684,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "warn", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", @@ -1702,6 +1711,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "error", "sourceDescription-type": "error", "sourceDescriptions-not-empty": "error", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "error", "step-onFailure-unique": "warn", "step-onSuccess-unique": "warn", @@ -2041,6 +2051,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "off", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", @@ -2067,6 +2078,7 @@ describe('loadConfig', () => { "sourceDescription-name-unique": "off", "sourceDescription-type": "off", "sourceDescriptions-not-empty": "off", + "spec-parameters-in-by-context": "error", "spec-step-mutually-exclusive-fields": "warn", "step-onFailure-unique": "off", "step-onSuccess-unique": "off", diff --git a/packages/core/src/config/all.ts b/packages/core/src/config/all.ts index 49e54dd65d..fc9f9c6929 100644 --- a/packages/core/src/config/all.ts +++ b/packages/core/src/config/all.ts @@ -299,6 +299,7 @@ const all: RawGovernanceConfig<'built-in'> = { 'sourceDescription-name-unique': 'error', 'sourceDescription-type': 'error', 'sourceDescriptions-not-empty': 'error', + 'spec-parameters-in-by-context': 'error', 'spec-step-mutually-exclusive-fields': 'error', 'step-onFailure-unique': 'error', 'step-onSuccess-unique': 'error', @@ -312,6 +313,7 @@ const all: RawGovernanceConfig<'built-in'> = { 'criteria-unique': 'error', 'no-criteria-xpath': 'off', 'no-enum-type-mismatch': 'error', + 'spec-parameters-in-by-context': 'error', 'no-mixed-number-range-constraints': 'error', 'no-required-schema-properties-undefined': 'error', 'no-schema-type-mismatch': 'error', diff --git a/packages/core/src/config/minimal.ts b/packages/core/src/config/minimal.ts index 7c2d28e0f8..e0a8211631 100644 --- a/packages/core/src/config/minimal.ts +++ b/packages/core/src/config/minimal.ts @@ -278,6 +278,7 @@ const minimal: RawGovernanceConfig<'built-in'> = { 'sourceDescription-name-unique': 'off', 'sourceDescription-type': 'off', 'sourceDescriptions-not-empty': 'off', + 'spec-parameters-in-by-context': 'off', 'spec-step-mutually-exclusive-fields': 'warn', 'step-onFailure-unique': 'off', 'step-onSuccess-unique': 'off', @@ -290,6 +291,7 @@ const minimal: RawGovernanceConfig<'built-in'> = { arazzo1_1Rules: { 'criteria-unique': 'off', 'no-criteria-xpath': 'off', + 'spec-parameters-in-by-context': 'error', 'no-enum-type-mismatch': 'warn', 'no-mixed-number-range-constraints': 'off', 'no-required-schema-properties-undefined': 'warn', diff --git a/packages/core/src/config/recommended-strict.ts b/packages/core/src/config/recommended-strict.ts index 1e484aeba6..33be25c50b 100644 --- a/packages/core/src/config/recommended-strict.ts +++ b/packages/core/src/config/recommended-strict.ts @@ -278,6 +278,7 @@ const recommendedStrict: RawGovernanceConfig<'built-in'> = { 'sourceDescription-name-unique': 'error', 'sourceDescription-type': 'error', 'sourceDescriptions-not-empty': 'error', + 'spec-parameters-in-by-context': 'error', 'spec-step-mutually-exclusive-fields': 'error', 'step-onFailure-unique': 'error', 'step-onSuccess-unique': 'error', @@ -291,6 +292,7 @@ const recommendedStrict: RawGovernanceConfig<'built-in'> = { 'criteria-unique': 'error', 'no-criteria-xpath': 'off', 'no-enum-type-mismatch': 'error', + 'spec-parameters-in-by-context': 'error', 'no-mixed-number-range-constraints': 'error', 'no-required-schema-properties-undefined': 'error', 'no-schema-type-mismatch': 'error', diff --git a/packages/core/src/config/recommended.ts b/packages/core/src/config/recommended.ts index 9877b5b6a7..b092d2d9fb 100644 --- a/packages/core/src/config/recommended.ts +++ b/packages/core/src/config/recommended.ts @@ -278,6 +278,7 @@ const recommended: RawGovernanceConfig<'built-in'> = { 'sourceDescription-name-unique': 'error', 'sourceDescription-type': 'error', 'sourceDescriptions-not-empty': 'error', + 'spec-parameters-in-by-context': 'warn', 'spec-step-mutually-exclusive-fields': 'error', 'step-onFailure-unique': 'warn', 'step-onSuccess-unique': 'warn', @@ -297,6 +298,7 @@ const recommended: RawGovernanceConfig<'built-in'> = { 'no-x-security-both-scheme-and-scheme-name': 'off', 'no-x-security-scheme-name-without-openapi': 'off', 'outputs-defined': 'warn', + 'spec-parameters-in-by-context': 'error', 'parameters-unique': 'error', 'requestBody-replacements-unique': 'warn', 'sourceDescription-name-unique': 'error', diff --git a/packages/core/src/config/spec.ts b/packages/core/src/config/spec.ts index 3b3e69c404..810c6ca922 100644 --- a/packages/core/src/config/spec.ts +++ b/packages/core/src/config/spec.ts @@ -278,6 +278,7 @@ const spec: RawGovernanceConfig<'built-in'> = { 'sourceDescription-name-unique': 'error', 'sourceDescription-type': 'error', 'sourceDescriptions-not-empty': 'error', + 'spec-parameters-in-by-context': 'error', 'spec-step-mutually-exclusive-fields': 'error', 'step-onFailure-unique': 'error', 'step-onSuccess-unique': 'error', @@ -301,6 +302,7 @@ const spec: RawGovernanceConfig<'built-in'> = { 'requestBody-replacements-unique': 'error', 'sourceDescription-name-unique': 'error', 'sourceDescription-type': 'error', + 'spec-parameters-in-by-context': 'error', 'sourceDescriptions-not-empty': 'error', 'spec-step-mutually-exclusive-fields': 'error', 'step-onFailure-unique': 'error', diff --git a/packages/core/src/rules/arazzo/__tests__/parameters-unique.test.ts b/packages/core/src/rules/arazzo/__tests__/parameters-unique.test.ts index 9722cc776c..c5f342d66f 100644 --- a/packages/core/src/rules/arazzo/__tests__/parameters-unique.test.ts +++ b/packages/core/src/rules/arazzo/__tests__/parameters-unique.test.ts @@ -112,4 +112,87 @@ describe('Arazzo parameters-unique', () => { ] `); }); + + it('should report on duplicated `parameters` defined on success/failure actions', async () => { + const actionDocument = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + description: A cool API + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: outer + steps: + - stepId: step-1 + operationId: museum-api.getMuseumHours + onSuccess: + - name: go-next + type: goto + workflowId: inner + parameters: + - name: token + value: a + - name: token + value: b + onFailure: + - name: recover + type: goto + workflowId: inner + parameters: + - name: retryToken + value: a + - name: retryToken + value: b + - workflowId: inner + steps: + - stepId: noop + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document: actionDocument, + config: await createConfig({ + rules: { 'parameters-unique': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/onSuccess/0/parameters/1", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The parameter \`name\` must be unique amongst listed parameters.", + "ruleId": "parameters-unique", + "severity": "error", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/onFailure/0/parameters/1", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The parameter \`name\` must be unique amongst listed parameters.", + "ruleId": "parameters-unique", + "severity": "error", + "suggest": [], + }, + ] + `); + }); }); diff --git a/packages/core/src/rules/arazzo/__tests__/spec-parameters-in-by-context.test.ts b/packages/core/src/rules/arazzo/__tests__/spec-parameters-in-by-context.test.ts new file mode 100644 index 0000000000..9a360d9839 --- /dev/null +++ b/packages/core/src/rules/arazzo/__tests__/spec-parameters-in-by-context.test.ts @@ -0,0 +1,453 @@ +import { outdent } from 'outdent'; + +import { parseYamlToDocument, replaceSourceWithRef } from '../../../../__tests__/utils.js'; +import { createConfig } from '../../../config/index.js'; +import { lintDocument } from '../../../lint.js'; +import { BaseResolver } from '../../../resolve.js'; + +describe('Arazzo spec-parameters-in-by-context', () => { + it('should not report when step references operationId and parameters specify `in`', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: get-museum-hours + steps: + - stepId: get-museum-hours + operationId: museum-api.getMuseumHours + parameters: + - in: header + name: Secret + value: Basic Og== + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should report when step references operationId but a parameter is missing `in`', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: get-museum-hours + steps: + - stepId: get-museum-hours + operationId: museum-api.getMuseumHours + parameters: + - name: Secret + value: Basic Og== + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/parameters/0", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "Parameter \`in\` field MUST be specified when the parent does not reference a \`workflowId\`.", + "ruleId": "spec-parameters-in-by-context", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should not report when step references workflowId and a parameter declares `in` (spec is silent on steps)', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: outer + steps: + - stepId: call-inner + workflowId: inner + parameters: + - in: header + name: token + value: abc + - workflowId: inner + steps: + - stepId: noop + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should not report when step references workflowId and parameter has no `in`', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: outer + steps: + - stepId: call-inner + workflowId: inner + parameters: + - name: token + value: abc + - workflowId: inner + steps: + - stepId: noop + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should report a struct error when an action parameter specifies `in`', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: outer + steps: + - stepId: step-1 + operationId: museum-api.getMuseumHours + onSuccess: + - name: go-next + type: goto + workflowId: inner + parameters: + - in: header + name: token + value: abc + - workflowId: inner + steps: + - stepId: noop + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { struct: 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "from": undefined, + "location": [ + { + "pointer": "#/workflows/0/steps/0/onSuccess/0/parameters/0/in", + "reportOnKey": true, + "source": "arazzo.yaml", + }, + ], + "message": "Property \`in\` is not expected here.", + "ruleId": "struct", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should not report when onFailure action with workflowId has parameter without `in`', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: outer + steps: + - stepId: step-1 + operationId: museum-api.getMuseumHours + onFailure: + - name: recover + type: goto + workflowId: inner + parameters: + - name: token + value: abc + - workflowId: inner + steps: + - stepId: noop + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should report when onSuccess action without workflowId defines parameters', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: outer + steps: + - stepId: step-1 + operationId: museum-api.getMuseumHours + onSuccess: + - name: go-step + type: goto + stepId: step-2 + parameters: + - in: header + name: token + value: abc + - stepId: step-2 + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/onSuccess/0/parameters", + "reportOnKey": true, + "source": "arazzo.yaml", + }, + ], + "message": "Parameters on success actions are only valid when the action references a \`workflowId\`.", + "ruleId": "spec-parameters-in-by-context", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should ignore reusable parameters (only `reference`)', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + components: + parameters: + shared: + in: header + name: token + value: abc + workflows: + - workflowId: outer + steps: + - stepId: step-1 + operationId: museum-api.getMuseumHours + parameters: + - reference: $components.parameters.shared + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should not report when workflow-level parameters specify `in`', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: get-museum-hours + parameters: + - in: header + name: Secret + value: Basic Og== + steps: + - stepId: get-museum-hours + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should report when workflow-level parameter is missing `in`', async () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.1' + info: + title: Cool API + version: 1.0.0 + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: get-museum-hours + parameters: + - name: Secret + value: Basic Og== + steps: + - stepId: get-museum-hours + operationId: museum-api.getMuseumHours + `, + 'arazzo.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ + rules: { 'spec-parameters-in-by-context': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/workflows/0/parameters/0", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "Parameter \`in\` field MUST be specified when the parent does not reference a \`workflowId\`.", + "ruleId": "spec-parameters-in-by-context", + "severity": "error", + "suggest": [], + }, + ] + `); + }); +}); diff --git a/packages/core/src/rules/arazzo/index.ts b/packages/core/src/rules/arazzo/index.ts index e41cfb6474..2dd0ba9a33 100644 --- a/packages/core/src/rules/arazzo/index.ts +++ b/packages/core/src/rules/arazzo/index.ts @@ -19,6 +19,7 @@ import { ParametersUnique } from './parameters-unique.js'; import { RequestBodyReplacementsUnique } from './requestBody-replacements-unique.js'; import { SourceDescriptionsNameUnique } from './sourceDescriptions-name-unique.js'; import { SourceDescriptionsNotEmpty } from './sourceDescriptions-not-empty.js'; +import { SpecParametersInByContext } from './spec-parameters-in-by-context.js'; import { SpecStepMutuallyExclusiveFields } from './spec-step-mutually-exclusive-fields.js'; import { StepOnFailureUnique } from './step-onFailure-unique.js'; import { StepOnSuccessUnique } from './step-onSuccess-unique.js'; @@ -43,6 +44,7 @@ export const rules: Arazzo1RuleSet<'built-in'> = { 'sourceDescription-name-unique': SourceDescriptionsNameUnique, 'sourceDescription-type': SourceDescriptionType, 'sourceDescriptions-not-empty': SourceDescriptionsNotEmpty, + 'spec-parameters-in-by-context': SpecParametersInByContext, 'spec-step-mutually-exclusive-fields': SpecStepMutuallyExclusiveFields, 'step-onFailure-unique': StepOnFailureUnique, 'step-onSuccess-unique': StepOnSuccessUnique, diff --git a/packages/core/src/rules/arazzo/parameters-unique.ts b/packages/core/src/rules/arazzo/parameters-unique.ts index ca69a82cfb..283e1fae80 100644 --- a/packages/core/src/rules/arazzo/parameters-unique.ts +++ b/packages/core/src/rules/arazzo/parameters-unique.ts @@ -1,30 +1,40 @@ +import type { Parameter } from '../../typings/arazzo.js'; import type { Arazzo1Rule } from '../../visitors.js'; import type { UserContext } from '../../walk.js'; -export const ParametersUnique: Arazzo1Rule = () => { - return { - Parameters: { - enter(parameters, { report, location }: UserContext) { - if (!parameters) return; - const seenParameters = new Set(); +function checkParametersUnique(parameters: Parameter[], { report, location }: UserContext) { + if (!parameters) return; + const seenParameters = new Set(); - for (const parameter of parameters) { - if (seenParameters.has(parameter?.name)) { - report({ - message: 'The parameter `name` must be unique amongst listed parameters.', - location: location.child([parameters.indexOf(parameter)]), - }); - } + for (const parameter of parameters) { + if (seenParameters.has(parameter?.name)) { + report({ + message: 'The parameter `name` must be unique amongst listed parameters.', + location: location.child([parameters.indexOf(parameter)]), + }); + } - if (seenParameters.has(parameter?.reference)) { - report({ - message: 'The parameter `reference` must be unique amongst listed parameters.', - location: location.child([parameters.indexOf(parameter)]), - }); - } + if (seenParameters.has(parameter?.reference)) { + report({ + message: 'The parameter `reference` must be unique amongst listed parameters.', + location: location.child([parameters.indexOf(parameter)]), + }); + } - seenParameters.add(parameter?.name ?? parameter?.reference); - } + seenParameters.add(parameter?.name ?? parameter?.reference); + } +} + +export const ParametersUnique: Arazzo1Rule = () => { + return { + Parameters: { + enter(parameters, ctx: UserContext) { + checkParametersUnique(parameters, ctx); + }, + }, + ActionParameters: { + enter(parameters, ctx: UserContext) { + checkParametersUnique(parameters, ctx); }, }, }; diff --git a/packages/core/src/rules/arazzo/spec-parameters-in-by-context.ts b/packages/core/src/rules/arazzo/spec-parameters-in-by-context.ts new file mode 100644 index 0000000000..b119c2d11c --- /dev/null +++ b/packages/core/src/rules/arazzo/spec-parameters-in-by-context.ts @@ -0,0 +1,70 @@ +import type { Parameter } from '../../typings/arazzo.js'; +import { isPlainObject } from '../../utils/is-plain-object.js'; +import type { Arazzo1Rule } from '../../visitors.js'; +import type { UserContext } from '../../walk.js'; + +function isInlineParameter(parameter: Parameter): boolean { + return isPlainObject(parameter) && !('reference' in parameter); +} + +function checkInRequired(parameters: Parameter[], { report, location }: UserContext) { + if (!Array.isArray(parameters)) return; + + for (let i = 0; i < parameters.length; i++) { + const parameter = parameters[i]; + if (!isInlineParameter(parameter)) continue; + + if (!('in' in parameter)) { + report({ + message: + 'Parameter `in` field MUST be specified when the parent does not reference a `workflowId`.', + location: location.child(['parameters', i]), + }); + } + } +} + +export const SpecParametersInByContext: Arazzo1Rule = () => { + return { + Workflow: { + enter(workflow, ctx: UserContext) { + if (!workflow.parameters) return; + // A workflow never references another workflow, so `in` is always required. + checkInRequired(workflow.parameters, ctx); + }, + }, + Step: { + enter(step, ctx: UserContext) { + if (!step.parameters) return; + if (step.workflowId) return; + checkInRequired(step.parameters, ctx); + }, + }, + SuccessActionObject: { + enter(action, ctx: UserContext) { + if (!action.parameters) return; + + if (!action.workflowId) { + ctx.report({ + message: + 'Parameters on success actions are only valid when the action references a `workflowId`.', + location: ctx.location.child(['parameters']).key(), + }); + } + }, + }, + FailureActionObject: { + enter(action, ctx: UserContext) { + if (!action.parameters) return; + + if (!action.workflowId) { + ctx.report({ + message: + 'Parameters on failure actions are only valid when the action references a `workflowId`.', + location: ctx.location.child(['parameters']).key(), + }); + } + }, + }, + }; +}; diff --git a/packages/core/src/types/arazzo.ts b/packages/core/src/types/arazzo.ts index db00fad02e..7afbe4b333 100755 --- a/packages/core/src/types/arazzo.ts +++ b/packages/core/src/types/arazzo.ts @@ -161,6 +161,29 @@ const Parameters: NodeType = { } }, }; +const ActionParameter: NodeType = { + properties: { + name: { + type: 'string', + description: 'REQUIRED. The name of the parameter. Parameter names are case sensitive.', + }, + value: {}, + }, + required: ['name', 'value'], + extensionsPrefix: 'x-', + description: + 'Describes a single parameter passed to a workflow referenced by a success or failure action. Action parameters map to the referenced workflow inputs, so the `in` field MUST NOT be used.', +}; +const ActionParameters: NodeType = { + properties: {}, + items: (value: any) => { + if (value?.reference) { + return 'ReusableObject'; + } else { + return 'ActionParameter'; + } + }, +}; const Workflow: NodeType = { properties: { workflowId: { @@ -397,6 +420,7 @@ const SuccessActionObject: NodeType = { description: 'The workflowId referencing an existing workflow within the Arazzo Description to transfer to upon success of the step. This field is only relevant when the type field value is "goto". If the referenced workflow is contained within an arazzo type sourceDescription, then the workflowId MUST be specified using a Runtime Expression (e.g., $sourceDescriptions..) to avoid ambiguity or potential clashes. This field is mutually exclusive to stepId.', }, + parameters: 'ActionParameters', criteria: listOf('CriterionObject', { description: 'A list of assertions to determine if this action SHALL be executed. Each assertion is described using a Criterion Object. All criteria assertions MUST be satisfied for the action to be executed.', @@ -450,6 +474,7 @@ const FailureActionObject: NodeType = { description: 'A non-negative integer indicating how many attempts to retry the step MAY be attempted before failing the overall step. If not specified then a single retry SHALL be attempted. This field only applies when the type field value is "retry". The retryLimit MUST be exhausted prior to executing subsequent failure actions.', }, + parameters: 'ActionParameters', criteria: listOf('CriterionObject', { description: 'A list of assertions to determine if this action SHALL be executed. Each assertion is described using a Criterion Object.', @@ -478,6 +503,8 @@ export const Arazzo1Types: Record = { ArazzoSourceDescription, Parameters, Parameter, + ActionParameters, + ActionParameter, ReusableObject, Workflows, Workflow, diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts index 44fc928835..ce46820148 100644 --- a/packages/core/src/types/redocly-yaml.ts +++ b/packages/core/src/types/redocly-yaml.ts @@ -169,6 +169,7 @@ const builtInArazzo1Rules = [ 'workflow-dependsOn', 'outputs-defined', 'parameters-unique', + 'spec-parameters-in-by-context', 'step-onSuccess-unique', 'step-onFailure-unique', 'requestBody-replacements-unique', diff --git a/packages/core/src/typings/arazzo.ts b/packages/core/src/typings/arazzo.ts index 3b7accf5e5..8879b83a45 100644 --- a/packages/core/src/typings/arazzo.ts +++ b/packages/core/src/typings/arazzo.ts @@ -51,6 +51,12 @@ export interface Parameter { reference?: string; } +export interface ActionParameter { + name: string; + value: string | number | boolean; + reference?: string; +} + export type ExtendedSecurity = | { schemeName: string; @@ -179,8 +185,8 @@ export interface OnSuccessObject { type: 'goto' | 'end'; stepId?: string; workflowId?: string; + parameters?: (ActionParameter | Parameter)[]; criteria?: CriterionObject[]; - parameters?: Parameter[]; // added in Arazzo 1.1 } export interface OnFailureObject { @@ -190,8 +196,8 @@ export interface OnFailureObject { stepId?: string; retryAfter?: number; retryLimit?: number; + parameters?: (ActionParameter | Parameter)[]; criteria?: CriterionObject[]; - parameters?: Parameter[]; // added in Arazzo 1.1 } export interface Step { diff --git a/packages/respect-core/src/arazzo-schema.ts b/packages/respect-core/src/arazzo-schema.ts index 331cc1cb26..e2ec1eb0c6 100644 --- a/packages/respect-core/src/arazzo-schema.ts +++ b/packages/respect-core/src/arazzo-schema.ts @@ -150,6 +150,7 @@ export const reusableObject = { required: ['reference'], additionalProperties: false, } as const; + export const parameter = { type: 'object', oneOf: [ @@ -169,10 +170,35 @@ export const parameter = { reusableObject, ], } as const; + const parameters = { type: 'array', items: parameter, } as const; + +export const actionParameter = { + type: 'object', + oneOf: [ + { + type: 'object', + properties: { + name: { type: 'string' }, + value: { + oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], + }, + }, + required: ['name', 'value'], + additionalProperties: false, + }, + reusableObject, + ], +} as const; + +const actionParameters = { + type: 'array', + items: actionParameter, +} as const; + export const infoObject = { type: 'object', properties: { @@ -260,6 +286,7 @@ export const onSuccessObject = { type: { type: 'string', enum: ['goto', 'end'] }, stepId: { type: 'string' }, workflowId: { type: 'string' }, + parameters: actionParameters, criteria: criteriaObjects, }, additionalProperties: false, @@ -280,6 +307,7 @@ export const onFailureObject = { stepId: { type: 'string' }, retryAfter: { type: 'number', minimum: 0 }, retryLimit: { type: 'number', minimum: 0 }, + parameters: actionParameters, criteria: criteriaObjects, }, additionalProperties: false, diff --git a/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts b/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts index 96495b06da..7a4a5729d3 100644 --- a/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts +++ b/packages/respect-core/src/modules/__tests__/flow-runner/run-step.test.ts @@ -1279,6 +1279,108 @@ describe('runStep', () => { expect(runWorkflow).toHaveBeenCalled(); }); + it('should map onSuccess action parameters to the target workflow inputs', async () => { + const stepOne: Step = { + stepId: 'get-bird', + 'x-operation': { + url: 'http://localhost:3000/bird', + method: 'get', + }, + successCriteria: [{ condition: '$statusCode == 200' }], + onSuccess: [ + { + name: 'success-action', + workflowId: 'success-action-workflow', + type: 'goto', + parameters: [{ name: 'birdId', value: 'abc-123' }], + criteria: [{ condition: '$statusCode == 200' }], + }, + ], + checks: [], + response: {} as any, + }; + const workflowId = 'get-bird-workflow'; + + vi.mocked(callAPIAndAnalyzeResults).mockImplementationOnce(async ({ step }: { step: Step }) => { + step.checks = [ + { + name: CHECKS.STATUS_CODE_CHECK, + passed: true, + message: '', + severity: 'error', + }, + ]; + + return { + successCriteriaCheck: true, + schemaCheck: true, + networkCheck: true, + unexpectedErrorCheck: true, + statusCodeCheck: true, + }; + }); + + vi.mocked(checkCriteria).mockImplementation(() => [ + { + name: CHECKS.SUCCESS_CRITERIA_CHECK, + passed: true, + message: 'Checking simple criteria: {"condition":"$statusCode == 200"}', + severity: 'error', + }, + ]); + + const context = { + ...basicCTX, + $workflows: { + 'get-bird-workflow': { steps: {}, inputs: {} }, + 'success-action-workflow': { steps: {}, inputs: {} }, + }, + workflows: [ + { + workflowId: 'get-bird-workflow', + steps: [stepOne], + }, + { + workflowId: 'success-action-workflow', + steps: [ + { + stepId: 'use-bird', + 'x-operation': { + url: 'http://localhost:3000/bird', + method: 'get', + }, + checks: [], + }, + ], + }, + ], + } as unknown as TestContext; + + let capturedInputs: Record | undefined; + vi.mocked(runWorkflow).mockImplementationOnce(async ({ ctx }) => { + capturedInputs = ctx.$workflows['success-action-workflow']?.inputs; + return { + type: 'workflow', + invocationContext: {}, + workflowId, + } as WorkflowExecutionResult; + }); + + vi.mocked(resolveWorkflowContext).mockImplementationOnce(async () => { + return { ...context, executedSteps: [] }; + }); + + await runStep({ + step: stepOne, + ctx: context, + workflowId, + executedStepsCount: { value: 0 }, + }); + + expect(runWorkflow).toHaveBeenCalled(); + expect(capturedInputs).toEqual({ birdId: 'abc-123' }); + }); + it('should fail the step when onSuccess goto action has both StepId and WorkflowId provided', async () => { const stepOne: Step = { stepId: 'get-bird', diff --git a/packages/respect-core/src/modules/flow-runner/run-step.ts b/packages/respect-core/src/modules/flow-runner/run-step.ts index dc0bc197a6..be86526777 100644 --- a/packages/respect-core/src/modules/flow-runner/run-step.ts +++ b/packages/respect-core/src/modules/flow-runner/run-step.ts @@ -10,6 +10,7 @@ import type { RuntimeExpressionContext, ResolvedParameter, ExecutedStepsCount, + Workflow, } from '../../types.js'; import { delay } from '../../utils/delay.js'; import { CHECKS } from '../checks/index.js'; @@ -34,6 +35,35 @@ import { prepareRequest, type RequestData } from './prepare-request.js'; import { runWorkflow, resolveWorkflowContext } from './runner.js'; import { checkCriteria } from './success-criteria/index.js'; +function mapParametersToWorkflowInputs({ + parameters, + ctx, + workflowId, +}: { + parameters: ResolvedParameter[]; + ctx: TestContext; + workflowId: string | undefined; +}): Record { + return parameters.filter(isParameterWithoutIn).reduce( + (acc, parameter: ParameterWithoutIn) => { + const ctxWithInputs = { + ...ctx, + $inputs: { + ...ctx.$inputs, + ...(workflowId ? ctx.$workflows[workflowId]?.inputs : {}), + }, + }; + acc[parameter.name] = getValueFromContext({ + value: parameter.value, + ctx: ctxWithInputs, + logger: ctx.options.logger, + }); + return acc; + }, + {} as Record + ); +} + export async function runStep({ step, ctx, @@ -90,25 +120,11 @@ export async function runStep({ if (resolvedParameters && resolvedParameters.length > 0) { // When the step in context specifies a workflowId, then all parameters without `in` maps to workflow inputs. - const workflowInputParameters = resolvedParameters.filter(isParameterWithoutIn).reduce( - (acc, parameter: ParameterWithoutIn) => { - const ctxWithInputs = { - ...ctx, - $inputs: { - ...ctx.$inputs, - ...(workflowId ? ctx.$workflows[workflowId]?.inputs : {}), - }, - }; - // Ensure parameter is of type ParameterWithoutIn - acc[parameter.name] = getValueFromContext({ - value: parameter.value, - ctx: ctxWithInputs, - logger: ctx.options.logger, - }); - return acc; - }, - {} as Record - ); + const workflowInputParameters = mapParametersToWorkflowInputs({ + parameters: resolvedParameters, + ctx, + workflowId, + }); // Merge the runtime inputs with the inputs passed in the step as parameters for the workflow workflowCtx.$workflows[targetWorkflow.workflowId].inputs = { @@ -350,7 +366,12 @@ export async function runStep({ if (matchesCriteria) { const targetWorkflow = action.workflowId - ? resolveWorkflowReference({ ref: action.workflowId, ctx }) + ? ctx.workflows.find((w) => w.workflowId === action.workflowId) || + (getValueFromContext({ + value: action.workflowId, + ctx, + logger: ctx.options.logger, + }) as Workflow | undefined) : undefined; if (action.workflowId && !targetWorkflow) { @@ -370,6 +391,25 @@ export async function runStep({ ) : { ...ctx, executedSteps: [] }; + const targetWorkflowInputs = + targetWorkflow?.workflowId && targetCtx.$workflows[targetWorkflow.workflowId]; + if (targetWorkflowInputs && action.parameters?.length) { + // The action parameters map to the inputs of the workflow referenced by the action's workflowId. + const resolvedActionParameters = action.parameters.map( + (parameter) => resolveReusableComponentItem(parameter, ctx) as ResolvedParameter + ); + const workflowInputParameters = mapParametersToWorkflowInputs({ + parameters: resolvedActionParameters, + ctx, + workflowId, + }); + + targetWorkflowInputs.inputs = { + ...targetWorkflowInputs.inputs, + ...workflowInputParameters, + }; + } + const targetStep = action.stepId ? action.stepId : undefined; if (type === 'retry') { diff --git a/tests/e2e/lint/arazzo-1-1-valid-description/snapshot.txt b/tests/e2e/lint/arazzo-1-1-valid-description/snapshot.txt index d3b1110675..99385dc7d6 100644 --- a/tests/e2e/lint/arazzo-1-1-valid-description/snapshot.txt +++ b/tests/e2e/lint/arazzo-1-1-valid-description/snapshot.txt @@ -1,8 +1,23 @@ +[1] cafe-workflows.yaml:104:13 at #/workflows/0/steps/2/onFailure/0/parameters + +Parameters on failure actions are only valid when the action references a `workflowId`. + +102 | retryAfter: 2 +103 | retryLimit: 3 +104 | parameters: + | ^^^^^^^^^^ +105 | - name: orderId +106 | value: $steps.create-order.outputs.orderId + +Error was generated by the spec-parameters-in-by-context rule. + + No configurations were provided -- using built in recommended configuration by default. validating cafe-workflows.yaml... cafe-workflows.yaml: validated in ms -Woohoo! Your API description is valid. 🎉 +❌ Validation failed with 1 error. +run `redocly lint --generate-ignore-file` to add all problems to the ignore file.