From 58abf6fed44615665bd25010689b62dd83ef8e10 Mon Sep 17 00:00:00 2001 From: Dmytro Anansky Date: Mon, 9 Sep 2024 15:42:04 +0300 Subject: [PATCH] feat: adding spot arazzo rules (#1713) --- .changeset/green-penguins-press.md | 6 + docs/guides/lint-arazzo.md | 3 + docs/rules/arazzo/criteria-unique.md | 60 +++++++ docs/rules/spot/no-actions-type-end.test.md | 55 ++++++ docs/rules/spot/no-criteria-xpath.md | 54 ++++++ .../config-resolvers.test.ts.snap | 6 + packages/core/src/config/all.ts | 3 + packages/core/src/config/minimal.ts | 3 + .../core/src/config/recommended-strict.ts | 3 + packages/core/src/config/recommended.ts | 3 + .../arazzo/__tests__/criteria-unique.test.ts | 161 ++++++++++++++++++ .../__tests__/no-actions-type-end.test.ts | 122 +++++++++++++ .../__tests__/no-criteria-xpath.test.ts | 127 ++++++++++++++ .../core/src/rules/arazzo/criteria-unique.ts | 63 +++++++ packages/core/src/rules/arazzo/index.ts | 6 + .../src/rules/spot/no-actions-type-end.ts | 27 +++ .../core/src/rules/spot/no-criteria-xpath.ts | 20 +++ packages/core/src/types/arazzo.ts | 2 +- packages/core/src/types/redocly-yaml.ts | 3 + resources/arazzo.yaml | 4 +- 20 files changed, 729 insertions(+), 2 deletions(-) create mode 100644 .changeset/green-penguins-press.md create mode 100644 docs/rules/arazzo/criteria-unique.md create mode 100644 docs/rules/spot/no-actions-type-end.test.md create mode 100644 docs/rules/spot/no-criteria-xpath.md create mode 100644 packages/core/src/rules/arazzo/__tests__/criteria-unique.test.ts create mode 100644 packages/core/src/rules/arazzo/__tests__/no-actions-type-end.test.ts create mode 100644 packages/core/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts create mode 100644 packages/core/src/rules/arazzo/criteria-unique.ts create mode 100644 packages/core/src/rules/spot/no-actions-type-end.ts create mode 100644 packages/core/src/rules/spot/no-criteria-xpath.ts diff --git a/.changeset/green-penguins-press.md b/.changeset/green-penguins-press.md new file mode 100644 index 00000000..beffdd85 --- /dev/null +++ b/.changeset/green-penguins-press.md @@ -0,0 +1,6 @@ +--- +"@redocly/openapi-core": minor +"@redocly/cli": minor +--- + +Added Spot and Arazzo rules: `no-criteria-xpath`, `no-actions-type-end`, `criteria-unique`. diff --git a/docs/guides/lint-arazzo.md b/docs/guides/lint-arazzo.md index e0cbef81..1efa1e22 100644 --- a/docs/guides/lint-arazzo.md +++ b/docs/guides/lint-arazzo.md @@ -84,6 +84,9 @@ The rules available for linting Arazzo are: - `step-onSuccess-unique`: the `onSuccess` actions of the `step` object must be unique. - `step-onFailure-unique`: the `onFailure` actions of the `step` object must be unique. - `requestBody-replacements-unique`: the `replacements` of the `requestBody` object must be unique. +- `no-criteria-xpath`: the `xpath` type criteria is not supported by Spot. +- `no-actions-type-end`: the `end` type action is not supported by Spot. +- `criteria-unique`: the criteria list must not contain duplicated assertions. Add the rules to `redocly.yaml`, but for Arazzo specifications, the rules go in their own configuration section called `arazzoRules`. The following example shows configuration for the minimal ruleset with some additional rules configuration: diff --git a/docs/rules/arazzo/criteria-unique.md b/docs/rules/arazzo/criteria-unique.md new file mode 100644 index 00000000..9cff899c --- /dev/null +++ b/docs/rules/arazzo/criteria-unique.md @@ -0,0 +1,60 @@ +--- +slug: /docs/cli/rules/arazzo/criteria-unique +--- + +# criteria-unique + +The criteria list must not contain duplicated assertions. + +| Arazzo | Compatibility | +| ------ | ------------- | +| 1.0.0 | ✅ | + +## API design principles + +The criteria list must not contain duplicated assertions. + +## Configuration + +| Option | Type | Description | +| -------- | ------ | ------------------------------------------------------- | +| severity | string | Possible values: `off`, `warn`, `error`. Default `off`. | + +An example configuration: + +```yaml +arazzoRules: + criteria-unique: error +``` + +## Examples + +Given the following configuration: + +```yaml +arazzoRules: + criteria-unique: error +``` + +Example of a criteria list: + +```yaml Object example +successCriteria: + - condition: $statusCode == 200 +onSuccess: + - name: 'onSuccessActionName' + type: 'goto' + stepId: 'buy-ticket' + criteria: + - condition: $response.body.open == true +onFailure: + - name: 'onFailureActionName' + type: 'goto' + stepId: 'buy-ticket' + criteria: + - condition: $response.body.open == true +``` + +## Resources + +- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/arazzo/criteria-unique.ts) diff --git a/docs/rules/spot/no-actions-type-end.test.md b/docs/rules/spot/no-actions-type-end.test.md new file mode 100644 index 00000000..3c28bab8 --- /dev/null +++ b/docs/rules/spot/no-actions-type-end.test.md @@ -0,0 +1,55 @@ +--- +slug: /docs/cli/rules/spot/no-actions-type-end +--- + +# no-actions-type-end + +The `end` type action is not supported by Spot. + +| Arazzo | Compatibility | +| ------ | ------------- | +| 1.0.0 | ✅ | + +## API design principles + +This is a `Spot`-specific rule. +The `end` type action is not supported by Spot. + +## Configuration + +| Option | Type | Description | +| -------- | ------ | ------------------------------------------------------- | +| severity | string | Possible values: `off`, `warn`, `error`. Default `off`. | + +An example configuration: + +```yaml +arazzoRules: + no-actions-type-end: error +``` + +## Examples + +Given the following configuration: + +```yaml +arazzoRules: + no-actions-type-end: error +``` + +Example of an action: + +```yaml Object example +onSuccess: + - name: 'onSuccessActionName' + type: 'goto' + stepId: 'buy-ticket' +onFailure: + - name: 'onFailureActionName' + type: 'goto' + stepId: 'buy-ticket' +``` + +## Resources + +- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/spot/no-actions-type-end.ts) diff --git a/docs/rules/spot/no-criteria-xpath.md b/docs/rules/spot/no-criteria-xpath.md new file mode 100644 index 00000000..596d7747 --- /dev/null +++ b/docs/rules/spot/no-criteria-xpath.md @@ -0,0 +1,54 @@ +--- +slug: /docs/cli/rules/spot/no-criteria-xpath +--- + +# no-criteria-xpath + +The `xpath` type criteria is not supported by Spot. + +| Arazzo | Compatibility | +| ------ | ------------- | +| 1.0.0 | ✅ | + +## API design principles + +This is `Spot` specific rule. +The `xpath` type criteria is not supported by Spot. + +## Configuration + +| Option | Type | Description | +| -------- | ------ | ------------------------------------------------------- | +| severity | string | Possible values: `off`, `warn`, `error`. Default `off`. | + +An example configuration: + +```yaml +arazzoRules: + no-criteria-xpath: error +``` + +## Examples + +Given the following configuration: + +```yaml +arazzoRules: + no-criteria-xpath: error +``` + +Example of criteria: + +```yaml Object example +successCriteria: + - condition: $statusCode == 201 + - context: $response.body + condition: $.name == 'Mermaid Treasure Identification and Analysis' + type: + type: jsonpath + version: draft-goessner-dispatch-jsonpath-00 +``` + +## Resources + +- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/spot/no-criteria-xpath.ts) 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 7bc2a92e..d28851a8 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 @@ -5,6 +5,9 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1 "arazzoDecorators": {}, "arazzoPreprocessors": {}, "arazzoRules": { + "criteria-unique": "warn", + "no-actions-type-end": "warn", + "no-criteria-xpath": "warn", "parameters-not-in-body": "warn", "parameters-unique": "error", "requestBody-replacements-unique": "warn", @@ -147,6 +150,9 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w "arazzoDecorators": {}, "arazzoPreprocessors": {}, "arazzoRules": { + "criteria-unique": "warn", + "no-actions-type-end": "warn", + "no-criteria-xpath": "warn", "parameters-not-in-body": "warn", "parameters-unique": "error", "requestBody-replacements-unique": "warn", diff --git a/packages/core/src/config/all.ts b/packages/core/src/config/all.ts index 61bec163..a6dd21f8 100644 --- a/packages/core/src/config/all.ts +++ b/packages/core/src/config/all.ts @@ -139,6 +139,9 @@ const all: PluginStyleguideConfig<'built-in'> = { 'step-onSuccess-unique': 'error', 'step-onFailure-unique': 'error', 'requestBody-replacements-unique': 'error', + 'no-criteria-xpath': 'error', + 'no-actions-type-end': 'error', + 'criteria-unique': 'error', }, }; diff --git a/packages/core/src/config/minimal.ts b/packages/core/src/config/minimal.ts index 2a15423c..f1ae082e 100644 --- a/packages/core/src/config/minimal.ts +++ b/packages/core/src/config/minimal.ts @@ -121,6 +121,9 @@ const minimal: PluginStyleguideConfig<'built-in'> = { 'step-onSuccess-unique': 'off', 'step-onFailure-unique': 'off', 'requestBody-replacements-unique': 'off', + 'no-criteria-xpath': 'off', + 'no-actions-type-end': 'off', + 'criteria-unique': 'off', }, }; diff --git a/packages/core/src/config/recommended-strict.ts b/packages/core/src/config/recommended-strict.ts index 33bd5b17..9cd52552 100644 --- a/packages/core/src/config/recommended-strict.ts +++ b/packages/core/src/config/recommended-strict.ts @@ -121,6 +121,9 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = { 'step-onSuccess-unique': 'error', 'step-onFailure-unique': 'error', 'requestBody-replacements-unique': 'error', + 'no-criteria-xpath': 'error', + 'no-actions-type-end': 'error', + 'criteria-unique': 'error', }, }; diff --git a/packages/core/src/config/recommended.ts b/packages/core/src/config/recommended.ts index 484e35ac..3a155d80 100644 --- a/packages/core/src/config/recommended.ts +++ b/packages/core/src/config/recommended.ts @@ -121,6 +121,9 @@ const recommended: PluginStyleguideConfig<'built-in'> = { 'step-onSuccess-unique': 'warn', 'step-onFailure-unique': 'warn', 'requestBody-replacements-unique': 'warn', + 'no-criteria-xpath': 'warn', + 'no-actions-type-end': 'warn', + 'criteria-unique': 'warn', }, }; diff --git a/packages/core/src/rules/arazzo/__tests__/criteria-unique.test.ts b/packages/core/src/rules/arazzo/__tests__/criteria-unique.test.ts new file mode 100644 index 00000000..a1dd0753 --- /dev/null +++ b/packages/core/src/rules/arazzo/__tests__/criteria-unique.test.ts @@ -0,0 +1,161 @@ +import { outdent } from 'outdent'; +import { lintDocument } from '../../../lint'; +import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils'; +import { BaseResolver } from '../../../resolve'; + +describe('Arazzo criteria-unique', () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.0' + info: + title: Cool API + version: 1.0.0 + description: A cool API + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: get-museum-hours + description: This workflow demonstrates how to get the museum opening hours and buy tickets. + parameters: + - in: header + name: Authorization + value: Basic Og== + steps: + - stepId: create-event + description: >- + Create a new special event. + operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post + requestBody: + payload: + name: 'Mermaid Treasure Identification and Analysis' + location: 'Under the seaaa 🦀 🎶 🌊.' + eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.' + dates: + - '2023-09-05' + - '2023-09-08' + price: 0 + successCriteria: + - condition: $statusCode == 200 + - condition: $statusCode == 200 + - context: $response.body + condition: $.name == 'Mermaid Treasure Identification and Analysis' + type: jsonpath + - context: $response.body + condition: $.name == 'Mermaid Treasure Identification and Analysis' + type: jsonpath + onSuccess: + - name: 'onSuccessActionName' + type: 'goto' + stepId: 'buy-ticket' + criteria: + - condition: $response.body.open == true + - condition: $response.body.open == true + onFailure: + - name: 'onFailureActionName' + type: 'goto' + stepId: 'buy-ticket' + criteria: + - condition: $response.body.open == true + - condition: $response.body.open == true + outputs: + createdEventId: $response.body.eventId + name: $response.body.name + - workflowId: get-museum-hours-2 + description: This workflow demonstrates how to get the museum opening hours and buy tickets. + parameters: + - in: header + name: Authorization + value: Basic Og== + steps: + - stepId: get-museum-hours + description: >- + Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description. + operationId: museum-api.getMuseumHours + successCriteria: + - condition: $statusCode == 200 + `, + 'arazzo.yaml' + ); + + it('should report when the duplicated criteria exists', async () => { + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ + rules: {}, + arazzoRules: { 'criteria-unique': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/successCriteria/1", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The Step SuccessCriteria items must be unique.", + "ruleId": "criteria-unique", + "severity": "error", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/successCriteria/3", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The Step SuccessCriteria items must be unique.", + "ruleId": "criteria-unique", + "severity": "error", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/onSuccess/0/criteria/1", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The SuccessAction criteria items must be unique.", + "ruleId": "criteria-unique", + "severity": "error", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/onFailure/0/criteria/1", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The FailureAction criteria items must be unique.", + "ruleId": "criteria-unique", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should not report when the duplicated criteria exists', async () => { + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ + rules: {}, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); +}); diff --git a/packages/core/src/rules/arazzo/__tests__/no-actions-type-end.test.ts b/packages/core/src/rules/arazzo/__tests__/no-actions-type-end.test.ts new file mode 100644 index 00000000..d2625119 --- /dev/null +++ b/packages/core/src/rules/arazzo/__tests__/no-actions-type-end.test.ts @@ -0,0 +1,122 @@ +import { outdent } from 'outdent'; +import { lintDocument } from '../../../lint'; +import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils'; +import { BaseResolver } from '../../../resolve'; + +describe('Arazzo no-actions-type-end', () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.0' + info: + title: Cool API + version: 1.0.0 + description: A cool API + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: get-museum-hours + description: This workflow demonstrates how to get the museum opening hours and buy tickets. + parameters: + - in: header + name: Authorization + value: Basic Og== + steps: + - stepId: create-event + description: >- + Create a new special event. + operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post + requestBody: + payload: + name: 'Mermaid Treasure Identification and Analysis' + location: 'Under the seaaa 🦀 🎶 🌊.' + eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.' + dates: + - '2023-09-05' + - '2023-09-08' + price: 0 + successCriteria: + - condition: $statusCode == 201 + onSuccess: + - name: 'onSuccessActionName' + type: 'end' + stepId: 'buy-ticket' + onFailure: + - name: 'onFailureActionName' + type: 'end' + stepId: 'buy-ticket' + outputs: + createdEventId: $response.body.eventId + name: $response.body.name + - workflowId: get-museum-hours-2 + description: This workflow demonstrates how to get the museum opening hours and buy tickets. + parameters: + - in: header + name: Authorization + value: Basic Og== + steps: + - stepId: get-museum-hours + description: >- + Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description. + operationId: museum-api.getMuseumHours + successCriteria: + - condition: $statusCode == 200 + `, + 'arazzo.yaml' + ); + + it('should report when the type `end` action exists', async () => { + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ + rules: {}, + arazzoRules: { 'no-actions-type-end': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/onSuccess/0/type", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The \`end\` type action is not supported by Spot.", + "ruleId": "no-actions-type-end", + "severity": "error", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/onFailure/0/type", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The \`end\` type action is not supported by Spot.", + "ruleId": "no-actions-type-end", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should not report when the type `end` action exists', async () => { + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ + rules: {}, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); +}); diff --git a/packages/core/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts b/packages/core/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts new file mode 100644 index 00000000..6d0e845c --- /dev/null +++ b/packages/core/src/rules/arazzo/__tests__/no-criteria-xpath.test.ts @@ -0,0 +1,127 @@ +import { outdent } from 'outdent'; +import { lintDocument } from '../../../lint'; +import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils'; +import { BaseResolver } from '../../../resolve'; + +describe('Arazzo no-criteria-xpath', () => { + const document = parseYamlToDocument( + outdent` + arazzo: '1.0.0' + info: + title: Cool API + version: 1.0.0 + description: A cool API + sourceDescriptions: + - name: museum-api + type: openapi + url: openapi.yaml + workflows: + - workflowId: get-museum-hours + description: This workflow demonstrates how to get the museum opening hours and buy tickets. + parameters: + - in: header + name: Authorization + value: Basic Og== + steps: + - stepId: create-event + description: >- + Create a new special event. + operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/post + requestBody: + payload: + name: 'Mermaid Treasure Identification and Analysis' + location: 'Under the seaaa 🦀 🎶 🌊.' + eventDescription: 'Join us as we review and classify a rare collection of 20 thingamabobs, gadgets, gizmos, whoosits, and whatsits, kindly donated by Ariel.' + dates: + - '2023-09-05' + - '2023-09-08' + price: 0 + successCriteria: + - condition: $statusCode == 201 + - context: $response.body + condition: $.name == 'Mermaid Treasure Identification and Analysis' + type: + type: jsonpath + version: draft-goessner-dispatch-jsonpath-00 + - context: $response.body + condition: $.name == 'Orca Identification and Analysis' + type: xpath + - context: $response.body + condition: $.name == 'Mermaid Treasure Identification and Analysis' + type: + type: xpath + version: xpath-30 + outputs: + createdEventId: $response.body.eventId + name: $response.body.name + - workflowId: get-museum-hours-2 + description: This workflow demonstrates how to get the museum opening hours and buy tickets. + parameters: + - in: header + name: Authorization + value: Basic Og== + steps: + - stepId: get-museum-hours + description: >- + Get museum hours by resolving request details with getMuseumHours operationId from openapi.yaml description. + operationId: museum-api.getMuseumHours + successCriteria: + - condition: $statusCode == 200 + `, + 'arazzo.yaml' + ); + + it('should report when the `xpath` criteria exists', async () => { + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ + rules: {}, + arazzoRules: { 'no-criteria-xpath': 'error' }, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/successCriteria/2/type", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The \`xpath\` type criteria is not supported by Spot.", + "ruleId": "no-criteria-xpath", + "severity": "error", + "suggest": [], + }, + { + "location": [ + { + "pointer": "#/workflows/0/steps/0/successCriteria/3/type", + "reportOnKey": false, + "source": "arazzo.yaml", + }, + ], + "message": "The \`xpath\` type criteria is not supported by Spot.", + "ruleId": "no-criteria-xpath", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should not report when the `xpath` criteria exists', async () => { + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await makeConfig({ + rules: {}, + }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); +}); diff --git a/packages/core/src/rules/arazzo/criteria-unique.ts b/packages/core/src/rules/arazzo/criteria-unique.ts new file mode 100644 index 00000000..d1947c06 --- /dev/null +++ b/packages/core/src/rules/arazzo/criteria-unique.ts @@ -0,0 +1,63 @@ +import type { ArazzoRule } from '../../visitors'; +import type { UserContext } from '../../walk'; + +export const CriteriaUnique: ArazzoRule = () => { + return { + FailureActionObject: { + enter(action, { report, location }: UserContext) { + const criterias = action.criteria; + const seen = new Set(); + for (const criteria of criterias) { + const key = JSON.stringify(criteria); + if (seen.has(key)) { + report({ + message: 'The FailureAction criteria items must be unique.', + location: location.child(['criteria', criterias.indexOf(criteria)]), + }); + } else { + seen.add(key); + } + } + }, + }, + SuccessActionObject: { + enter(action, { report, location }: UserContext) { + const criterias = action.criteria; + const seen = new Set(); + for (const criteria of criterias) { + const key = JSON.stringify(criteria); + if (seen.has(key)) { + report({ + message: 'The SuccessAction criteria items must be unique.', + location: location.child(['criteria', criterias.indexOf(criteria)]), + }); + } else { + seen.add(key); + } + } + }, + }, + Step: { + enter(step, { report, location }: UserContext) { + if (!step.successCriteria) { + return; + } + + const successCriterias = step.successCriteria; + const seen = new Set(); + + for (const criteria of successCriterias) { + const key = JSON.stringify(criteria); + if (seen.has(key)) { + report({ + message: 'The Step SuccessCriteria items must be unique.', + location: location.child(['successCriteria', successCriterias.indexOf(criteria)]), + }); + } else { + seen.add(key); + } + } + }, + }, + }; +}; diff --git a/packages/core/src/rules/arazzo/index.ts b/packages/core/src/rules/arazzo/index.ts index ffdaadfe..c00261a2 100644 --- a/packages/core/src/rules/arazzo/index.ts +++ b/packages/core/src/rules/arazzo/index.ts @@ -11,6 +11,9 @@ import { ParametersUnique } from './parameters-unique'; import { StepOnSuccessUnique } from './step-onSuccess-unique'; import { StepOnFailureUnique } from './step-onFailure-unique'; import { RequestBodyReplacementsUnique } from './requestBody-replacements-unique'; +import { NoCriteriaXpath } from '../spot/no-criteria-xpath'; +import { NoActionsTypeEnd } from '../spot/no-actions-type-end'; +import { CriteriaUnique } from './criteria-unique'; import type { ArazzoRule } from '../../visitors'; import type { ArazzoRuleSet } from '../../oas-types'; @@ -29,6 +32,9 @@ export const rules: ArazzoRuleSet<'built-in'> = { 'step-onSuccess-unique': StepOnSuccessUnique as ArazzoRule, 'step-onFailure-unique': StepOnFailureUnique as ArazzoRule, 'requestBody-replacements-unique': RequestBodyReplacementsUnique as ArazzoRule, + 'no-criteria-xpath': NoCriteriaXpath as ArazzoRule, + 'no-actions-type-end': NoActionsTypeEnd as ArazzoRule, + 'criteria-unique': CriteriaUnique as ArazzoRule, }; export const preprocessors = {}; diff --git a/packages/core/src/rules/spot/no-actions-type-end.ts b/packages/core/src/rules/spot/no-actions-type-end.ts new file mode 100644 index 00000000..f0e816d2 --- /dev/null +++ b/packages/core/src/rules/spot/no-actions-type-end.ts @@ -0,0 +1,27 @@ +import type { ArazzoRule } from '../../visitors'; +import type { UserContext } from '../../walk'; + +export const NoActionsTypeEnd: ArazzoRule = () => { + return { + FailureActionObject: { + enter(action, { report, location }: UserContext) { + if (action.type === 'end') { + report({ + message: 'The `end` type action is not supported by Spot.', + location: location.child(['type']), + }); + } + }, + }, + SuccessActionObject: { + enter(action, { report, location }: UserContext) { + if (action.type === 'end') { + report({ + message: 'The `end` type action is not supported by Spot.', + location: location.child(['type']), + }); + } + }, + }, + }; +}; diff --git a/packages/core/src/rules/spot/no-criteria-xpath.ts b/packages/core/src/rules/spot/no-criteria-xpath.ts new file mode 100644 index 00000000..dac5a950 --- /dev/null +++ b/packages/core/src/rules/spot/no-criteria-xpath.ts @@ -0,0 +1,20 @@ +import type { ArazzoRule } from '../../visitors'; +import type { UserContext } from '../../walk'; + +export const NoCriteriaXpath: ArazzoRule = () => { + return { + CriterionObject: { + enter(criteria, { report, location }: UserContext) { + if (!criteria.type) { + return; + } + if (criteria?.type?.type === 'xpath' || criteria?.type === 'xpath') { + report({ + message: 'The `xpath` type criteria is not supported by Spot.', + location: location.child(['type']), + }); + } + }, + }, + }; +}; diff --git a/packages/core/src/types/arazzo.ts b/packages/core/src/types/arazzo.ts index c330d3a1..a435af9e 100755 --- a/packages/core/src/types/arazzo.ts +++ b/packages/core/src/types/arazzo.ts @@ -205,7 +205,7 @@ const CriterionObject: NodeType = { return undefined; } else if (typeof value === 'string') { return { enum: ['regex', 'jsonpath', 'simple', 'xpath'] }; - } else if (value.type === 'jsonpath') { + } else if (value?.type === 'jsonpath') { return 'JSONPathCriterion'; } else { return 'XPathCriterion'; diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts index 7887ae2a..dc8675ef 100644 --- a/packages/core/src/types/redocly-yaml.ts +++ b/packages/core/src/types/redocly-yaml.ts @@ -122,6 +122,9 @@ const builtInArazzoRules = [ 'step-onSuccess-unique', 'step-onFailure-unique', 'requestBody-replacements-unique', + 'no-criteria-xpath', + 'no-actions-type-end', + 'criteria-unique', ] as const; export type BuiltInArazzoRuleId = typeof builtInArazzoRules[number]; diff --git a/resources/arazzo.yaml b/resources/arazzo.yaml index 348a20e9..db32f08d 100644 --- a/resources/arazzo.yaml +++ b/resources/arazzo.yaml @@ -68,7 +68,9 @@ workflows: - condition: $statusCode == 201 - context: $response.body condition: $.name == 'Mermaid Treasure Identification and Analysis' - type: jsonpath + type: + type: jsonpath + version: draft-goessner-dispatch-jsonpath-00 outputs: createdEventId: $response.body.eventId name: $response.body.name