From 6b970512e0ac9f688f1de748f40c8691c53c68e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Tue, 5 Mar 2024 09:30:35 +0100 Subject: [PATCH] feat(reference): add parser plugin for parsing dehydrated ApiDOM (#3892) Refs #3889 --- packages/apidom-reference/README.md | 26 ++- packages/apidom-reference/package.json | 5 + .../src/configuration/saturated.ts | 2 + .../src/parse/parsers/apidom-json/index.ts | 64 +++++++ .../parsers/apidom-json/fixtures/apidom.json | 12 ++ .../test/parse/parsers/apidom-json/index.ts | 163 ++++++++++++++++++ 6 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 packages/apidom-reference/src/parse/parsers/apidom-json/index.ts create mode 100644 packages/apidom-reference/test/parse/parsers/apidom-json/fixtures/apidom.json create mode 100644 packages/apidom-reference/test/parse/parsers/apidom-json/index.ts diff --git a/packages/apidom-reference/README.md b/packages/apidom-reference/README.md index ec8f4dbbf..ccc456721 100644 --- a/packages/apidom-reference/README.md +++ b/packages/apidom-reference/README.md @@ -101,6 +101,20 @@ so providing it is always a better option. Parse component comes with number of default parser plugins. +#### [apidom-json](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/parse/parsers/apidom-json) + +Parses dehydrated ApiDOM structure and hydrates it. +This parser plugin is uniquely identified by `apidom-json` name. + +Supported media types are: + +```js +[ + 'application/vnd.apidom', + 'application/vnd.apidom+json', +] +``` + #### [openapi-json-2](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/parse/parsers/openapi-json-2) Wraps [@swagger-api/apidom-parser-adapter-openapi-json-2](https://github.com/swagger-api/apidom/tree/main/packages/apidom-parser-adapter-openapi-json-2) package @@ -361,7 +375,6 @@ returns `true` or until entire list of parser plugins is exhausted (throws error OpenApiYaml2Parser({ allowEmpty: true, sourceMap: false }), OpenApiJson3_0Parser({ allowEmpty: true, sourceMap: false }), OpenApiYaml3_0Parser({ allowEmpty: true, sourceMap: false }), - OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }), OpenApiJson3_1Parser({ allowEmpty: true, sourceMap: false }), OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }), AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }), @@ -370,6 +383,7 @@ returns `true` or until entire list of parser plugins is exhausted (throws error WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), + ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }), JsonParser({ allowEmpty: true, sourceMap: false }), YamlParser({ allowEmpty: true, sourceMap: false }), BinaryParser({ allowEmpty: true }), @@ -391,6 +405,7 @@ import AsyncApiJson2Parser from '@swagger-api/apidom-reference/parse/parsers/asy 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 WorkflowsYaml1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-yaml-1'; +import ApiDOMJsonParser from '@swagger-api/apidom-reference/parse/parsers/apidom-json'; 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'; @@ -411,6 +426,7 @@ options.parse.parsers = [ WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), + ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }), YamlParser({ allowEmpty: true, sourceMap: false }), JsonParser({ allowEmpty: true, sourceMap: false }), BinaryParser({ allowEmpty: true }), @@ -431,6 +447,7 @@ import AsyncApiJson2Parser from '@swagger-api/apidom-reference/parse/parsers/asy 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 WorkflowsYaml1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-yaml-1'; +import ApiDOMJsonParser from '@swagger-api/apidom-reference/parse/parsers/apidom-json'; 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'; @@ -443,18 +460,19 @@ await parse('/home/user/oas.json', { parsers: [ OpenApiJson2Parser({ allowEmpty: true, sourceMap: false }), OpenApiYaml2Parser({ allowEmpty: true, sourceMap: false }), - OpenApiJson3_1Parser({ allowEmpty: true, sourceMap: false }), - OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }), OpenApiJson3_0Parser({ allowEmpty: true, sourceMap: false }), OpenApiYaml3_0Parser({ allowEmpty: true, sourceMap: false }), + OpenApiJson3_1Parser({ allowEmpty: true, sourceMap: false }), + OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }), 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 }), + ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }), JsonParser({ allowEmpty: true, sourceMap: false }), + YamlParser({ allowEmpty: true, sourceMap: false }), BinaryParser({ allowEmpty: true }), ], }, diff --git a/packages/apidom-reference/package.json b/packages/apidom-reference/package.json index 84f6cdd86..165963d6c 100644 --- a/packages/apidom-reference/package.json +++ b/packages/apidom-reference/package.json @@ -103,6 +103,11 @@ "require": "./cjs/parse/parsers/workflows-yaml-1/index.cjs", "types": "./types/parse/parsers/workflows-yaml-1/index.d.ts" }, + "./parse/parsers/apidom-json": { + "import": "./es/parse/parsers/apidom-json/index.mjs", + "require": "./cjs/parse/parsers/apidom-json/index.cjs", + "types": "./types/parse/parsers/apidom-json/index.d.ts" + }, "./parse/parsers/binary": { "browser": { "import": "./es/parse/parsers/binary/index-browser.mjs", diff --git a/packages/apidom-reference/src/configuration/saturated.ts b/packages/apidom-reference/src/configuration/saturated.ts index 2746755f4..a961ba2dc 100644 --- a/packages/apidom-reference/src/configuration/saturated.ts +++ b/packages/apidom-reference/src/configuration/saturated.ts @@ -16,6 +16,7 @@ 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 ApiDOMJsonParser from '../parse/parsers/apidom-json'; import JsonParser from '../parse/parsers/json'; import YamlParser from '../parse/parsers/yaml-1-2'; import BinaryParser from '../parse/parsers/binary/index-node'; @@ -40,6 +41,7 @@ options.parse.parsers = [ WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }), ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }), + ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }), JsonParser({ allowEmpty: true, sourceMap: false }), YamlParser({ allowEmpty: true, sourceMap: false }), BinaryParser({ allowEmpty: true }), diff --git a/packages/apidom-reference/src/parse/parsers/apidom-json/index.ts b/packages/apidom-reference/src/parse/parsers/apidom-json/index.ts new file mode 100644 index 000000000..4ed4857ac --- /dev/null +++ b/packages/apidom-reference/src/parse/parsers/apidom-json/index.ts @@ -0,0 +1,64 @@ +import stampit from 'stampit'; +import { + ParseResultElement, + isParseResultElement, + namespace as baseNamespace, +} from '@swagger-api/apidom-core'; + +import ParserError from '../../../errors/ParserError'; +import { Parser as IParser, File as IFile } from '../../../types'; +import Parser from '../Parser'; + +const ApiDOMJsonParser: stampit.Stamp = stampit(Parser, { + props: { + name: 'apidom-json', + fileExtensions: ['.json'], + mediaTypes: ['application/vnd.apidom', 'application/vnd.apidom+json'], + namespace: baseNamespace, + }, + init({ namespace } = {}) { + this.namespace = namespace ?? this.namespace; + }, + methods: { + canParse(file: IFile): boolean { + 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) { + try { + return this.namespace.fromRefract(JSON.parse(file.toString())) && true; + } catch { + return false; + } + } + return false; + }, + parse(file: IFile): ParseResultElement { + const source = file.toString(); + const namespace = this['apidom-json']?.namespace ?? this.namespace; + + // allow empty files + if (this.allowEmpty && source.trim() === '') { + return new ParseResultElement(); + } + + try { + const element = namespace.fromRefract(JSON.parse(source)); + + if (!isParseResultElement(element)) { + element.classes.push('result'); + return new ParseResultElement([element]); + } + + return element; + } catch (error: unknown) { + throw new ParserError(`Error parsing "${file.uri}"`, { cause: error }); + } + }, + }, +}); + +export default ApiDOMJsonParser; diff --git a/packages/apidom-reference/test/parse/parsers/apidom-json/fixtures/apidom.json b/packages/apidom-reference/test/parse/parsers/apidom-json/fixtures/apidom.json new file mode 100644 index 000000000..df1187ff7 --- /dev/null +++ b/packages/apidom-reference/test/parse/parsers/apidom-json/fixtures/apidom.json @@ -0,0 +1,12 @@ +{ + "element": "object", + "content": [ + { + "element": "member", + "content": { + "key": { "element": "string", "content": "a" }, + "value": { "element": "string", "content": "b" } + } + } + ] +} diff --git a/packages/apidom-reference/test/parse/parsers/apidom-json/index.ts b/packages/apidom-reference/test/parse/parsers/apidom-json/index.ts new file mode 100644 index 000000000..11a6be951 --- /dev/null +++ b/packages/apidom-reference/test/parse/parsers/apidom-json/index.ts @@ -0,0 +1,163 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { assert } from 'chai'; +import { isParseResultElement } from '@swagger-api/apidom-core'; + +import { ParserError, File } from '../../../../src'; +import ApiDOMJsonParser from '../../../../src/parse/parsers/apidom-json'; + +describe('parsers', function () { + context('ApiDOMJsonParser', 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/apidom.json', + mediaType: 'application/vnd.apidom', + }); + const file2 = File({ + uri: '/path/to/apidom.json', + mediaType: 'application/vnd.apidom+json', + }); + const parser = ApiDOMJsonParser(); + + 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/apidom.json', + mediaType: 'application/vnd.aai.asyncapi+json;version=2.6.0', + }); + const parser = ApiDOMJsonParser(); + + 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/apidom.yaml', + mediaType: 'application/vnd.apidom', + }); + const parser = ApiDOMJsonParser(); + + 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/apidom', + mediaType: 'application/vnd.apidom', + }); + const parser = ApiDOMJsonParser(); + + assert.isFalse(await parser.canParse(file)); + }); + }); + + context('given file with supported extension', function () { + context('and file data is buffer and can be detected as ApiDOM', function () { + specify('should return true', async function () { + const url = path.join(__dirname, 'fixtures', 'apidom.json'); + const file = File({ + uri: '/path/to/apidom.json', + data: fs.readFileSync(url), + }); + const parser = ApiDOMJsonParser(); + + assert.isTrue(await parser.canParse(file)); + }); + }); + + context('and file data is string and can be detected as ApiDOM', function () { + specify('should return true', async function () { + const url = path.join(__dirname, 'fixtures', 'apidom.json'); + const file = File({ + uri: '/path/to/apidom.json', + data: fs.readFileSync(url).toString(), + }); + const parser = ApiDOMJsonParser(); + + assert.isTrue(await parser.canParse(file)); + }); + }); + }); + }); + + context('parse', function () { + context('given ApiDOM JSON data', function () { + specify('should return parse result', async function () { + const uri = path.join(__dirname, 'fixtures', 'apidom.json'); + const data = fs.readFileSync(uri).toString(); + const file = File({ + uri, + data, + mediaType: 'application/vnd.apidom+json', + }); + const parser = ApiDOMJsonParser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + }); + }); + + context('given ApiDOM JSON data as buffer', function () { + specify('should return parse result', async function () { + const uri = path.join(__dirname, 'fixtures', 'apidom.json'); + const data = fs.readFileSync(uri); + const file = File({ + uri, + data, + mediaType: 'application/vnd.apidom+json', + }); + const parser = ApiDOMJsonParser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + }); + }); + + context('given data that is not an ApiDOM JSON data', function () { + specify('should throw error', async function () { + const file = File({ + uri: '/path/to/file.json', + data: 1, + mediaType: 'application/vnd.apidom+json', + }); + const parser = ApiDOMJsonParser(); + + try { + await parser.parse(file); + assert.fail('Should throw ParserError'); + } catch (e) { + assert.instanceOf(e, ParserError); + } + }); + }); + + context('given empty file', function () { + specify('should return empty parse result', async function () { + const file = File({ + uri: '/path/to/file.json', + data: '', + mediaType: 'application/vnd.apidom+json', + }); + const parser = ApiDOMJsonParser(); + const parseResult = await parser.parse(file); + + assert.isTrue(isParseResultElement(parseResult)); + assert.isTrue(parseResult.isEmpty); + }); + }); + }); + }); +});