From 1b12b50f833cc3e55691d378ef94dd4fd87d7bee Mon Sep 17 00:00:00 2001 From: Frank Kilcommins Date: Thu, 21 Dec 2023 05:56:42 +0000 Subject: [PATCH] feat(reference): add Workflows 1.0.0 JSON parser plugin (#3568) Refs #3567 --- packages/apidom-reference/README.md | 19 ++ packages/apidom-reference/package.json | 7 + .../src/configuration/saturated.ts | 2 + .../parse/parsers/workflows-json-1/index.ts | 42 ++++ .../fixtures/sample-workflow.json | 92 ++++++++ .../parse/parsers/workflows-json-1/index.ts | 196 ++++++++++++++++++ 6 files changed, 358 insertions(+) create mode 100644 packages/apidom-reference/src/parse/parsers/workflows-json-1/index.ts create mode 100644 packages/apidom-reference/test/parse/parsers/workflows-json-1/fixtures/sample-workflow.json create mode 100644 packages/apidom-reference/test/parse/parsers/workflows-json-1/index.ts diff --git a/packages/apidom-reference/README.md b/packages/apidom-reference/README.md index 350cfb93c9..d9a120693f 100644 --- a/packages/apidom-reference/README.md +++ b/packages/apidom-reference/README.md @@ -251,6 +251,20 @@ Supported media types are: ] ``` +#### [workflows-json-1](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/parse/parsers/workflows-json-1) + +Wraps [@swagger-api/apidom-parser-adapter-workflows-json-1](https://github.com/swagger-api/apidom/tree/main/packages/apidom-parser-adapter-workflows-json-1) package +and is uniquely identified by `workflows-json-1` name. + +Supported media types are: + +```js +[ + 'application/vnd.oai.workflows;version=1.0.0', + 'application/vnd.oai.workflows+json;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 @@ -338,6 +352,7 @@ returns `true` or until entire list of parser plugins is exhausted (throws error OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }), AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsJson1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), JsonParser({ allowEmpty: true, sourceMap: false }), @@ -359,6 +374,7 @@ 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 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'; @@ -375,6 +391,7 @@ options.parse.parsers = [ OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }), AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsJson1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), YamlParser({ allowEmpty: true, sourceMap: false }), @@ -395,6 +412,7 @@ 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 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'; @@ -413,6 +431,7 @@ await parse('/home/user/oas.json', { OpenApiYaml3_0Parser({ allowEmpty: true, sourceMap: false }), AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsJson1Parser({ 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 852720d95f..9435585c01 100644 --- a/packages/apidom-reference/package.json +++ b/packages/apidom-reference/package.json @@ -93,6 +93,11 @@ "require": "./cjs/parse/parsers/asyncapi-yaml-2/index.cjs", "types": "./types/parse/parsers/asyncapi-yaml-2/index.d.ts" }, + "./parse/parsers/workflows-json-1": { + "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/binary": { "browser": { "import": "./es/parse/parsers/binary/index-browser.mjs", @@ -255,6 +260,7 @@ "@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-ns-workflows-1": "^0.89.0", "@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.89.0" }, "devDependencies": { @@ -275,6 +281,7 @@ "@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-ns-workflows-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 8f178b8a12..633326d48a 100644 --- a/packages/apidom-reference/src/configuration/saturated.ts +++ b/packages/apidom-reference/src/configuration/saturated.ts @@ -14,6 +14,7 @@ import OpenApiJson3_1Parser from '../parse/parsers/openapi-json-3-1'; 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 JsonParser from '../parse/parsers/json'; import YamlParser from '../parse/parsers/yaml-1-2'; import BinaryParser from '../parse/parsers/binary/index-node'; @@ -33,6 +34,7 @@ options.parse.parsers = [ OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }), AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }), + WorkflowsJson1Parser({ 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-json-1/index.ts b/packages/apidom-reference/src/parse/parsers/workflows-json-1/index.ts new file mode 100644 index 0000000000..3fc440b685 --- /dev/null +++ b/packages/apidom-reference/src/parse/parsers/workflows-json-1/index.ts @@ -0,0 +1,42 @@ +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-json-1'; + +import ParserError from '../../../errors/ParserError'; +import { File as IFile, Parser as IParser } from '../../../types'; +import Parser from '../Parser'; + +const WorkflowsJson1Parser: stampit.Stamp = stampit(Parser, { + props: { + name: 'workflows-json-1', + fileExtensions: ['.json'], + 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', 'syntacticAnalysis', 'refractorOpts'], this); + return await parse(source, parserOpts); + } catch (error: any) { + throw new ParserError(`Error parsing "${file.uri}"`, { cause: error }); + } + }, + }, +}); + +export default WorkflowsJson1Parser; diff --git a/packages/apidom-reference/test/parse/parsers/workflows-json-1/fixtures/sample-workflow.json b/packages/apidom-reference/test/parse/parsers/workflows-json-1/fixtures/sample-workflow.json new file mode 100644 index 0000000000..9e20ae9b4f --- /dev/null +++ b/packages/apidom-reference/test/parse/parsers/workflows-json-1/fixtures/sample-workflow.json @@ -0,0 +1,92 @@ +{ + "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.\n", + "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": [ + { + "name": "username", + "in": "query", + "value": "$inputs.username" + }, + { + "name": "password", + "in": "query", + "value": "$inputs.password" + } + ], + "successCriteria": [ + { + "condition": "$statusCode == 200" + } + ], + "outputs": { + "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": { + "availablePets": "$response.body" + } + } + ], + "outputs": { + "available": "$steps.getPetStep.availablePets" + } + } + ] + } \ No newline at end of file diff --git a/packages/apidom-reference/test/parse/parsers/workflows-json-1/index.ts b/packages/apidom-reference/test/parse/parsers/workflows-json-1/index.ts new file mode 100644 index 0000000000..98627c4692 --- /dev/null +++ b/packages/apidom-reference/test/parse/parsers/workflows-json-1/index.ts @@ -0,0 +1,196 @@ +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-json-1'; + +import File from '../../../../src/util/File'; +import WorkflowsJson1Parser from '../../../../src/parse/parsers/workflows-json-1'; + +describe('parsers', function () { + context('WorkflowsJson1Parser', function () { + context('canParse', function () { + context('given file with .json extension', function () { + context('and with proper media type', function () { + specify('should return true', async function () { + const file1 = File({ + uri: '/path/to/workflows.json', + mediaType: mediaTypes.latest('generic'), + }); + const file2 = File({ + uri: '/path/to/workflows.json', + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + + 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.json', + mediaType: 'application/vnd.aai.asyncapi+json;version=2.6.0', + }); + const parser = WorkflowsJson1Parser(); + + 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.yaml', + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + + 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/workflows', + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + + 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 uri = path.join(__dirname, 'fixtures', 'sample-workflow.json'); + const file = File({ + uri: '/path/to/workflows.json', + data: fs.readFileSync(uri), + }); + const parser = WorkflowsJson1Parser(); + + 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 uri = path.join(__dirname, 'fixtures', 'sample-workflow.json'); + const file = File({ + uri: '/path/to/workflows.json', + data: fs.readFileSync(uri).toString(), + }); + const parser = WorkflowsJson1Parser(); + + assert.isTrue(await parser.canParse(file)); + }); + }); + }); + }); + + context('parse', function () { + context('given Workflows 1.0.0 JSON data', function () { + specify('should return parse result', async function () { + const uri = path.join(__dirname, 'fixtures', 'sample-workflow.json'); + const data = fs.readFileSync(uri).toString(); + const file = File({ + uri, + data, + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + }); + }); + + context('given Workflows 1.0.0 JSON data as buffer', function () { + specify('should return parse result', async function () { + const uri = path.join(__dirname, 'fixtures', 'sample-workflow.json'); + const data = fs.readFileSync(uri); + const file = File({ + uri, + data, + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + }); + }); + + context('given data that is not a Workflows 1.0.0 JSON data', function () { + specify('should coerce to string and parse', async function () { + const file = File({ + uri: '/path/to/file.json', + data: 1, + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + const parseResult = await parser.parse(file); + const numberElement: NumberElement = parseResult.get(0); + + assert.isTrue(isParseResultElement(parseResult)); + 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.json', + data: '', + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + 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.json'); + const data = fs.readFileSync(uri).toString(); + const file = File({ + uri, + data, + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser({ 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.json'); + const data = fs.readFileSync(uri).toString(); + const file = File({ + uri, + data, + mediaType: mediaTypes.latest('json'), + }); + const parser = WorkflowsJson1Parser(); + const parseResult = await parser.parse(file); + + assert.isUndefined(parseResult.api?.meta.get('sourceMap')); + }); + }); + }); + }); + }); +});