From 9a0cc13f93e4d2ef033a4eacaaca049c1e07b6e5 Mon Sep 17 00:00:00 2001 From: Frank Kilcommins Date: Fri, 22 Dec 2023 12:48:28 +0000 Subject: [PATCH 1/2] feat(reference): add Workflows 1.0.0 YAML parser plugin --- package-lock.json | 4 + packages/apidom-reference/README.md | 23 +- packages/apidom-reference/package.json | 11 +- .../src/configuration/saturated.ts | 2 + .../parse/parsers/workflows-yaml-1/index.ts | 43 ++++ .../fixtures/sample-workflow.yaml | 61 +++++ .../parse/parsers/workflows-yaml-1/index.ts | 223 ++++++++++++++++++ 7 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts create mode 100644 packages/apidom-reference/test/parse/parsers/workflows-yaml-1/fixtures/sample-workflow.yaml create mode 100644 packages/apidom-reference/test/parse/parsers/workflows-yaml-1/index.ts diff --git a/package-lock.json b/package-lock.json index a10778e59a..eeb3a005fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39940,6 +39940,8 @@ "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "*", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "*", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "*", + "@swagger-api/apidom-parser-adapter-workflows-json-1": "*", + "@swagger-api/apidom-parser-adapter-workflows-yaml-1": "*", "@swagger-api/apidom-parser-adapter-yaml-1-2": "*", "axios-mock-adapter": "^1.21.4" }, @@ -39962,6 +39964,8 @@ "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^0.89.0", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.89.0", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.89.0", + "@swagger-api/apidom-parser-adapter-workflows-json-1": "^0.89.0", + "@swagger-api/apidom-parser-adapter-workflows-yaml-1": "^0.89.0", "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0" } }, diff --git a/packages/apidom-reference/README.md b/packages/apidom-reference/README.md index d9a120693f..81ad4dba52 100644 --- a/packages/apidom-reference/README.md +++ b/packages/apidom-reference/README.md @@ -265,6 +265,20 @@ Supported media types are: ] ``` +#### [workflows-yaml-1](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/parse/parsers/workflows-yaml-1) + +Wraps [@swagger-api/apidom-parser-adapter-workflows-yaml-1](https://github.com/swagger-api/apidom/tree/main/packages/apidom-parser-adapter-workflows-yaml-1) package +and is uniquely identified by `workflows-yaml-1` name. + +Supported media types are: + +```js +[ + 'application/vnd.oai.workflows;version=1.0.0', + 'application/vnd.oai.workflows+yaml;version=1.0.0', +] +``` + #### [api-design-systems-json](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/parse/parsers/api-design-systems-json) Wraps [@swagger-api/apidom-parser-adapter-api-design-systsems-json](https://github.com/swagger-api/apidom/tree/main/packages/apidom-parser-adapter-api-design-systems-json) package @@ -353,6 +367,7 @@ returns `true` or until entire list of parser plugins is exhausted (throws error AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), WorkflowsJson1Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), JsonParser({ allowEmpty: true, sourceMap: false }), @@ -374,7 +389,8 @@ import OpenApiJson3_1Parser from '@swagger-api/apidom-reference/parse/parsers/op import OpenApiYaml3_1Parser from '@swagger-api/apidom-reference/parse/parsers/openapi-yaml-3-1' import AsyncApiJson2Parser from '@swagger-api/apidom-reference/parse/parsers/asyncapi-json-2'; import AsyncApiYaml2Parser from '@swagger-api/apidom-reference/parse/parsers/asyncapi-yaml-2'; -import WorkflowsJson1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-json-1'; +import WorkflowsJson1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-json-1'; +import WorkflowsYaml1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-yaml-1'; import ApiDesignSystemsJsonParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json'; import ApiDesignSystemsYamlParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json'; import JsonParser from '@swagger-api/apidom-reference/parse/parsers/json'; @@ -392,6 +408,7 @@ options.parse.parsers = [ AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), WorkflowsJson1Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), YamlParser({ allowEmpty: true, sourceMap: false }), @@ -412,7 +429,8 @@ import OpenApiJson3_1Parser from '@swagger-api/apidom-reference/parse/parsers/op import OpenApiYaml3_1Parser from '@swagger-api/apidom-reference/parse/parsers/openapi-yaml-3-1' import AsyncApiJson2Parser from '@swagger-api/apidom-reference/parse/parsers/asyncapi-json-2'; import AsyncApiYaml2Parser from '@swagger-api/apidom-reference/parse/parsers/asyncapi-yaml-2'; -import WorkflowsJson1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-json-2'; +import WorkflowsJson1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-json-1'; +import WorkflowsYaml1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-yaml-1'; import ApiDesignSystemsJsonParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json'; import ApiDesignSystemsYamlParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json'; import JsonParser from '@swagger-api/apidom-reference/parse/parsers/json'; @@ -432,6 +450,7 @@ await parse('/home/user/oas.json', { AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), WorkflowsJson1Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), YamlParser({ allowEmpty: true, sourceMap: false }), diff --git a/packages/apidom-reference/package.json b/packages/apidom-reference/package.json index 9435585c01..c475d4e91d 100644 --- a/packages/apidom-reference/package.json +++ b/packages/apidom-reference/package.json @@ -97,7 +97,12 @@ "import": "./es/parse/parsers/workflows-json-1/index.mjs", "require": "./cjs/parse/parsers/workflows-json-1/index.cjs", "types": "./types/parse/parsers/workflows-json-1/index.d.ts" - }, + }, + "./parse/parsers/workflows-yaml-1": { + "import": "./es/parse/parsers/workflows-yaml-1/index.mjs", + "require": "./cjs/parse/parsers/workflows-yaml-1/index.cjs", + "types": "./types/parse/parsers/workflows-yaml-1/index.d.ts" + }, "./parse/parsers/binary": { "browser": { "import": "./es/parse/parsers/binary/index-browser.mjs", @@ -261,6 +266,8 @@ "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^0.89.0", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^0.89.0", "@swagger-api/apidom-ns-workflows-1": "^0.89.0", + "@swagger-api/apidom-parser-adapter-workflows-json-1": "^0.89.0", + "@swagger-api/apidom-parser-adapter-workflows-yaml-1": "^0.89.0", "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0" }, "devDependencies": { @@ -282,6 +289,8 @@ "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "*", "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "*", "@swagger-api/apidom-ns-workflows-1": "*", + "@swagger-api/apidom-parser-adapter-workflows-json-1": "*", + "@swagger-api/apidom-parser-adapter-workflows-yaml-1": "*", "@swagger-api/apidom-parser-adapter-yaml-1-2": "*", "axios-mock-adapter": "^1.21.4" }, diff --git a/packages/apidom-reference/src/configuration/saturated.ts b/packages/apidom-reference/src/configuration/saturated.ts index 633326d48a..e839e41544 100644 --- a/packages/apidom-reference/src/configuration/saturated.ts +++ b/packages/apidom-reference/src/configuration/saturated.ts @@ -15,6 +15,7 @@ import OpenApiYaml3_1Parser from '../parse/parsers/openapi-yaml-3-1'; import AsyncApiJson2Parser from '../parse/parsers/asyncapi-json-2'; import AsyncApiYaml2Parser from '../parse/parsers/asyncapi-yaml-2'; import WorkflowsJson1Parser from '../parse/parsers/workflows-json-1'; +import WorkflowsYaml1Parser from '../parse/parsers/workflows-yaml-1'; import JsonParser from '../parse/parsers/json'; import YamlParser from '../parse/parsers/yaml-1-2'; import BinaryParser from '../parse/parsers/binary/index-node'; @@ -35,6 +36,7 @@ options.parse.parsers = [ AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), WorkflowsJson1Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), JsonParser({ allowEmpty: true, sourceMap: false }), diff --git a/packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts b/packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts new file mode 100644 index 0000000000..4db0f9467d --- /dev/null +++ b/packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts @@ -0,0 +1,43 @@ +import stampit from 'stampit'; +import { pick } from 'ramda'; +import { ParseResultElement } from '@swagger-api/apidom-core'; +import { parse, mediaTypes, detect } from '@swagger-api/apidom-parser-adapter-workflows-yaml-1'; + +import ParserError from '../../../errors/ParserError'; +import { File as IFile, Parser as IParser } from '../../../types'; +import Parser from '../Parser'; + +const WorkflowsYaml1Parser: stampit.Stamp = stampit(Parser, { + props: { + name: 'workflows-yaml-1', + fileExtensions: ['.yaml', '.yml'], + mediaTypes, + }, + methods: { + async canParse(file: IFile): Promise { + const hasSupportedFileExtension = + this.fileExtensions.length === 0 ? true : this.fileExtensions.includes(file.extension); + const hasSupportedMediaType = this.mediaTypes.includes(file.mediaType); + + if (!hasSupportedFileExtension) return false; + if (hasSupportedMediaType) return true; + if (!hasSupportedMediaType) { + return detect(file.toString()); + } + return false; + }, + async parse(file: IFile): Promise { + const source = file.toString(); + + try { + const parserOpts = pick(['sourceMap', 'refractorOpts'], this); + return await parse(source, parserOpts); + } catch (error: any) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw new ParserError(`Error parsing "${file.uri}"`, { cause: error }); + } + }, + }, +}); + +export default WorkflowsYaml1Parser; diff --git a/packages/apidom-reference/test/parse/parsers/workflows-yaml-1/fixtures/sample-workflow.yaml b/packages/apidom-reference/test/parse/parsers/workflows-yaml-1/fixtures/sample-workflow.yaml new file mode 100644 index 0000000000..edc47f8cf6 --- /dev/null +++ b/packages/apidom-reference/test/parse/parsers/workflows-yaml-1/fixtures/sample-workflow.yaml @@ -0,0 +1,61 @@ +workflowsSpec: 1.0.0 +info: + title: A pet purchasing workflow + summary: This workflow showcases how to purchase a pet through a sequence of API calls + description: | + This workflow walks you through the steps of `searching` for, `selecting`, and `purchasing` an available pet. + version: 1.0.1 +sourceDescriptions: +- name: petStoreDescription + url: https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml + type: openapi + +workflows: +- workflowId: loginUserRetrievePet + summary: Login User and then retrieve pets + description: This procedure lays out the steps to login a user and then retrieve pets + inputs: + type: object + properties: + username: + type: string + password: + type: string + steps: + - stepId: loginStep + description: This step demonstrates the user login step + operationId: petStoreDescription.loginUser + parameters: + # parameters to inject into the loginUser operation (parameter name must be resolvable at the referenced operation and the value is determined using {expression} syntax) + - name: username + in: query + value: $inputs.username + - name: password + in: query + value: $inputs.password + successCriteria: + # assertions to determine step was successful + - condition: $statusCode == 200 + outputs: + # outputs from this step + tokenExpires: $response.header.X-Expires-After + rateLimit: $response.header.X-Rate-Limit + sessionToken: $response.body + - stepId: getPetStep + description: retrieve a pet by status from the GET pets endpoint + operationRef: https://petstore3.swagger.io/api/v3/openapi.json#/paths/users/~findbystatus~1{status}/get + dependsOn: loginStep + parameters: + - name: status + in: query + value: 'available' + - name: Authorization + in: header + value: $steps.loginUser.outputs.sessionToken + successCriteria: + - condition: $statusCode == 200 + outputs: + # outputs from this step + availablePets: $response.body + outputs: + available: $steps.getPetStep.availablePets \ No newline at end of file diff --git a/packages/apidom-reference/test/parse/parsers/workflows-yaml-1/index.ts b/packages/apidom-reference/test/parse/parsers/workflows-yaml-1/index.ts new file mode 100644 index 0000000000..9275909a26 --- /dev/null +++ b/packages/apidom-reference/test/parse/parsers/workflows-yaml-1/index.ts @@ -0,0 +1,223 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { assert } from 'chai'; +import { NumberElement, isParseResultElement, isSourceMapElement } from '@swagger-api/apidom-core'; +import { mediaTypes } from '@swagger-api/apidom-parser-adapter-workflows-yaml-1'; + +import File from '../../../../src/util/File'; +import WorkflowsYaml1Parser from '../../../../src/parse/parsers/workflows-yaml-1'; + +describe('parsers', function () { + context('WorkflowsYaml1Parser', function () { + context('canParse', function () { + context('given file with .yaml extension', function () { + context('and with proper media type', function () { + specify('should return true', async function () { + const file1 = File({ + uri: '/path/to/workflows.yaml', + mediaType: mediaTypes.latest('yaml'), + }); + const file2 = File({ + uri: '/path/to/worklfows.yaml', + mediaType: mediaTypes.latest('generic'), + }); + const parser = WorkflowsYaml1Parser(); + + assert.isTrue(await parser.canParse(file1)); + assert.isTrue(await parser.canParse(file2)); + }); + }); + + context('and with improper media type', function () { + specify('should return false', async function () { + const file = File({ + uri: '/path/to/workflows.yaml', + mediaType: 'application/vnd.aai.asyncapi;version=2.6.0', + }); + const parser = WorkflowsYaml1Parser(); + + assert.isFalse(await parser.canParse(file)); + }); + }); + }); + + context('given file with .yml extension', function () { + context('and with proper media type', function () { + specify('should return true', async function () { + const file1 = File({ + uri: '/path/to/workflows.yml', + mediaType: mediaTypes.latest('yaml'), + }); + const file2 = File({ + uri: '/path/to/workflows.yml', + mediaType: mediaTypes.latest('generic'), + }); + const parser = WorkflowsYaml1Parser(); + + assert.isTrue(await parser.canParse(file1)); + assert.isTrue(await parser.canParse(file2)); + }); + }); + + context('and with improper media type', function () { + specify('should return false', async function () { + const file = File({ + uri: '/path/to/workflows.yaml', + mediaType: 'application/vnd.aai.asyncapi;version=2.6.0', + }); + const parser = WorkflowsYaml1Parser(); + + assert.isFalse(await parser.canParse(file)); + }); + }); + }); + + context('given file with unknown extension', function () { + specify('should return false', async function () { + const file = File({ + uri: '/path/to/workflows.txt', + mediaType: mediaTypes.latest('yaml'), + }); + const parser = WorkflowsYaml1Parser(); + + assert.isFalse(await parser.canParse(file)); + }); + }); + + context('given file with no extension', function () { + specify('should return false', async function () { + const file = File({ + uri: '/path/to/worklfows', + mediaType: mediaTypes.latest('yaml'), + }); + const parser = WorkflowsYaml1Parser(); + + assert.isFalse(await parser.canParse(file)); + }); + }); + + context('given file with supported extension', function () { + context('and file data is buffer and can be detected as Workflows 1.0.0', function () { + specify('should return true', async function () { + const url = path.join(__dirname, 'fixtures', 'sample-workflow.yaml'); + const file = File({ + uri: '/path/to/workflows.yaml', + data: fs.readFileSync(url), + }); + const parser = WorkflowsYaml1Parser(); + + assert.isTrue(await parser.canParse(file)); + }); + }); + + context('and file data is string and can be detected as Workflows 1.0.0', function () { + specify('should return true', async function () { + const url = path.join(__dirname, 'fixtures', 'sample-workflow.yaml'); + const file = File({ + uri: '/path/to/workflows.yaml', + data: fs.readFileSync(url).toString(), + }); + const parser = WorkflowsYaml1Parser(); + + assert.isTrue(await parser.canParse(file)); + }); + }); + }); + }); + + context('parse', function () { + context('given Workflows 1.0.0 YAML data', function () { + specify('should return parse result', async function () { + const uri = path.join(__dirname, 'fixtures', 'sample-workflow.yaml'); + const data = fs.readFileSync(uri).toString(); + const file = File({ + uri, + data, + mediaType: mediaTypes.latest('yaml'), + }); + const parser = WorkflowsYaml1Parser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + }); + }); + + context('given Workflows 1.0.0 YAML data as buffer', function () { + specify('should return parse result', async function () { + const uri = path.join(__dirname, 'fixtures', 'sample-workflow.yaml'); + const data = fs.readFileSync(uri); + const file = File({ + uri, + data, + mediaType: mediaTypes.latest('yaml'), + }); + const parser = WorkflowsYaml1Parser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + }); + }); + + context('given data that is not an Workflows YAML data', function () { + specify('should coerce to string and parse', async function () { + const file = File({ + uri: '/path/to/file.yaml', + data: 1, + mediaType: mediaTypes.latest('yaml'), + }); + const parser = WorkflowsYaml1Parser(); + const result = await parser.parse(file); + const numberElement: NumberElement = result.get(0); + + assert.isTrue(isParseResultElement(result)); + assert.isTrue(numberElement.equals(1)); + }); + }); + + context('given empty file', function () { + specify('should return empty parse result', async function () { + const file = File({ + uri: '/path/to/file.yaml', + data: '', + mediaType: mediaTypes.latest('yaml'), + }); + const parser = WorkflowsYaml1Parser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + assert.isTrue(parseResult.isEmpty); + }); + }); + + context('sourceMap', function () { + context('given sourceMap enabled', function () { + specify('should decorate ApiDOM with source maps', async function () { + const uri = path.join(__dirname, 'fixtures', 'sample-workflow.yaml'); + const data = fs.readFileSync(uri).toString(); + const file = File({ + uri, + data, + mediaType: mediaTypes.latest('yaml'), + }); + const parser = WorkflowsYaml1Parser({ sourceMap: true }); + const parseResult = await parser.parse(file); + + assert.isTrue(isSourceMapElement(parseResult.api?.meta.get('sourceMap'))); + }); + }); + + context('given sourceMap disabled', function () { + specify('should not decorate ApiDOM with source maps', async function () { + const uri = path.join(__dirname, 'fixtures', 'sample-workflow.yaml'); + const data = fs.readFileSync(uri).toString(); + const file = File({ uri, data }); + const parser = WorkflowsYaml1Parser({ sourceMap: false }); + const parseResult = await parser.parse(file); + + assert.isUndefined(parseResult.api?.meta.get('sourceMap')); + }); + }); + }); + }); + }); +}); From 79fac59850d2b5f5176e90002850f88090e0c213 Mon Sep 17 00:00:00 2001 From: Frank Kilcommins Date: Fri, 22 Dec 2023 13:14:46 +0000 Subject: [PATCH 2/2] chore: remove lint annotation --- .../apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts b/packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts index 4db0f9467d..c0b922ba53 100644 --- a/packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts +++ b/packages/apidom-reference/src/parse/parsers/workflows-yaml-1/index.ts @@ -33,7 +33,6 @@ const WorkflowsYaml1Parser: stampit.Stamp = stampit(Parser, { const parserOpts = pick(['sourceMap', 'refractorOpts'], this); return await parse(source, parserOpts); } catch (error: any) { - // eslint-disable-next-line @typescript-eslint/no-throw-literal throw new ParserError(`Error parsing "${file.uri}"`, { cause: error }); } },