Skip to content

Commit

Permalink
feat(converter): add complete code infrastructure
Browse files Browse the repository at this point in the history
Refs #3697
Closes #3743
  • Loading branch information
char0n committed Jan 30, 2024
1 parent 41eb221 commit 682b154
Show file tree
Hide file tree
Showing 36 changed files with 518 additions and 128 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/apidom-converter/config/webpack/browser.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ const browser = {
},
resolve: {
extensions: ['.ts', '.mjs', '.js', '.json'],
fallback: {
fs: false,
path: false,
},
},
module: {
rules: [
{
test: /\.wasm$/,
loader: 'file-loader',
type: 'javascript/auto',
},
{
test: /\.(ts|js)?$/,
exclude: /node_modules/,
Expand Down Expand Up @@ -48,9 +57,18 @@ const browserMin = {
},
resolve: {
extensions: ['.ts', '.mjs', '.js', '.json'],
fallback: {
fs: false,
path: false,
},
},
module: {
rules: [
{
test: /\.wasm$/,
loader: 'file-loader',
type: 'javascript/auto',
},
{
test: /\.(ts|js)?$/,
exclude: /node_modules/,
Expand Down
8 changes: 4 additions & 4 deletions packages/apidom-converter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
"dependencies": {
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-core": "^0.93.0",
"@swagger-api/apidom-parser-adapter-yaml-1-2": "^0.93.0",
"@swagger-api/apidom-ns-openapi-3-1": "^0.93.0",
"@swagger-api/apidom-ns-openapi-3-0": "^0.93.0",
"@swagger-api/apidom-ns-openapi-2": "^0.93.0"
"@swagger-api/apidom-ns-openapi-3-1": "^0.93.0",
"@swagger-api/apidom-reference": "^0.93.0",
"stampit": "^4.3.2"
},
"files": [
"cjs/",
Expand All @@ -56,4 +56,4 @@
"README.md",
"CHANGELOG.md"
]
}
}
5 changes: 5 additions & 0 deletions packages/apidom-converter/src/errors/ConvertError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ApiDOMError } from '@swagger-api/apidom-error';

class ConvertError extends ApiDOMError {}

export default ConvertError;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ConvertError from './ConvertError';

class UnmatchedConvertStrategyError extends ConvertError {}

export default UnmatchedConvertStrategyError;
18 changes: 0 additions & 18 deletions packages/apidom-converter/src/get-refractor.ts

This file was deleted.

45 changes: 33 additions & 12 deletions packages/apidom-converter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import { parse } from '@swagger-api/apidom-parser-adapter-yaml-1-2';

import getOpenAPIRefractor from './get-refractor';
import getPluginsBySpec from './plugins/get-plugins-by-spec';

const convert = async (yaml: string, from: string) => {
const apiDOM = await parse(yaml);
const refractor = getOpenAPIRefractor(from);
const openApiElement = refractor.refract(apiDOM.result, {
plugins: [...getPluginsBySpec(from)],
}) as unknown as typeof refractor;
return openApiElement;
import { ParseResultElement } from '@swagger-api/apidom-core';
import { mergeOptions, bundle, File } from '@swagger-api/apidom-reference';

import defaultOptions, { ConverterOptions } from './options';
import ConvertError from './errors/ConvertError';
import UnmatchedConvertStrategyError from './errors/UnmatchedConvertStrategyError';

export { ConvertError, UnmatchedConvertStrategyError };

/**
* `convertApiDOM` already assumes that the ApiDOM is bundled.
*/
export const convertApiDOM = async (element: ParseResultElement, options = {}) => {
const mergedOptions = mergeOptions(defaultOptions, options || {}) as ConverterOptions;
const file = File({
uri: mergedOptions.resolve.baseURI,
parseResult: element,
mediaType: mergedOptions.convert.sourceMediaType || mergedOptions.parse.mediaType,
});
const strategy = mergedOptions.convert.strategies.find((s) => s.canConvert(file, mergedOptions));

if (typeof strategy === 'undefined') {
throw new UnmatchedConvertStrategyError(file.uri);
}

return strategy.convert(file, mergedOptions);
};

const convert = async (uri: string, options = {}) => {
const mergedOptions = mergeOptions(defaultOptions, options || {}) as ConverterOptions;
const parseResult = await bundle(uri, mergedOptions);

return convertApiDOM(parseResult, mergedOptions);
};

export default convert;
40 changes: 40 additions & 0 deletions packages/apidom-converter/src/options/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { options as referenceOptions } from '@swagger-api/apidom-reference';

import ConvertStrategy from '../strategies/ConvertStrategy';
import OpenAPI31ToOpenAPI30ConvertStrategy from '../strategies/openapi-3-1-to-openapi-3-0-3';

type ReferenceOptions = typeof referenceOptions;

interface ConvertOptions {
strategies: Array<ConvertStrategy>;
sourceMediaType: string;
targetMediaType: string;
}

export interface ConverterOptions extends ReferenceOptions {
readonly convert: ConvertOptions;
}

const defaultOptions: ConverterOptions = {
...referenceOptions,
convert: {
/**
* Determines strategies how ApiDOM is bundled.
* Strategy is determined by media type or by inspecting ApiDOM to be bundled.
*
* You can add additional bundle strategies of your own, replace an existing one with
* your own implementation, or remove any bundle strategy by removing it from the list.
*/
strategies: [new OpenAPI31ToOpenAPI30ConvertStrategy()],
/**
* Media type of source API definition.
*/
sourceMediaType: 'text/plain',
/**
* Media type of target API definition.
*/
targetMediaType: 'text/plain',
},
};

export default defaultOptions;
16 changes: 0 additions & 16 deletions packages/apidom-converter/src/plugins/get-plugins-by-spec.ts

This file was deleted.

13 changes: 0 additions & 13 deletions packages/apidom-converter/src/plugins/openapi3_1/spec-downgrade.ts

This file was deleted.

26 changes: 26 additions & 0 deletions packages/apidom-converter/src/strategies/ConvertStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import stampit from 'stampit';
import { ParseResultElement } from '@swagger-api/apidom-core';
import { File } from '@swagger-api/apidom-reference';

import type { ConverterOptions } from '../options';

type ExtractGenericType<T> = T extends stampit.Stamp<infer U> ? U : never;
export type IFile = ExtractGenericType<typeof File>;

export interface ConvertStrategyOptions {
readonly name: string;
}

abstract class ConvertStrategy {
public readonly name: string;

protected constructor({ name }: ConvertStrategyOptions) {
this.name = name;
}

abstract canConvert(file: IFile, options: ConverterOptions): boolean;

abstract convert(file: IFile, options: ConverterOptions): Promise<ParseResultElement>;
}

export default ConvertStrategy;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
OpenApi3_0Element,
mediaTypes as openAPI3_0MediaTypes,
} from '@swagger-api/apidom-ns-openapi-3-0';
import {
isOpenApi3_1Element,
mediaTypes as openAPI3_1MediaTypes,
createToolbox,
keyMap,
getNodeType,
} from '@swagger-api/apidom-ns-openapi-3-1';
import {
ParseResultElement,
dispatchRefractorPlugins,
AnnotationElement,
cloneShallow,
} from '@swagger-api/apidom-core';

import ConvertStrategy, { IFile } from '../ConvertStrategy';
import openAPIVersionRefractorPlugin from './refractor-plugins/openapi-version';
import webhooksRefractorPlugin from './refractor-plugins/webhooks';
import type { ConverterOptions } from '../../options';

// eslint-disable-next-line @typescript-eslint/naming-convention
const openAPI3_0_3MediaTypes = [
openAPI3_0MediaTypes.findBy('3.0.3', 'generic'),
openAPI3_0MediaTypes.findBy('3.0.3', 'json'),
openAPI3_0MediaTypes.findBy('3.0.3', 'yaml'),
];

/* eslint-disable class-methods-use-this */
class OpenAPI31ToOpenAPI30ConvertStrategy extends ConvertStrategy {
constructor() {
super({ name: 'openapi-3-1-to-openapi-3-0-3' });
}

canConvert(file: IFile, options: ConverterOptions): boolean {
let hasRecognizedSourceMediaType = false;
const hasRecognizedTargetMediaType = openAPI3_0_3MediaTypes.includes(
options.convert.targetMediaType,
);

// source detection
if (openAPI3_1MediaTypes.includes(options.convert.sourceMediaType)) {
hasRecognizedSourceMediaType = true;
} else if (file.mediaType !== 'text/plain') {
hasRecognizedSourceMediaType = openAPI3_1MediaTypes.includes(file.mediaType);
} else if (isOpenApi3_1Element(file.parseResult?.result)) {
hasRecognizedSourceMediaType = true;
}

return hasRecognizedSourceMediaType && hasRecognizedTargetMediaType;
}

async convert(file: IFile): Promise<ParseResultElement> {
const parseResultElement = file.parseResult;
const annotations: AnnotationElement[] = [];
const converted = dispatchRefractorPlugins(
parseResultElement,
[openAPIVersionRefractorPlugin(), webhooksRefractorPlugin({ annotations })],
{
toolboxCreator: createToolbox,
visitorOptions: { keyMap, nodeTypeGetter: getNodeType },
},
);

const annotated = cloneShallow(converted);
annotations.forEach((a) => annotated.push(a));
annotated.replaceResult(OpenApi3_0Element.refract(converted.api));

return annotated;
}
}
/* eslint-enable class-methods-use-this */

export default OpenAPI31ToOpenAPI30ConvertStrategy;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OpenapiElement as Openapi30Element } from '@swagger-api/apidom-ns-openapi-3-0';

const openAPIVersionRefractorPlugin = () => () => ({
visitor: {
OpenapiElement() {
return new Openapi30Element('3.0.3');
},
},
});

export default openAPIVersionRefractorPlugin;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { OpenApi3_1Element } from '@swagger-api/apidom-ns-openapi-3-1';
import { AnnotationElement, cloneShallow } from '@swagger-api/apidom-core';

type WebhooksRefractorPluginOptions = {
annotations: AnnotationElement[];
};

const webhooksRefractorPlugin =
({ annotations }: WebhooksRefractorPluginOptions) =>
() => ({
visitor: {
OpenApi3_1Element(element: OpenApi3_1Element) {
if (!element.hasKey('webhooks')) return undefined;

const copy = cloneShallow(element);
const annotation = new AnnotationElement(
'Webhooks are not supported in OpenAPI 3.0.3. They will be removed from the converted document.',
{ classes: ['warning'] },
{ code: 'webhooks' },
);

annotations.push(annotation);
copy.remove('webhooks');

return copy;
},
},
});

export default webhooksRefractorPlugin;
13 changes: 13 additions & 0 deletions packages/apidom-converter/test/__snapshots__/index.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`apidom-converter convert given URI should convert 1`] = `
{
"openapi": "3.0.3"
}
`;

exports[`apidom-converter convertApiDOM given ApiDOM data should convert 1`] = `
{
"openapi": "3.0.3"
}
`;
Loading

0 comments on commit 682b154

Please sign in to comment.