Skip to content

Commit 6b97051

Browse files
authored
feat(reference): add parser plugin for parsing dehydrated ApiDOM (#3892)
Refs #3889
1 parent 3246c9f commit 6b97051

File tree

6 files changed

+268
-4
lines changed

6 files changed

+268
-4
lines changed

packages/apidom-reference/README.md

+22-4
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ so providing it is always a better option.
101101

102102
Parse component comes with number of default parser plugins.
103103

104+
#### [apidom-json](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/parse/parsers/apidom-json)
105+
106+
Parses dehydrated ApiDOM structure and hydrates it.
107+
This parser plugin is uniquely identified by `apidom-json` name.
108+
109+
Supported media types are:
110+
111+
```js
112+
[
113+
'application/vnd.apidom',
114+
'application/vnd.apidom+json',
115+
]
116+
```
117+
104118
#### [openapi-json-2](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/parse/parsers/openapi-json-2)
105119

106120
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
361375
OpenApiYaml2Parser({ allowEmpty: true, sourceMap: false }),
362376
OpenApiJson3_0Parser({ allowEmpty: true, sourceMap: false }),
363377
OpenApiYaml3_0Parser({ allowEmpty: true, sourceMap: false }),
364-
OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }),
365378
OpenApiJson3_1Parser({ allowEmpty: true, sourceMap: false }),
366379
OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }),
367380
AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }),
@@ -370,6 +383,7 @@ returns `true` or until entire list of parser plugins is exhausted (throws error
370383
WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }),
371384
ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }),
372385
ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }),
386+
ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }),
373387
JsonParser({ allowEmpty: true, sourceMap: false }),
374388
YamlParser({ allowEmpty: true, sourceMap: false }),
375389
BinaryParser({ allowEmpty: true }),
@@ -391,6 +405,7 @@ import AsyncApiJson2Parser from '@swagger-api/apidom-reference/parse/parsers/asy
391405
import AsyncApiYaml2Parser from '@swagger-api/apidom-reference/parse/parsers/asyncapi-yaml-2';
392406
import WorkflowsJson1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-json-1';
393407
import WorkflowsYaml1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-yaml-1';
408+
import ApiDOMJsonParser from '@swagger-api/apidom-reference/parse/parsers/apidom-json';
394409
import ApiDesignSystemsJsonParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json';
395410
import ApiDesignSystemsYamlParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json';
396411
import JsonParser from '@swagger-api/apidom-reference/parse/parsers/json';
@@ -411,6 +426,7 @@ options.parse.parsers = [
411426
WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }),
412427
ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }),
413428
ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }),
429+
ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }),
414430
YamlParser({ allowEmpty: true, sourceMap: false }),
415431
JsonParser({ allowEmpty: true, sourceMap: false }),
416432
BinaryParser({ allowEmpty: true }),
@@ -431,6 +447,7 @@ import AsyncApiJson2Parser from '@swagger-api/apidom-reference/parse/parsers/asy
431447
import AsyncApiYaml2Parser from '@swagger-api/apidom-reference/parse/parsers/asyncapi-yaml-2';
432448
import WorkflowsJson1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-json-1';
433449
import WorkflowsYaml1Parser from '@swagger-api/apidom-reference/parse/parsers/workflows-yaml-1';
450+
import ApiDOMJsonParser from '@swagger-api/apidom-reference/parse/parsers/apidom-json';
434451
import ApiDesignSystemsJsonParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json';
435452
import ApiDesignSystemsYamlParser from '@swagger-api/apidom-reference/parse/parsers/api-design-systems-json';
436453
import JsonParser from '@swagger-api/apidom-reference/parse/parsers/json';
@@ -443,18 +460,19 @@ await parse('/home/user/oas.json', {
443460
parsers: [
444461
OpenApiJson2Parser({ allowEmpty: true, sourceMap: false }),
445462
OpenApiYaml2Parser({ allowEmpty: true, sourceMap: false }),
446-
OpenApiJson3_1Parser({ allowEmpty: true, sourceMap: false }),
447-
OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }),
448463
OpenApiJson3_0Parser({ allowEmpty: true, sourceMap: false }),
449464
OpenApiYaml3_0Parser({ allowEmpty: true, sourceMap: false }),
465+
OpenApiJson3_1Parser({ allowEmpty: true, sourceMap: false }),
466+
OpenApiYaml3_1Parser({ allowEmpty: true, sourceMap: false }),
450467
AsyncApiJson2Parser({ allowEmpty: true, sourceMap: false }),
451468
AsyncApiYaml2Parser({ allowEmpty: true, sourceMap: false }),
452469
WorkflowsJson1Parser({ allowEmpty: true, sourceMap: false }),
453470
WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }),
454471
ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }),
455472
ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }),
456-
YamlParser({ allowEmpty: true, sourceMap: false }),
473+
ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }),
457474
JsonParser({ allowEmpty: true, sourceMap: false }),
475+
YamlParser({ allowEmpty: true, sourceMap: false }),
458476
BinaryParser({ allowEmpty: true }),
459477
],
460478
},

packages/apidom-reference/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@
103103
"require": "./cjs/parse/parsers/workflows-yaml-1/index.cjs",
104104
"types": "./types/parse/parsers/workflows-yaml-1/index.d.ts"
105105
},
106+
"./parse/parsers/apidom-json": {
107+
"import": "./es/parse/parsers/apidom-json/index.mjs",
108+
"require": "./cjs/parse/parsers/apidom-json/index.cjs",
109+
"types": "./types/parse/parsers/apidom-json/index.d.ts"
110+
},
106111
"./parse/parsers/binary": {
107112
"browser": {
108113
"import": "./es/parse/parsers/binary/index-browser.mjs",

packages/apidom-reference/src/configuration/saturated.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import AsyncApiJson2Parser from '../parse/parsers/asyncapi-json-2';
1616
import AsyncApiYaml2Parser from '../parse/parsers/asyncapi-yaml-2';
1717
import WorkflowsJson1Parser from '../parse/parsers/workflows-json-1';
1818
import WorkflowsYaml1Parser from '../parse/parsers/workflows-yaml-1';
19+
import ApiDOMJsonParser from '../parse/parsers/apidom-json';
1920
import JsonParser from '../parse/parsers/json';
2021
import YamlParser from '../parse/parsers/yaml-1-2';
2122
import BinaryParser from '../parse/parsers/binary/index-node';
@@ -40,6 +41,7 @@ options.parse.parsers = [
4041
WorkflowsYaml1Parser({ allowEmpty: true, sourceMap: false }),
4142
ApiDesignSystemsJsonParser({ allowEmpty: true, sourceMap: false }),
4243
ApiDesignSystemsYamlParser({ allowEmpty: true, sourceMap: false }),
44+
ApiDOMJsonParser({ allowEmpty: true, sourceMap: false }),
4345
JsonParser({ allowEmpty: true, sourceMap: false }),
4446
YamlParser({ allowEmpty: true, sourceMap: false }),
4547
BinaryParser({ allowEmpty: true }),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import stampit from 'stampit';
2+
import {
3+
ParseResultElement,
4+
isParseResultElement,
5+
namespace as baseNamespace,
6+
} from '@swagger-api/apidom-core';
7+
8+
import ParserError from '../../../errors/ParserError';
9+
import { Parser as IParser, File as IFile } from '../../../types';
10+
import Parser from '../Parser';
11+
12+
const ApiDOMJsonParser: stampit.Stamp<IParser> = stampit(Parser, {
13+
props: {
14+
name: 'apidom-json',
15+
fileExtensions: ['.json'],
16+
mediaTypes: ['application/vnd.apidom', 'application/vnd.apidom+json'],
17+
namespace: baseNamespace,
18+
},
19+
init({ namespace } = {}) {
20+
this.namespace = namespace ?? this.namespace;
21+
},
22+
methods: {
23+
canParse(file: IFile): boolean {
24+
const hasSupportedFileExtension =
25+
this.fileExtensions.length === 0 ? true : this.fileExtensions.includes(file.extension);
26+
const hasSupportedMediaType = this.mediaTypes.includes(file.mediaType);
27+
28+
if (!hasSupportedFileExtension) return false;
29+
if (hasSupportedMediaType) return true;
30+
if (!hasSupportedMediaType) {
31+
try {
32+
return this.namespace.fromRefract(JSON.parse(file.toString())) && true;
33+
} catch {
34+
return false;
35+
}
36+
}
37+
return false;
38+
},
39+
parse(file: IFile): ParseResultElement {
40+
const source = file.toString();
41+
const namespace = this['apidom-json']?.namespace ?? this.namespace;
42+
43+
// allow empty files
44+
if (this.allowEmpty && source.trim() === '') {
45+
return new ParseResultElement();
46+
}
47+
48+
try {
49+
const element = namespace.fromRefract(JSON.parse(source));
50+
51+
if (!isParseResultElement(element)) {
52+
element.classes.push('result');
53+
return new ParseResultElement([element]);
54+
}
55+
56+
return element;
57+
} catch (error: unknown) {
58+
throw new ParserError(`Error parsing "${file.uri}"`, { cause: error });
59+
}
60+
},
61+
},
62+
});
63+
64+
export default ApiDOMJsonParser;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"element": "object",
3+
"content": [
4+
{
5+
"element": "member",
6+
"content": {
7+
"key": { "element": "string", "content": "a" },
8+
"value": { "element": "string", "content": "b" }
9+
}
10+
}
11+
]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
import { assert } from 'chai';
4+
import { isParseResultElement } from '@swagger-api/apidom-core';
5+
6+
import { ParserError, File } from '../../../../src';
7+
import ApiDOMJsonParser from '../../../../src/parse/parsers/apidom-json';
8+
9+
describe('parsers', function () {
10+
context('ApiDOMJsonParser', function () {
11+
context('canParse', function () {
12+
context('given file with .json extension', function () {
13+
context('and with proper media type', function () {
14+
specify('should return true', async function () {
15+
const file1 = File({
16+
uri: '/path/to/apidom.json',
17+
mediaType: 'application/vnd.apidom',
18+
});
19+
const file2 = File({
20+
uri: '/path/to/apidom.json',
21+
mediaType: 'application/vnd.apidom+json',
22+
});
23+
const parser = ApiDOMJsonParser();
24+
25+
assert.isTrue(await parser.canParse(file1));
26+
assert.isTrue(await parser.canParse(file2));
27+
});
28+
});
29+
30+
context('and with improper media type', function () {
31+
specify('should return false', async function () {
32+
const file = File({
33+
uri: '/path/to/apidom.json',
34+
mediaType: 'application/vnd.aai.asyncapi+json;version=2.6.0',
35+
});
36+
const parser = ApiDOMJsonParser();
37+
38+
assert.isFalse(await parser.canParse(file));
39+
});
40+
});
41+
});
42+
43+
context('given file with unknown extension', function () {
44+
specify('should return false', async function () {
45+
const file = File({
46+
uri: '/path/to/apidom.yaml',
47+
mediaType: 'application/vnd.apidom',
48+
});
49+
const parser = ApiDOMJsonParser();
50+
51+
assert.isFalse(await parser.canParse(file));
52+
});
53+
});
54+
55+
context('given file with no extension', function () {
56+
specify('should return false', async function () {
57+
const file = File({
58+
uri: '/path/to/apidom',
59+
mediaType: 'application/vnd.apidom',
60+
});
61+
const parser = ApiDOMJsonParser();
62+
63+
assert.isFalse(await parser.canParse(file));
64+
});
65+
});
66+
67+
context('given file with supported extension', function () {
68+
context('and file data is buffer and can be detected as ApiDOM', function () {
69+
specify('should return true', async function () {
70+
const url = path.join(__dirname, 'fixtures', 'apidom.json');
71+
const file = File({
72+
uri: '/path/to/apidom.json',
73+
data: fs.readFileSync(url),
74+
});
75+
const parser = ApiDOMJsonParser();
76+
77+
assert.isTrue(await parser.canParse(file));
78+
});
79+
});
80+
81+
context('and file data is string and can be detected as ApiDOM', function () {
82+
specify('should return true', async function () {
83+
const url = path.join(__dirname, 'fixtures', 'apidom.json');
84+
const file = File({
85+
uri: '/path/to/apidom.json',
86+
data: fs.readFileSync(url).toString(),
87+
});
88+
const parser = ApiDOMJsonParser();
89+
90+
assert.isTrue(await parser.canParse(file));
91+
});
92+
});
93+
});
94+
});
95+
96+
context('parse', function () {
97+
context('given ApiDOM JSON data', function () {
98+
specify('should return parse result', async function () {
99+
const uri = path.join(__dirname, 'fixtures', 'apidom.json');
100+
const data = fs.readFileSync(uri).toString();
101+
const file = File({
102+
uri,
103+
data,
104+
mediaType: 'application/vnd.apidom+json',
105+
});
106+
const parser = ApiDOMJsonParser();
107+
const parseResult = await parser.parse(file);
108+
109+
assert.isTrue(isParseResultElement(parseResult));
110+
});
111+
});
112+
113+
context('given ApiDOM JSON data as buffer', function () {
114+
specify('should return parse result', async function () {
115+
const uri = path.join(__dirname, 'fixtures', 'apidom.json');
116+
const data = fs.readFileSync(uri);
117+
const file = File({
118+
uri,
119+
data,
120+
mediaType: 'application/vnd.apidom+json',
121+
});
122+
const parser = ApiDOMJsonParser();
123+
const parseResult = await parser.parse(file);
124+
125+
assert.isTrue(isParseResultElement(parseResult));
126+
});
127+
});
128+
129+
context('given data that is not an ApiDOM JSON data', function () {
130+
specify('should throw error', async function () {
131+
const file = File({
132+
uri: '/path/to/file.json',
133+
data: 1,
134+
mediaType: 'application/vnd.apidom+json',
135+
});
136+
const parser = ApiDOMJsonParser();
137+
138+
try {
139+
await parser.parse(file);
140+
assert.fail('Should throw ParserError');
141+
} catch (e) {
142+
assert.instanceOf(e, ParserError);
143+
}
144+
});
145+
});
146+
147+
context('given empty file', function () {
148+
specify('should return empty parse result', async function () {
149+
const file = File({
150+
uri: '/path/to/file.json',
151+
data: '',
152+
mediaType: 'application/vnd.apidom+json',
153+
});
154+
const parser = ApiDOMJsonParser();
155+
const parseResult = await parser.parse(file);
156+
157+
assert.isTrue(isParseResultElement(parseResult));
158+
assert.isTrue(parseResult.isEmpty);
159+
});
160+
});
161+
});
162+
});
163+
});

0 commit comments

Comments
 (0)