Skip to content

Commit

Permalink
feat(reference): add OpenAPI 2.0 dereference strategy (#3435)
Browse files Browse the repository at this point in the history
Refs #3102
  • Loading branch information
char0n authored Nov 22, 2023
1 parent 7cb6980 commit aa3710b
Show file tree
Hide file tree
Showing 66 changed files with 2,019 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { isObjectElement, Element } from '@swagger-api/apidom-core';
import { isObjectElement, ObjectElement } from '@swagger-api/apidom-core';

export interface JSONReferenceLikeElement extends ObjectElement {
hasKey: (value: '$ref') => true;
}

// eslint-disable-next-line import/prefer-default-export
export const isJSONReferenceLikeElement = <T extends Element>(element: T): boolean => {
// @ts-ignore
export const isJSONReferenceLikeElement = (
element: unknown,
): element is JSONReferenceLikeElement => {
return isObjectElement(element) && element.hasKey('$ref');
};
4 changes: 4 additions & 0 deletions packages/apidom-ns-openapi-2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
} from '@swagger-api/apidom-core';
export {
isJSONReferenceElement,
isJSONReferenceLikeElement,
JSONReferenceElement,
} from '@swagger-api/apidom-ns-json-schema-draft-4';

Expand All @@ -33,6 +34,7 @@ export {
isLicenseElement,
isPathsElement,
isPathItemElement,
isPathItemElementExternal,
isOperationElement,
isExternalDocumentationElement,
isParameterElement,
Expand All @@ -44,7 +46,9 @@ export {
isHeaderElement,
isTagElement,
isReferenceElement,
isReferenceElementExternal,
isSchemaElement,
isJSONReferenceElementExternal,
isXmlElement,
isDefinitionsElement,
isParametersDefinitionsElement,
Expand Down
56 changes: 55 additions & 1 deletion packages/apidom-ns-openapi-2/src/predicates.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { createPredicate } from '@swagger-api/apidom-core';
import {
createPredicate,
ElementPredicate,
isStringElement,
toValue,
} from '@swagger-api/apidom-core';
import {
isJSONReferenceElement,
JSONReferenceElement,
} from '@swagger-api/apidom-ns-json-schema-draft-4';

import SwaggerElement from './elements/Swagger';
import SwaggerVersionElement from './elements/SwaggerVersion';
Expand Down Expand Up @@ -98,6 +107,21 @@ export const isPathItemElement = createPredicate(
},
);

export const isPathItemElementExternal: ElementPredicate<PathItemElement> = (
element: unknown,
): element is PathItemElement => {
if (!isPathItemElement(element)) {
return false;
}
if (!isStringElement(element.$ref)) {
return false;
}

const value = toValue(element.$ref);

return typeof value === 'string' && value.length > 0 && !value.startsWith('#');
};

export const isOperationElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: unknown): element is OperationElement =>
Expand Down Expand Up @@ -208,6 +232,21 @@ export const isReferenceElement = createPredicate(
},
);

export const isReferenceElementExternal: ElementPredicate<ReferenceElement> = (
element: unknown,
): element is ReferenceElement => {
if (!isReferenceElement(element)) {
return false;
}
if (!isStringElement(element.$ref)) {
return false;
}

const value = toValue(element.$ref);

return typeof value === 'string' && value.length > 0 && !value.startsWith('#');
};

export const isSchemaElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: unknown): element is SchemaElement =>
Expand All @@ -218,6 +257,21 @@ export const isSchemaElement = createPredicate(
},
);

export const isJSONReferenceElementExternal: ElementPredicate<JSONReferenceElement> = (
element: unknown,
): element is PathItemElement => {
if (!isJSONReferenceElement(element)) {
return false;
}
if (!isStringElement(element.$ref)) {
return false;
}

const value = toValue(element.$ref);

return typeof value === 'string' && value.length > 0 && !value.startsWith('#');
};

export const isXmlElement = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq }) => {
return (element: unknown): element is XmlElement =>
Expand Down
43 changes: 31 additions & 12 deletions packages/apidom-reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1099,9 +1099,9 @@ It's possible to **change** strategies **order globally** by mutating global `re

```js
import { options } from '@swagger-api/apidom-reference';
import { AsyncApi2ResolveStrategy } from '@swagger-api/apidom-reference/resolve/strategies/asyncapi-2';
import { OpenApi3_0ResolveStrategy } from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-0';
import { OpenApi3_1ResolveStrategy } from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-1';
import AsyncApi2ResolveStrategy from '@swagger-api/apidom-reference/resolve/strategies/asyncapi-2';
import OpenApi3_0ResolveStrategy from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-0';
import OpenApi3_1ResolveStrategy from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-1';

options.resolve.strategies = [
OpenApi3_0ResolveStrategy(),
Expand All @@ -1114,9 +1114,9 @@ To **change** the strategies **order** on ad-hoc basis:

```js
import { resolve } from '@swagger-api/apidom-reference';
import { AsyncApi2ResolveStrategy } from '@swagger-api/apidom-reference/resolve/strategies/asyncapi-2';
import { OpenApi3_0ResolveStrategy } from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-0';
import { OpenApi3_1ResolveStrategy } from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-1';
import AsyncApi2ResolveStrategy from '@swagger-api/apidom-reference/resolve/strategies/asyncapi-2';
import OpenApi3_0ResolveStrategy from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-0';
import OpenApi3_1ResolveStrategy from '@swagger-api/apidom-reference/resolve/strategies/openapi-3-1';


await resolve('/home/user/oas.json', {
Expand Down Expand Up @@ -1361,6 +1361,20 @@ Supported media types:
]
```

##### [openapi-2](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/dereference/strategies/openapi-2)

Dereference strategy for dereferencing [OpenApi 2.0](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md) definitions.

Supported media types:

```js
[
'application/vnd.oai.openapi;version=2.0',
'application/vnd.oai.openapi+json;version=2.0',
'application/vnd.oai.openapi+yaml;version=2.0',
]
```

##### [openapi-3-0](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/dereference/strategies/openapi-3-0)

Dereference strategy for dereferencing [OpenApi 3.0.x](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md) definitions.
Expand Down Expand Up @@ -1410,6 +1424,7 @@ returns `true` or until entire list of strategies is exhausted (throws error).

```js
[
OpenApi2DereferenceStrategy(),
OpenApi3_0DereferenceStrategy(),
OpenApi3_1DereferenceStrategy(),
AsyncApi2DereferenceStrategy(),
Expand All @@ -1421,11 +1436,13 @@ It's possible to **change** strategies **order globally** by mutating global `de

```js
import { options } from '@swagger-api/apidom-reference';
import { AsyncApi2DereferenceStrategy } from '@swagger-api/apidom-reference/dereference/strategies/asyncapi-2'
import { OpenApi3_0DereferenceStrategy } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-0'
import { OpenApi3_1DereferenceStrategy } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1'
import AsyncApi2DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/asyncapi-2'
import OpenApi2DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-2'
import OpenApi3_0DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-0'
import OpenApi3_1DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1'

options.dereference.strategies = [
OpenApi2DereferenceStrategy(),
OpenApi3_0DereferenceStrategy(),
OpenApi3_1DereferenceStrategy(),
AsyncApi2DereferenceStrategy(),
Expand All @@ -1436,9 +1453,10 @@ To **change** the strategies **order** on ad-hoc basis:

```js
import { dereference } from '@swagger-api/apidom-reference';
import { AsyncApi2DereferenceStrategy } from '@swagger-api/apidom-reference/dereference/strategies/asyncapi-2'
import { OpenApi3_0DereferenceStrategy } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-0'
import { OpenApi3_1DereferenceStrategy } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1'
import AsyncApi2DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/asyncapi-2'
import OpenApi2DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-2'
import OpenApi3_0DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-0'
import OpenApi3_1DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1'

await dereference('/home/user/oas.json', {
parse: {
Expand All @@ -1447,6 +1465,7 @@ await dereference('/home/user/oas.json', {
dereference: {
strategies: [
AsyncApi2DereferenceStrategy(),
OpenApi2DereferenceStrategy(),
OpenApi3_0DereferenceStrategy(),
OpenApi3_1DereferenceStrategy(),
]
Expand Down
5 changes: 5 additions & 0 deletions packages/apidom-reference/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@
"require": "./cjs/dereference/strategies/asyncapi-2/index.cjs",
"types": "./types/dereference/strategies/asyncapi-2/index.d.ts"
},
"./dereference/strategies/openapi-2": {
"import": "./es/dereference/strategies/openapi-2/index.mjs",
"require": "./cjs/dereference/strategies/openapi-2/index.cjs",
"types": "./types/dereference/strategies/openapi-2/index.d.ts"
},
"./dereference/strategies/openapi-3-0": {
"import": "./es/dereference/strategies/openapi-3-0/index.mjs",
"require": "./cjs/dereference/strategies/openapi-3-0/index.cjs",
Expand Down
2 changes: 2 additions & 0 deletions packages/apidom-reference/src/configuration/saturated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import AsyncApiYaml2Parser from '../parse/parsers/asyncapi-yaml-2';
import JsonParser from '../parse/parsers/json';
import YamlParser from '../parse/parsers/yaml-1-2';
import BinaryParser from '../parse/parsers/binary/index-node';
import OpenApi2DereferenceStrategy from '../dereference/strategies/openapi-2';
import OpenApi3_0DereferenceStrategy from '../dereference/strategies/openapi-3-0';
import OpenApi3_1DereferenceStrategy from '../dereference/strategies/openapi-3-1';
import AsyncApi2DereferenceStrategy from '../dereference/strategies/asyncapi-2';
Expand Down Expand Up @@ -49,6 +50,7 @@ options.resolve.strategies = [
];

options.dereference.strategies = [
OpenApi2DereferenceStrategy(),
OpenApi3_0DereferenceStrategy(),
OpenApi3_1DereferenceStrategy(),
AsyncApi2DereferenceStrategy(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import stampit from 'stampit';
import { defaultTo, propEq } from 'ramda';
import { createNamespace, visit, Element } from '@swagger-api/apidom-core';
import openApi2Namespace, {
getNodeType,
isSwaggerElement,
keyMap,
mediaTypes,
} from '@swagger-api/apidom-ns-openapi-2';

import DereferenceStrategy from '../DereferenceStrategy';
import {
DereferenceStrategy as IDereferenceStrategy,
File as IFile,
ReferenceOptions as IReferenceOptions,
} from '../../../types';
import Reference from '../../../Reference';
import ReferenceSet from '../../../ReferenceSet';
import OpenApi2DereferenceVisitor from './visitor';

// @ts-ignore
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];

const OpenApi2DereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampit(
DereferenceStrategy,
{
init() {
this.name = 'openapi-2';
},
methods: {
canDereference(file: IFile): boolean {
// assert by media type
if (file.mediaType !== 'text/plain') {
return mediaTypes.includes(file.mediaType);
}

// assert by inspecting ApiDOM
return isSwaggerElement(file.parseResult?.api);
},

async dereference(file: IFile, options: IReferenceOptions): Promise<Element> {
const namespace = createNamespace(openApi2Namespace);
const refSet = defaultTo(ReferenceSet(), options.dereference.refSet);
let reference;

if (!refSet.has(file.uri)) {
reference = Reference({ uri: file.uri, value: file.parseResult });
refSet.add(reference);
} else {
// pre-computed refSet was provided as configuration option
reference = refSet.find(propEq(file.uri, 'uri'));
}

const visitor = OpenApi2DereferenceVisitor({ reference, namespace, options });
const dereferencedElement = await visitAsync(refSet.rootRef.value, visitor, {
keyMap,
nodeTypeGetter: getNodeType,
});

/**
* Release all memory if this refSet was not provided as an configuration option.
* If provided as configuration option, then provider is responsible for cleanup.
*/
if (options.dereference.refSet === null) {
refSet.clean();
}

return dereferencedElement;
},
},
},
);

export default OpenApi2DereferenceStrategy;
Loading

0 comments on commit aa3710b

Please sign in to comment.