feat: spot-arazzo rules (#1670)

This commit is contained in:
Dmytro Anansky
2024-08-29 12:19:34 +03:00
committed by GitHub
parent 77da124e87
commit 7c3de8509b
41 changed files with 1551 additions and 54 deletions

View File

@@ -0,0 +1,6 @@
---
"@redocly/openapi-core": minor
"@redocly/cli": minor
---
Updated the Arazzo validation types for workflows input, parameter objects, and criteria to match the specification.

View File

@@ -0,0 +1,6 @@
---
"@redocly/openapi-core": minor
"@redocly/cli": minor
---
Added Arazzo rulesets so that users can customize their linting rules for this format.

View File

@@ -68,9 +68,36 @@ arazzo/museum-api.arazzo.yaml: validated in 14ms
run `redocly lint --generate-ignore-file` to add all problems to the ignore file.
```
{% admonition type="info" name="Validation only" %}
No additional rules or configuration are available for Arazzo in the current version of Redocly CLI; the tool merely checks that the file meets the specification.
{% /admonition %}
## Configure the linting rules
Choose from the ready-made rulesets (`minimal`, `recommended` or `recommended-strict`), or go one better and configure the rules that suit your use case.
The rules available for linting Arazzo are:
- `parameters-not-in-body`: the `in` section inside `parameters` must not contain a `body`.
- `sourceDescription-type`: the `type` property of the `sourceDescription` object must be either `openapi` or `arazzo`.
- `version-enum`: the `version` property must be one of the supported values.
- `workflowId-unique`: the `workflowId` property must be unique across all workflows.
- `stepId-unique`: the `stepId` must be unique amongst all steps described in the workflow.
- `sourceDescription-name-unique`: the `name` property of the `sourceDescription` object must be unique across all source descriptions.
- `workflow-dependsOn`: the items in the `workflow` `dependsOn` property must exist and be unique.
- `parameters-unique`: the `parameters` list must not include duplicate parameters.
- `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.
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:
```yaml
extends:
- minimal
arazzoRules:
sourceDescription-name-unique: warn
version-enum: error
```
The configuration shown here gives some good entry-level linting using the `minimal` standard, and adds checks that we're using a supported version of Arazzo, and warns if each source description doesn't have a unique name.
## Choose output format
@@ -114,13 +141,6 @@ With this action in place, the intentional errors I added to the Arazzo descript
![Screenshot of annotation flagging "workfloo" as an unexpected value and suggesting "workflow"](images/museum-arazzo-lint.png)
## Arazzo rules
To expand the linting checks for an Arazzo description, start by enabling
some of the built-in rules. The currently-supported rules are:
- `parameters-no-body-inside-in`: the `in` section inside `parameters` must not contain a `body`.
## Participate in Redocly CLI
Redocly CLI is an open source project, so we invite you to check out the [code on GitHub](https://github.com/Redocly/redocly-cli/), and open issues to report problems or request features.

View File

@@ -1,13 +1,9 @@
import * as fs from 'fs';
import * as path from 'path';
import { slash } from '@redocly/openapi-core';
import { pluralize } from '@redocly/openapi-core/lib/utils';
import { green, yellow } from 'colorette';
import {
exitWithError,
HandledError,
pluralize,
printExecutionTime,
} from '../../utils/miscellaneous';
import { exitWithError, HandledError, printExecutionTime } from '../../utils/miscellaneous';
import { handlePushStatus } from './push-status';
import { ReuniteApiClient, getDomain, getApiKeys } from '../api';

View File

@@ -8,6 +8,7 @@ import {
lintConfig,
} from '@redocly/openapi-core';
import { ConfigValidationError } from '@redocly/openapi-core/lib/config';
import { pluralize } from '@redocly/openapi-core/lib/utils';
import {
checkIfRulesetExist,
exitWithError,
@@ -15,7 +16,6 @@ import {
getFallbackApisOrExit,
handleError,
notifyAboutIncompatibleConfigOptions,
pluralize,
printConfigLintTotals,
printLintTotals,
printUnusedWarnings,

View File

@@ -13,12 +13,12 @@ import {
getMergedConfig,
getProxyAgent,
} from '@redocly/openapi-core';
import { pluralize } from '@redocly/openapi-core/lib/utils';
import {
exitWithError,
printExecutionTime,
getFallbackApisOrExit,
dumpBundle,
pluralize,
} from '../utils/miscellaneous';
import { promptClientToken } from './login';
import { handlePush as handleCMSPush } from '../cms/commands/push';

View File

@@ -1,4 +1,3 @@
import * as pluralizeOne from 'pluralize';
import { basename, dirname, extname, join, resolve, relative, isAbsolute } from 'path';
import { blue, gray, green, red, yellow } from 'colorette';
import { performance } from 'perf_hooks';
@@ -17,7 +16,7 @@ import {
loadConfig,
RedoclyClient,
} from '@redocly/openapi-core';
import { isEmptyObject, isPlainObject } from '@redocly/openapi-core/lib/utils';
import { isEmptyObject, isPlainObject, pluralize } from '@redocly/openapi-core/lib/utils';
import { ConfigValidationError } from '@redocly/openapi-core/lib/config';
import { deprecatedRefDocsSchema } from '@redocly/config/lib/reference-docs-config-schema';
import { outputExtensions } from '../types';
@@ -277,13 +276,6 @@ export function getAndValidateFileExtension(fileName: string): NonNullable<Outpu
return 'yaml';
}
export function pluralize(sentence: string, count?: number, inclusive?: boolean) {
return sentence
.split(' ')
.map((word) => pluralizeOne(word, count, inclusive))
.join(' ');
}
export function handleError(e: Error, ref: string) {
switch (e.constructor) {
case HandledError: {

View File

@@ -5,8 +5,18 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
"arazzoDecorators": {},
"arazzoPreprocessors": {},
"arazzoRules": {
"parameters-no-body-inside-in": "off",
"parameters-not-in-body": "warn",
"parameters-unique": "error",
"requestBody-replacements-unique": "warn",
"sourceDescription-name-unique": "error",
"sourceDescription-type": "error",
"spec": "error",
"step-onFailure-unique": "warn",
"step-onSuccess-unique": "warn",
"stepId-unique": "error",
"version-enum": "warn",
"workflow-dependsOn": "error",
"workflowId-unique": "error",
},
"async2Decorators": {},
"async2Preprocessors": {},
@@ -137,8 +147,18 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
"arazzoDecorators": {},
"arazzoPreprocessors": {},
"arazzoRules": {
"parameters-no-body-inside-in": "off",
"parameters-not-in-body": "warn",
"parameters-unique": "error",
"requestBody-replacements-unique": "warn",
"sourceDescription-name-unique": "error",
"sourceDescription-type": "error",
"spec": "error",
"step-onFailure-unique": "warn",
"step-onSuccess-unique": "warn",
"stepId-unique": "error",
"version-enum": "warn",
"workflow-dependsOn": "error",
"workflowId-unique": "error",
},
"async2Decorators": {},
"async2Preprocessors": {},

View File

@@ -126,7 +126,20 @@ const all: PluginStyleguideConfig<'built-in'> = {
'channels-kebab-case': 'error',
'no-channel-trailing-slash': 'error',
},
arazzoRules: { spec: 'error', 'parameters-no-body-inside-in': 'off' },
arazzoRules: {
spec: 'error',
'parameters-not-in-body': 'error',
'sourceDescription-type': 'error',
'version-enum': 'error',
'workflowId-unique': 'error',
'stepId-unique': 'error',
'sourceDescription-name-unique': 'error',
'workflow-dependsOn': 'error',
'parameters-unique': 'error',
'step-onSuccess-unique': 'error',
'step-onFailure-unique': 'error',
'requestBody-replacements-unique': 'error',
},
};
export default all;

View File

@@ -110,7 +110,17 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
},
arazzoRules: {
spec: 'error',
'parameters-no-body-inside-in': 'off',
'parameters-not-in-body': 'off',
'sourceDescription-type': 'off',
'version-enum': 'warn',
'workflowId-unique': 'error',
'stepId-unique': 'error',
'sourceDescription-name-unique': 'off',
'workflow-dependsOn': 'off',
'parameters-unique': 'off',
'step-onSuccess-unique': 'off',
'step-onFailure-unique': 'off',
'requestBody-replacements-unique': 'off',
},
};

View File

@@ -110,7 +110,17 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
},
arazzoRules: {
spec: 'error',
'parameters-no-body-inside-in': 'off',
'parameters-not-in-body': 'error',
'sourceDescription-type': 'error',
'version-enum': 'error',
'workflowId-unique': 'error',
'stepId-unique': 'error',
'sourceDescription-name-unique': 'error',
'workflow-dependsOn': 'error',
'parameters-unique': 'error',
'step-onSuccess-unique': 'error',
'step-onFailure-unique': 'error',
'requestBody-replacements-unique': 'error',
},
};

View File

@@ -110,7 +110,17 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
},
arazzoRules: {
spec: 'error',
'parameters-no-body-inside-in': 'off',
'parameters-not-in-body': 'warn',
'sourceDescription-type': 'error',
'version-enum': 'warn',
'workflowId-unique': 'error',
'stepId-unique': 'error',
'sourceDescription-name-unique': 'error',
'workflow-dependsOn': 'error',
'parameters-unique': 'error',
'step-onSuccess-unique': 'warn',
'step-onFailure-unique': 'warn',
'requestBody-replacements-unique': 'warn',
},
};

View File

@@ -5,6 +5,7 @@ import { AsyncApi2Types } from './types/asyncapi2';
import { AsyncApi3Types } from './types/asyncapi3';
import { ArazzoTypes } from './types/arazzo';
import { isPlainObject } from './utils';
import { VERSION_PATTERN } from './typings/arazzo';
import type {
BuiltInAsync2RuleId,
@@ -132,7 +133,7 @@ export function detectSpec(root: unknown): SpecVersion {
throw new Error(`Unsupported AsyncAPI version: ${root.asyncapi}`);
}
if (typeof root.arazzo === 'string' && root.arazzo.startsWith('1.')) {
if (typeof root.arazzo === 'string' && VERSION_PATTERN.test(root.arazzo)) {
return SpecVersion.Arazzo;
}

View File

@@ -2,10 +2,8 @@ import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
import { StyleguideConfig } from '../../../config';
import { ArazzoRule } from '../../../visitors';
describe('Arazzo parameters-no-body-inside-in', () => {
describe('Spot parameters-not-in-body', () => {
const document = parseYamlToDocument(
outdent`
arazzo: '1.0.0'
@@ -51,7 +49,7 @@ describe('Arazzo parameters-no-body-inside-in', () => {
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'parameters-no-body-inside-in': 'error' },
arazzoRules: { 'parameters-not-in-body': 'error' },
}),
});
@@ -66,7 +64,7 @@ describe('Arazzo parameters-no-body-inside-in', () => {
},
],
"message": "The \`body\` value of the \`in\` property is not supported by Spot.",
"ruleId": "parameters-no-body-inside-in",
"ruleId": "parameters-not-in-body",
"severity": "error",
"suggest": [],
},

View File

@@ -0,0 +1,114 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo parameters-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==
- 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
parameters:
- in: header
name: Secret
value: Basic Og==
- in: header
name: Secret
value: Basic Og==
- reference: $components.parameters.notify
value: 12
- reference: $components.parameters.notify
value: 12
successCriteria:
- condition: $statusCode == 200
`,
'arazzo.yaml'
);
it('should not report on `parameters` duplication', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({ rules: {} }),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
it('should report on `parameters` duplication', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'parameters-unique': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/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/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/parameters/3",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The parameter \`reference\` must be unique amongst listed parameters.",
"ruleId": "parameters-unique",
"severity": "error",
"suggest": [],
},
]
`);
});
});

View File

@@ -0,0 +1,109 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo requestBody-replacements-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:
replacements:
- target: $randomString
value: $randomString
- target: $randomString
value: $randomString
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: jsonpath
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 `replacement` is not unique amongst all `replacements` in the RequestBody', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'requestBody-replacements-unique': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/0/steps/0/requestBody/replacements/1/target",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "Every \`replacement\` in \`requestBody\` must be unique.",
"ruleId": "requestBody-replacements-unique",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report when the `replacement` is not unique amongst all `replacements` in the RequestBody', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -0,0 +1,80 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo sourceDescription-type', () => {
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
- name: api
type: none
x-serverUrl: 'http://localhost/api'
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: 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 on sourceDescription with type `none`', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'sourceDescription-type': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/sourceDescriptions/1",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The \`type\` property of the \`sourceDescription\` object must be either \`openapi\` or \`arazzo\`.",
"ruleId": "sourceDescription-type",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report on sourceDescription with type `none`', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'sourceDescription-type': 'off' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -0,0 +1,79 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo sourceDescription-name-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
- 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: 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 an error when sourceDescription `name` is not unique among all sourceDescriptions', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'sourceDescription-name-unique': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/sourceDescriptions/1",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The \`name\` must be unique amongst all SourceDescriptions.",
"ruleId": "sourceDescription-name-unique",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report an error when sourceDescription `name` is not unique among all sourceDescriptions', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -0,0 +1,111 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo step-onFailure-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: 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
onFailure:
- name: test
workflowId: events-crud
type: goto
- name: test
workflowId: events-crud
type: goto
- reference: $steps.test.outputs.createdEventId
- reference: $steps.test.outputs.createdEventId
- 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 action `name` or `reference` is not unique amongst all onFailure actions in the step', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'step-onFailure-unique': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/0/steps/0/onFailure/1",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The action \`name\` must be unique amongst listed \`onFailure\` actions.",
"ruleId": "step-onFailure-unique",
"severity": "error",
"suggest": [],
},
{
"location": [
{
"pointer": "#/workflows/0/steps/0/onFailure/3",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The action \`reference\` must be unique amongst listed \`onFailure\` actions.",
"ruleId": "step-onFailure-unique",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report when the action `name` or `reference` is not unique amongst all onFailure actions in the step', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -0,0 +1,111 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo step-onSuccess-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: 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
onSuccess:
- name: test
workflowId: events-crud
type: goto
- name: test
workflowId: events-crud
type: goto
- reference: $steps.test.outputs.createdEventId
- reference: $steps.test.outputs.createdEventId
- 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 action `name` or `reference` is not unique amongst all onSuccess actions in the step', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'step-onSuccess-unique': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/0/steps/0/onSuccess/1",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The action \`name\` must be unique amongst listed \`onSuccess\` actions.",
"ruleId": "step-onSuccess-unique",
"severity": "error",
"suggest": [],
},
{
"location": [
{
"pointer": "#/workflows/0/steps/0/onSuccess/3",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The action \`reference\` must be unique amongst listed \`onSuccess\` actions.",
"ruleId": "step-onSuccess-unique",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report when the action `name` or `reference` is not unique amongst all onSuccess actions in the step', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -0,0 +1,95 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo stepId-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: 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
- 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
- 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 `stepId` is not unique amongst all steps described in the workflow', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'stepId-unique': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/1/steps/1",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "The \`stepId\` must be unique amongst all steps described in the workflow.",
"ruleId": "stepId-unique",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report when the `stepId` is not unique amongst all steps described in the workflow', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -0,0 +1,76 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo version-enum', () => {
const document = 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: 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: 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 on arazzo version error', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'version-enum': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/arazzo",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "Only 1.0.0 Arazzo version is supported by Spot.",
"ruleId": "version-enum",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report on arazzo version error', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -0,0 +1,212 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo workflow-dependsOn', () => {
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.
dependsOn:
- get-museum-hours-2
- get-museum-hours-3
- get-museum-hours-2
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
- 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
- workflowId: get-museum-hours-3
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'
);
const documentWithNotExistingWorkflows = parseYamlToDocument(
outdent`
arazzo: 1.0.0
info:
title: Redocly Museum API Test Workflow
description: >-
Use the Museum API with Arazzo as an example of describing multi-step workflows.
Built with love by Redocly.
version: 1.0.0
sourceDescriptions:
- name: museum-api
type: openapi
url: ../openapi.yaml
- name: tickets-from-museum-api
type: arazzo
url: museum-tickets.arazzo.yaml
workflows:
- workflowId: get-museum-hours
dependsOn:
- events-crud
- events-crus
- $sourceDescriptions.tickets-from-museum-apis.workflows.get-museum-tickets
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
outputs:
schedule: $response.body
- stepId: buy-ticket
description: >-
Buy a ticket for the museum by calling an external workflow from another Arazzo file.
workflowId: $sourceDescriptions.tickets-from-museum-api.workflows.get-museum-tickets
outputs:
ticketId: $outputs.ticketId
- workflowId: events-crud
description: >-
This workflow demonstrates how to list, create, update, and delete special events at the museum.
parameters:
- in: header
name: Authorization
value: Basic Og==
steps:
- stepId: list-events
description: >-
Request the list of events.
operationPath: $sourceDescriptions.museum-api#/paths/~1special-events/get
outputs:
events: $response.body
`,
'arazzo.yaml'
);
it('should report on dependsOn unique violation', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'workflow-dependsOn': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/0/dependsOn",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "Every workflow in dependsOn must be unique.",
"ruleId": "workflow-dependsOn",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report on dependsOn unique violation', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'workflow-dependsOn': 'off' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
it('should report on not existing workflows in dependsOn', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document: documentWithNotExistingWorkflows,
config: await makeConfig({
rules: {},
arazzoRules: { 'workflow-dependsOn': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/0/dependsOn/1",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "Workflow events-crus must be defined in workflows.",
"ruleId": "workflow-dependsOn",
"severity": "error",
"suggest": [],
},
{
"location": [
{
"pointer": "#/workflows/0/dependsOn/2",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "SourceDescription tickets-from-museum-apis must be defined in sourceDescriptions.",
"ruleId": "workflow-dependsOn",
"severity": "error",
"suggest": [],
},
]
`);
});
});

View File

@@ -0,0 +1,90 @@
import { outdent } from 'outdent';
import { lintDocument } from '../../../lint';
import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../../../__tests__/utils';
import { BaseResolver } from '../../../resolve';
describe('Arazzo workflowId-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: 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
- 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: 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 on workflowId unique violation', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'workflowId-unique': 'error' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"location": [
{
"pointer": "#/workflows/1/get-museum-hours",
"reportOnKey": false,
"source": "arazzo.yaml",
},
],
"message": "Every workflow must have a unique \`workflowId\`.",
"ruleId": "workflowId-unique",
"severity": "error",
"suggest": [],
},
]
`);
});
it('should not report on workflowId unique violation', async () => {
const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({
rules: {},
arazzoRules: { 'workflowId-unique': 'off' },
}),
});
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});

View File

@@ -1,6 +1,16 @@
import { Spec } from '../common/spec';
import { Assertions } from '../common/assertions';
import { ParametersNoBodyInsideIn } from '../spot/parameters-no-body-inside-in';
import { ParametersNotInBody } from '../spot/parameters-not-in-body';
import { SourceDescriptionType } from '../arazzo/source-description-type';
import { VersionEnum } from '../spot/version-enum';
import { WorkflowIdUnique } from './workflowId-unique';
import { StepIdUnique } from './stepId-unique';
import { SourceDescriptionsNameUnique } from './sourceDescriptions-name-unique';
import { WorkflowDependsOn } from './workflow-dependsOn';
import { ParametersUnique } from './parameters-unique';
import { StepOnSuccessUnique } from './step-onSuccess-unique';
import { StepOnFailureUnique } from './step-onFailure-unique';
import { RequestBodyReplacementsUnique } from './requestBody-replacements-unique';
import type { ArazzoRule } from '../../visitors';
import type { ArazzoRuleSet } from '../../oas-types';
@@ -8,7 +18,17 @@ import type { ArazzoRuleSet } from '../../oas-types';
export const rules: ArazzoRuleSet<'built-in'> = {
spec: Spec as ArazzoRule,
assertions: Assertions as ArazzoRule,
'parameters-no-body-inside-in': ParametersNoBodyInsideIn as ArazzoRule,
'parameters-not-in-body': ParametersNotInBody as ArazzoRule,
'sourceDescription-type': SourceDescriptionType as ArazzoRule,
'version-enum': VersionEnum as ArazzoRule,
'workflowId-unique': WorkflowIdUnique as ArazzoRule,
'stepId-unique': StepIdUnique as ArazzoRule,
'sourceDescription-name-unique': SourceDescriptionsNameUnique as ArazzoRule,
'workflow-dependsOn': WorkflowDependsOn as ArazzoRule,
'parameters-unique': ParametersUnique as ArazzoRule,
'step-onSuccess-unique': StepOnSuccessUnique as ArazzoRule,
'step-onFailure-unique': StepOnFailureUnique as ArazzoRule,
'requestBody-replacements-unique': RequestBodyReplacementsUnique as ArazzoRule,
};
export const preprocessors = {};

View File

@@ -0,0 +1,33 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const ParametersUnique: ArazzoRule = () => {
return {
Parameters: {
enter(parameters, { 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)]),
});
}
if (seenParameters.has(parameter?.reference)) {
report({
message: 'The parameter `reference` must be unique amongst listed parameters.',
location: location.child([parameters.indexOf(parameter)]),
});
}
parameter?.name
? seenParameters.add(parameter.name)
: seenParameters.add(parameter.reference);
}
},
},
};
};

View File

@@ -0,0 +1,28 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const RequestBodyReplacementsUnique: ArazzoRule = () => {
const seenReplacements = new Set();
return {
RequestBody: {
enter(requestBody, { report, location }: UserContext) {
if (!requestBody.replacements) return;
for (const replacement of requestBody.replacements) {
if (seenReplacements.has(replacement.target)) {
report({
message: 'Every `replacement` in `requestBody` must be unique.',
location: location.child([
'replacements',
requestBody.replacements.indexOf(replacement),
`target`,
]),
});
}
seenReplacements.add(replacement.target);
}
},
},
};
};

View File

@@ -0,0 +1,20 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const SourceDescriptionType: ArazzoRule = () => {
return {
SourceDescriptions: {
enter(SourceDescriptions, { report, location }: UserContext) {
for (const sourceDescription of SourceDescriptions) {
if (!['openapi', 'arazzo'].includes(sourceDescription?.type)) {
report({
message:
'The `type` property of the `sourceDescription` object must be either `openapi` or `arazzo`.',
location: location.child([SourceDescriptions.indexOf(sourceDescription)]),
});
}
}
},
},
};
};

View File

@@ -0,0 +1,23 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const SourceDescriptionsNameUnique: ArazzoRule = () => {
const seenSourceDescriptions = new Set();
return {
SourceDescriptions: {
enter(sourceDescriptions, { report, location }: UserContext) {
if (!sourceDescriptions.length) return;
for (const sourceDescription of sourceDescriptions) {
if (seenSourceDescriptions.has(sourceDescription.name)) {
report({
message: 'The `name` must be unique amongst all SourceDescriptions.',
location: location.child([sourceDescriptions.indexOf(sourceDescription)]),
});
}
seenSourceDescriptions.add(sourceDescription.name);
}
},
},
};
};

View File

@@ -0,0 +1,33 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const StepOnFailureUnique: ArazzoRule = () => {
return {
OnFailureActionList: {
enter(onFailureActionList, { report, location }: UserContext) {
if (!onFailureActionList) return;
const seenFailureActions = new Set();
for (const onFailureAction of onFailureActionList) {
if (seenFailureActions.has(onFailureAction?.name)) {
report({
message: 'The action `name` must be unique amongst listed `onFailure` actions.',
location: location.child([onFailureActionList.indexOf(onFailureAction)]),
});
}
if (seenFailureActions.has(onFailureAction?.reference)) {
report({
message: 'The action `reference` must be unique amongst listed `onFailure` actions.',
location: location.child([onFailureActionList.indexOf(onFailureAction)]),
});
}
onFailureAction?.name
? seenFailureActions.add(onFailureAction.name)
: seenFailureActions.add(onFailureAction.reference);
}
},
},
};
};

View File

@@ -0,0 +1,33 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const StepOnSuccessUnique: ArazzoRule = () => {
return {
OnSuccessActionList: {
enter(onSuccessActionList, { report, location }: UserContext) {
if (!onSuccessActionList) return;
const seenSuccessActions = new Set();
for (const onSuccessAction of onSuccessActionList) {
if (seenSuccessActions.has(onSuccessAction?.name)) {
report({
message: 'The action `name` must be unique amongst listed `onSuccess` actions.',
location: location.child([onSuccessActionList.indexOf(onSuccessAction)]),
});
}
if (seenSuccessActions.has(onSuccessAction?.reference)) {
report({
message: 'The action `reference` must be unique amongst listed `onSuccess` actions.',
location: location.child([onSuccessActionList.indexOf(onSuccessAction)]),
});
}
onSuccessAction?.name
? seenSuccessActions.add(onSuccessAction.name)
: seenSuccessActions.add(onSuccessAction.reference);
}
},
},
};
};

View File

@@ -0,0 +1,24 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const StepIdUnique: ArazzoRule = () => {
return {
Workflow: {
enter(workflow, { report, location }: UserContext) {
if (!workflow.steps) return;
const seenSteps = new Set();
for (const step of workflow.steps) {
if (!step.stepId) return;
if (seenSteps.has(step.stepId)) {
report({
message: 'The `stepId` must be unique amongst all steps described in the workflow.',
location: location.child(['steps', workflow.steps.indexOf(step)]),
});
}
seenSteps.add(step.stepId);
}
},
},
};
};

View File

@@ -0,0 +1,56 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const WorkflowDependsOn: ArazzoRule = () => {
const seenWorkflow = new Set();
const existingSourceDescriptions = new Set();
const existingWorkflowIds = new Set();
return {
SourceDescriptions: {
enter(sourceDescriptions) {
for (const sourceDescription of sourceDescriptions) {
existingSourceDescriptions.add(sourceDescription.name);
}
},
},
Workflows: {
enter(workflows) {
for (const workflow of workflows) {
existingWorkflowIds.add(workflow.workflowId);
}
},
},
Workflow: {
leave(workflow, { report, location }: UserContext) {
if (!workflow.dependsOn) return;
for (const item of workflow.dependsOn) {
// Possible dependsOn workflow pattern: $sourceDescriptions.<name>.<workflowId>
if (item.startsWith('$sourceDescriptions.')) {
const sourceDescriptionName = item.split('.')[1];
if (!existingSourceDescriptions.has(sourceDescriptionName)) {
report({
message: `SourceDescription ${sourceDescriptionName} must be defined in sourceDescriptions.`,
location: location.child([`dependsOn`, workflow.dependsOn.indexOf(item)]),
});
}
}
if (!item.startsWith('$sourceDescriptions') && !existingWorkflowIds.has(item)) {
report({
message: `Workflow ${item} must be defined in workflows.`,
location: location.child([`dependsOn`, workflow.dependsOn.indexOf(item)]),
});
}
if (seenWorkflow.has(item)) {
report({
message: 'Every workflow in dependsOn must be unique.',
location: location.child([`dependsOn`]),
});
}
seenWorkflow.add(item);
}
},
},
};
};

View File

@@ -0,0 +1,21 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const WorkflowIdUnique: ArazzoRule = () => {
const seenWorkflow = new Set();
return {
Workflow: {
enter(workflow, { report, location }: UserContext) {
if (!workflow.workflowId) return;
if (seenWorkflow.has(workflow.workflowId)) {
report({
message: 'Every workflow must have a unique `workflowId`.',
location: location.child([workflow.workflowId]),
});
}
seenWorkflow.add(workflow.workflowId);
},
},
};
};

View File

@@ -1,7 +1,7 @@
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const ParametersNoBodyInsideIn: ArazzoRule = () => {
export const ParametersNotInBody: ArazzoRule = () => {
return {
Parameter: {
enter(parameter, { report, location }: UserContext) {

View File

@@ -0,0 +1,24 @@
import { ARAZZO_VERSIONS_SUPPORTED_BY_SPOT } from '../../typings/arazzo';
import { pluralize } from '../../utils';
import type { ArazzoRule } from '../../visitors';
import type { UserContext } from '../../walk';
export const VersionEnum: ArazzoRule = () => {
const supportedVersions = ARAZZO_VERSIONS_SUPPORTED_BY_SPOT.join(', ');
return {
Root: {
enter(root, { report, location }: UserContext) {
if (!ARAZZO_VERSIONS_SUPPORTED_BY_SPOT.includes(root.arazzo)) {
report({
message: `Only ${supportedVersions} Arazzo ${pluralize(
'version is',
ARAZZO_VERSIONS_SUPPORTED_BY_SPOT.length
)} supported by Spot.`,
location: location.child('arazzo'),
});
}
},
},
};
};

View File

@@ -4,7 +4,7 @@ import { Discriminator, DiscriminatorMapping, ExternalDocs, Xml } from './oas3';
const Root: NodeType = {
properties: {
arazzo: { type: 'string', enum: ['1.0.0'] },
arazzo: { type: 'string' },
info: 'Info',
sourceDescriptions: 'SourceDescriptions',
'x-parameters': 'Parameters',
@@ -89,9 +89,7 @@ const ArazzoSourceDescription: NodeType = {
const ReusableObject: NodeType = {
properties: {
reference: { type: 'string' },
value: {
type: 'string',
},
value: {}, // any
},
required: ['reference'],
extensionsPrefix: 'x-',
@@ -108,10 +106,10 @@ const Parameter: NodeType = {
const Parameters: NodeType = {
properties: {},
items: (value: any) => {
if (value?.in) {
return 'Parameter';
} else {
if (value?.reference) {
return 'ReusableObject';
} else {
return 'Parameter';
}
},
};
@@ -122,7 +120,7 @@ const Workflow: NodeType = {
description: { type: 'string' },
parameters: 'Parameters',
dependsOn: { type: 'array', items: { type: 'string' } },
inputs: 'NamedInputs',
inputs: 'Schema',
outputs: 'Outputs',
steps: 'Steps',
successActions: 'OnSuccessActionList',
@@ -234,7 +232,7 @@ const SuccessActionObject: NodeType = {
type: { type: 'string', enum: ['goto', 'end'] },
stepId: { type: 'string' },
workflowId: { type: 'string' },
criteria: 'CriterionObject',
criteria: listOf('CriterionObject'),
},
required: ['type', 'name'],
};
@@ -256,7 +254,7 @@ const FailureActionObject: NodeType = {
stepId: { type: 'string' },
retryAfter: { type: 'number' },
retryLimit: { type: 'number' },
criteria: 'CriterionObject',
criteria: listOf('CriterionObject'),
},
required: ['type', 'name'],
};

View File

@@ -109,7 +109,20 @@ export type BuiltInAsync2RuleId = typeof builtInAsync2Rules[number];
export type BuiltInAsync3RuleId = typeof builtInAsync3Rules[number];
const builtInArazzoRules = ['spec', 'parameters-no-body-inside-in'] as const;
const builtInArazzoRules = [
'spec',
'parameters-not-in-body',
'sourceDescription-type',
'version-enum',
'workflowId-unique',
'stepId-unique',
'sourceDescription-name-unique',
'workflow-dependsOn',
'parameters-unique',
'step-onSuccess-unique',
'step-onFailure-unique',
'requestBody-replacements-unique',
] as const;
export type BuiltInArazzoRuleId = typeof builtInArazzoRules[number];

View File

@@ -170,3 +170,7 @@ export interface ArazzoDefinition {
};
};
}
export const VERSION_PATTERN = /^1\.0\.\d+(-.+)?$/;
export const ARAZZO_VERSIONS_SUPPORTED_BY_SPOT = ['1.0.0'];

View File

@@ -6,6 +6,7 @@ import { parseYaml } from './js-yaml';
import { env } from './env';
import { logger, colorize } from './logger';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as pluralizeOne from 'pluralize';
import type { HttpResolveConfig } from './config';
import type { UserContext } from './walk';
@@ -23,6 +24,13 @@ export function pushStack<T, P extends Stack<T> = Stack<T>>(head: P, value: T) {
return { prev: head, value };
}
export function pluralize(sentence: string, count?: number, inclusive?: boolean) {
return sentence
.split(' ')
.map((word) => pluralizeOne(word, count, inclusive))
.join(' ');
}
export function popStack<T, P extends Stack<T>>(head: P) {
return head?.prev ?? null;
}

View File

@@ -78,7 +78,7 @@ workflows:
operationPath: $sourceDescriptions.museum-api#/paths/~1special-events~1{eventId}/get
parameters:
- name: eventId
in: body
in: path
value: $steps.create-event.outputs.createdEventId
successCriteria:
- context: $statusCode