Skip to content

Commit 682b154

Browse files
committed
feat(converter): add complete code infrastructure
Refs #3697 Closes #3743
1 parent 41eb221 commit 682b154

File tree

36 files changed

+518
-128
lines changed

36 files changed

+518
-128
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@
4040
"dependencies": {
4141
"@babel/runtime-corejs3": "^7.20.7",
4242
"@swagger-api/apidom-core": "^0.93.0",
43-
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.93.0",
44-
"@swagger-api/apidom-ns-openapi-3-1": "^0.93.0",
4543
"@swagger-api/apidom-ns-openapi-3-0": "^0.93.0",
46-
"@swagger-api/apidom-ns-openapi-2": "^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"
4747
},
4848
"files": [
4949
"cjs/",
@@ -56,4 +56,4 @@
5656
"README.md",
5757
"CHANGELOG.md"
5858
]
59-
}
59+
}
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;

packages/apidom-converter/src/get-refractor.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
1-
import { parse } from '@swagger-api/apidom-parser-adapter-yaml-1-2';
2-
3-
import getOpenAPIRefractor from './get-refractor';
4-
import getPluginsBySpec from './plugins/get-plugins-by-spec';
5-
6-
const convert = async (yaml: string, from: string) => {
7-
const apiDOM = await parse(yaml);
8-
const refractor = getOpenAPIRefractor(from);
9-
const openApiElement = refractor.refract(apiDOM.result, {
10-
plugins: [...getPluginsBySpec(from)],
11-
}) as unknown as typeof refractor;
12-
return openApiElement;
1+
import { ParseResultElement } from '@swagger-api/apidom-core';
2+
import { mergeOptions, bundle, File } from '@swagger-api/apidom-reference';
3+
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);
1334
};
1435

1536
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;

packages/apidom-converter/src/plugins/get-plugins-by-spec.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

packages/apidom-converter/src/plugins/openapi3_1/spec-downgrade.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.
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+
`;

0 commit comments

Comments
 (0)