feat: adding spot arazzo rules (#1713)

This commit is contained in:
Dmytro Anansky
2024-09-09 15:42:04 +03:00
committed by GitHub
parent e29a7db8f1
commit 58abf6fed4
20 changed files with 729 additions and 2 deletions

View File

@@ -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`.

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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",

View File

@@ -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',
},
};

View File

@@ -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',
},
};

View File

@@ -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',
},
};

View File

@@ -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',
},
};

View File

@@ -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(`[]`);
});
});

View File

@@ -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(`[]`);
});
});

View File

@@ -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(`[]`);
});
});

View File

@@ -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<string>();
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<string>();
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<string>();
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);
}
}
},
},
};
};

View File

@@ -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 = {};

View File

@@ -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']),
});
}
},
},
};
};

View File

@@ -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']),
});
}
},
},
};
};

View File

@@ -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';

View File

@@ -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];

View File

@@ -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