Skip to content

Commit f882828

Browse files
authored
feat(converter): add complete code infrastructure (#3755)
Co-authored-by: Krzysztof Kowalczyk <[email protected]> Refs #3697 Closes #3743
1 parent 3ecbf56 commit f882828

File tree

31 files changed

+526
-40
lines changed

31 files changed

+526
-40
lines changed

package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/apidom-converter/config/webpack/browser.config.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,18 @@ const browser = {
1717
},
1818
resolve: {
1919
extensions: ['.ts', '.mjs', '.js', '.json'],
20+
fallback: {
21+
fs: false,
22+
path: false,
23+
},
2024
},
2125
module: {
2226
rules: [
27+
{
28+
test: /\.wasm$/,
29+
loader: 'file-loader',
30+
type: 'javascript/auto',
31+
},
2332
{
2433
test: /\.(ts|js)?$/,
2534
exclude: /node_modules/,
@@ -48,9 +57,18 @@ const browserMin = {
4857
},
4958
resolve: {
5059
extensions: ['.ts', '.mjs', '.js', '.json'],
60+
fallback: {
61+
fs: false,
62+
path: false,
63+
},
5164
},
5265
module: {
5366
rules: [
67+
{
68+
test: /\.wasm$/,
69+
loader: 'file-loader',
70+
type: 'javascript/auto',
71+
},
5472
{
5573
test: /\.(ts|js)?$/,
5674
exclude: /node_modules/,

packages/apidom-converter/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@
3838
"author": "Vladimír Gorej",
3939
"license": "Apache-2.0",
4040
"dependencies": {
41-
"@babel/runtime-corejs3": "^7.20.7"
41+
"@babel/runtime-corejs3": "^7.20.7",
42+
"@swagger-api/apidom-core": "^0.93.0",
43+
"@swagger-api/apidom-ns-openapi-3-0": "^0.93.0",
44+
"@swagger-api/apidom-ns-openapi-3-1": "^0.93.0",
45+
"@swagger-api/apidom-reference": "^0.93.0",
46+
"stampit": "^4.3.2"
4247
},
4348
"files": [
4449
"cjs/",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ApiDOMError } from '@swagger-api/apidom-error';
2+
3+
class ConvertError extends ApiDOMError {}
4+
5+
export default ConvertError;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import ConvertError from './ConvertError';
2+
3+
class UnmatchedConvertStrategyError extends ConvertError {}
4+
5+
export default UnmatchedConvertStrategyError;
Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1-
const foo = Symbol('foo');
1+
import { ParseResultElement } from '@swagger-api/apidom-core';
2+
import { mergeOptions, bundle, File } from '@swagger-api/apidom-reference';
23

3-
export default foo;
4+
import defaultOptions, { ConverterOptions } from './options';
5+
import ConvertError from './errors/ConvertError';
6+
import UnmatchedConvertStrategyError from './errors/UnmatchedConvertStrategyError';
7+
8+
export { ConvertError, UnmatchedConvertStrategyError };
9+
10+
/**
11+
* `convertApiDOM` already assumes that the ApiDOM is bundled.
12+
*/
13+
export const convertApiDOM = async (element: ParseResultElement, options = {}) => {
14+
const mergedOptions = mergeOptions(defaultOptions, options || {}) as ConverterOptions;
15+
const file = File({
16+
uri: mergedOptions.resolve.baseURI,
17+
parseResult: element,
18+
mediaType: mergedOptions.convert.sourceMediaType || mergedOptions.parse.mediaType,
19+
});
20+
const strategy = mergedOptions.convert.strategies.find((s) => s.canConvert(file, mergedOptions));
21+
22+
if (typeof strategy === 'undefined') {
23+
throw new UnmatchedConvertStrategyError(file.uri);
24+
}
25+
26+
return strategy.convert(file, mergedOptions);
27+
};
28+
29+
const convert = async (uri: string, options = {}) => {
30+
const mergedOptions = mergeOptions(defaultOptions, options || {}) as ConverterOptions;
31+
const parseResult = await bundle(uri, mergedOptions);
32+
33+
return convertApiDOM(parseResult, mergedOptions);
34+
};
35+
36+
export default convert;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { options as referenceOptions } from '@swagger-api/apidom-reference';
2+
3+
import ConvertStrategy from '../strategies/ConvertStrategy';
4+
import OpenAPI31ToOpenAPI30ConvertStrategy from '../strategies/openapi-3-1-to-openapi-3-0-3';
5+
6+
type ReferenceOptions = typeof referenceOptions;
7+
8+
interface ConvertOptions {
9+
strategies: Array<ConvertStrategy>;
10+
sourceMediaType: string;
11+
targetMediaType: string;
12+
}
13+
14+
export interface ConverterOptions extends ReferenceOptions {
15+
readonly convert: ConvertOptions;
16+
}
17+
18+
const defaultOptions: ConverterOptions = {
19+
...referenceOptions,
20+
convert: {
21+
/**
22+
* Determines strategies how ApiDOM is bundled.
23+
* Strategy is determined by media type or by inspecting ApiDOM to be bundled.
24+
*
25+
* You can add additional bundle strategies of your own, replace an existing one with
26+
* your own implementation, or remove any bundle strategy by removing it from the list.
27+
*/
28+
strategies: [new OpenAPI31ToOpenAPI30ConvertStrategy()],
29+
/**
30+
* Media type of source API definition.
31+
*/
32+
sourceMediaType: 'text/plain',
33+
/**
34+
* Media type of target API definition.
35+
*/
36+
targetMediaType: 'text/plain',
37+
},
38+
};
39+
40+
export default defaultOptions;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import stampit from 'stampit';
2+
import { ParseResultElement } from '@swagger-api/apidom-core';
3+
import { File } from '@swagger-api/apidom-reference';
4+
5+
import type { ConverterOptions } from '../options';
6+
7+
type ExtractGenericType<T> = T extends stampit.Stamp<infer U> ? U : never;
8+
export type IFile = ExtractGenericType<typeof File>;
9+
10+
export interface ConvertStrategyOptions {
11+
readonly name: string;
12+
}
13+
14+
abstract class ConvertStrategy {
15+
public readonly name: string;
16+
17+
protected constructor({ name }: ConvertStrategyOptions) {
18+
this.name = name;
19+
}
20+
21+
abstract canConvert(file: IFile, options: ConverterOptions): boolean;
22+
23+
abstract convert(file: IFile, options: ConverterOptions): Promise<ParseResultElement>;
24+
}
25+
26+
export default ConvertStrategy;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
OpenApi3_0Element,
3+
mediaTypes as openAPI3_0MediaTypes,
4+
} from '@swagger-api/apidom-ns-openapi-3-0';
5+
import {
6+
isOpenApi3_1Element,
7+
mediaTypes as openAPI3_1MediaTypes,
8+
createToolbox,
9+
keyMap,
10+
getNodeType,
11+
} from '@swagger-api/apidom-ns-openapi-3-1';
12+
import {
13+
ParseResultElement,
14+
dispatchRefractorPlugins,
15+
AnnotationElement,
16+
cloneShallow,
17+
} from '@swagger-api/apidom-core';
18+
19+
import ConvertStrategy, { IFile } from '../ConvertStrategy';
20+
import openAPIVersionRefractorPlugin from './refractor-plugins/openapi-version';
21+
import webhooksRefractorPlugin from './refractor-plugins/webhooks';
22+
import type { ConverterOptions } from '../../options';
23+
24+
// eslint-disable-next-line @typescript-eslint/naming-convention
25+
const openAPI3_0_3MediaTypes = [
26+
openAPI3_0MediaTypes.findBy('3.0.3', 'generic'),
27+
openAPI3_0MediaTypes.findBy('3.0.3', 'json'),
28+
openAPI3_0MediaTypes.findBy('3.0.3', 'yaml'),
29+
];
30+
31+
/* eslint-disable class-methods-use-this */
32+
class OpenAPI31ToOpenAPI30ConvertStrategy extends ConvertStrategy {
33+
constructor() {
34+
super({ name: 'openapi-3-1-to-openapi-3-0-3' });
35+
}
36+
37+
canConvert(file: IFile, options: ConverterOptions): boolean {
38+
let hasRecognizedSourceMediaType = false;
39+
const hasRecognizedTargetMediaType = openAPI3_0_3MediaTypes.includes(
40+
options.convert.targetMediaType,
41+
);
42+
43+
// source detection
44+
if (openAPI3_1MediaTypes.includes(options.convert.sourceMediaType)) {
45+
hasRecognizedSourceMediaType = true;
46+
} else if (file.mediaType !== 'text/plain') {
47+
hasRecognizedSourceMediaType = openAPI3_1MediaTypes.includes(file.mediaType);
48+
} else if (isOpenApi3_1Element(file.parseResult?.result)) {
49+
hasRecognizedSourceMediaType = true;
50+
}
51+
52+
return hasRecognizedSourceMediaType && hasRecognizedTargetMediaType;
53+
}
54+
55+
async convert(file: IFile): Promise<ParseResultElement> {
56+
const parseResultElement = file.parseResult;
57+
const annotations: AnnotationElement[] = [];
58+
const converted = dispatchRefractorPlugins(
59+
parseResultElement,
60+
[openAPIVersionRefractorPlugin(), webhooksRefractorPlugin({ annotations })],
61+
{
62+
toolboxCreator: createToolbox,
63+
visitorOptions: { keyMap, nodeTypeGetter: getNodeType },
64+
},
65+
);
66+
67+
const annotated = cloneShallow(converted);
68+
annotations.forEach((a) => annotated.push(a));
69+
annotated.replaceResult(OpenApi3_0Element.refract(converted.api));
70+
71+
return annotated;
72+
}
73+
}
74+
/* eslint-enable class-methods-use-this */
75+
76+
export default OpenAPI31ToOpenAPI30ConvertStrategy;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { OpenapiElement as Openapi30Element } from '@swagger-api/apidom-ns-openapi-3-0';
2+
3+
const openAPIVersionRefractorPlugin = () => () => ({
4+
visitor: {
5+
OpenapiElement() {
6+
return new Openapi30Element('3.0.3');
7+
},
8+
},
9+
});
10+
11+
export default openAPIVersionRefractorPlugin;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { OpenApi3_1Element } from '@swagger-api/apidom-ns-openapi-3-1';
2+
import { AnnotationElement, cloneShallow } from '@swagger-api/apidom-core';
3+
4+
type WebhooksRefractorPluginOptions = {
5+
annotations: AnnotationElement[];
6+
};
7+
8+
const webhooksRefractorPlugin =
9+
({ annotations }: WebhooksRefractorPluginOptions) =>
10+
() => ({
11+
visitor: {
12+
OpenApi3_1Element(element: OpenApi3_1Element) {
13+
if (!element.hasKey('webhooks')) return undefined;
14+
15+
const copy = cloneShallow(element);
16+
const annotation = new AnnotationElement(
17+
'Webhooks are not supported in OpenAPI 3.0.3. They will be removed from the converted document.',
18+
{ classes: ['warning'] },
19+
{ code: 'webhooks' },
20+
);
21+
22+
annotations.push(annotation);
23+
copy.remove('webhooks');
24+
25+
return copy;
26+
},
27+
},
28+
});
29+
30+
export default webhooksRefractorPlugin;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`apidom-converter convert given URI should convert 1`] = `
4+
{
5+
"openapi": "3.0.3"
6+
}
7+
`;
8+
9+
exports[`apidom-converter convertApiDOM given ApiDOM data should convert 1`] = `
10+
{
11+
"openapi": "3.0.3"
12+
}
13+
`;
Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,62 @@
1-
import { assert } from 'chai';
1+
import path from 'node:path';
2+
import { expect } from 'chai';
3+
import { toJSON } from '@swagger-api/apidom-core';
4+
import { mediaTypes as openAPI30MediaTypes } from '@swagger-api/apidom-parser-adapter-openapi-json-3-0';
5+
import { mediaTypes as openAPI31MediaTypes } from '@swagger-api/apidom-parser-adapter-openapi-json-3-1';
6+
import { parse } from '@swagger-api/apidom-reference';
7+
8+
import convert, { convertApiDOM } from '../src';
29

310
describe('apidom-converter', function () {
4-
it('initial test', async function () {
5-
assert.strictEqual(true, true);
11+
context('convert', function () {
12+
context('given URI', function () {
13+
specify('should convert', async function () {
14+
const fixturePath = path.join(
15+
__dirname,
16+
'strategies',
17+
'openapi-3-1-to-openapi-3-0-3',
18+
'refractor-plugins',
19+
'openapi-version',
20+
'fixtures',
21+
'openapi-version.json',
22+
);
23+
const convertedParseResult = await convert(fixturePath, {
24+
convert: {
25+
sourceMediaType: openAPI31MediaTypes.findBy('3.1.0', 'json'),
26+
targetMediaType: openAPI30MediaTypes.findBy('3.0.3', 'json'),
27+
},
28+
});
29+
30+
expect(toJSON(convertedParseResult.api!, undefined, 2)).toMatchSnapshot();
31+
});
32+
});
33+
});
34+
35+
context('convertApiDOM', function () {
36+
context('given ApiDOM data', function () {
37+
specify('should convert', async function () {
38+
const fixturePath = path.join(
39+
__dirname,
40+
'strategies',
41+
'openapi-3-1-to-openapi-3-0-3',
42+
'refractor-plugins',
43+
'openapi-version',
44+
'fixtures',
45+
'openapi-version.json',
46+
);
47+
const parseResult = await parse(fixturePath);
48+
const convertedParseResult = await convertApiDOM(parseResult, {
49+
convert: {
50+
sourceMediaType: openAPI31MediaTypes.findBy('3.1.0', 'json'),
51+
targetMediaType: openAPI30MediaTypes.findBy('3.0.3', 'json'),
52+
},
53+
resolve: {
54+
baseURI: fixturePath,
55+
},
56+
});
57+
58+
expect(toJSON(convertedParseResult.api!, undefined, 2)).toMatchSnapshot();
59+
});
60+
});
661
});
762
});

packages/apidom-converter/test/mocha-bootstrap.cjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require('@babel/register')({ extensions: ['.js', '.ts'], rootMode: 'upward' });
22

3+
const { options } = require('@swagger-api/apidom-reference');
34
const chai = require('chai');
45
const { jestSnapshotPlugin, addSerializer } = require('mocha-chai-jest-snapshot');
56

@@ -9,3 +10,7 @@ const jestStringSerializer = require('../../../scripts/jest-serializer-string.cj
910
chai.use(jestSnapshotPlugin());
1011
addSerializer(jestApiDOMSerializer);
1112
addSerializer(jestStringSerializer);
13+
14+
// setup allow list for file resolution
15+
const [fileResolver] = options.resolve.resolvers;
16+
fileResolver.fileAllowList = ['*'];

0 commit comments

Comments
 (0)