Skip to content

Commit

Permalink
feat(subtree-resolver): adapt to support OpenAPI 3.1.0
Browse files Browse the repository at this point in the history
Refs #2738
  • Loading branch information
char0n committed Jan 18, 2023
1 parent fb935c7 commit 72fe8ec
Show file tree
Hide file tree
Showing 9 changed files with 612 additions and 101 deletions.
44 changes: 22 additions & 22 deletions package-lock.json

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

19 changes: 18 additions & 1 deletion src/helpers/normalize/openapi-3-1.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { dispatchRefractorPlugins, isObjectElement } from '@swagger-api/apidom-core';
/* eslint-disable camelcase */
import { dispatchRefractorPlugins, isObjectElement, toValue } from '@swagger-api/apidom-core';
import {
refractorPluginNormalizeOperationIds,
refractorPluginNormalizeParameters,
Expand All @@ -9,6 +10,7 @@ import {
createToolbox,
keyMap,
getNodeType,
OpenApi3_1Element,
} from '@swagger-api/apidom-ns-openapi-3-1';

import opId from '../op-id.js';
Expand Down Expand Up @@ -38,4 +40,19 @@ const normalize = (element) => {
return normalized;
};

/**
* This adapter allow to perform normalization on Plain Old JavaScript Objects.
* The function adapts the `normalize` function interface and is able to accept
* Plain Old JavaScript Objects and returns Plain Old JavaScript Objects.
*/
export const pojoAdapter = (normalizeFn) => (spec) => {
if (spec?.$$normalized) return spec;

const openApiElement = OpenApi3_1Element.refract(spec);
const normalized = normalizeFn(openApiElement);

return toValue(normalized);
};

export default normalize;
/* eslint-enable camelcase */
135 changes: 72 additions & 63 deletions src/resolver/strategies/openapi-3-1.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { toValue, transclude, ParseResultElement } from '@swagger-api/apidom-cor
import {
compile as jsonPointerCompile,
evaluate as jsonPointerEvaluate,
EvaluationJsonPointerError,
InvalidJsonPointerError,
} from '@swagger-api/apidom-json-pointer';
import { OpenApi3_1Element, mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1';
import {
Expand Down Expand Up @@ -37,77 +39,84 @@ const resolveOpenAPI31Strategy = async (options) => {
parameterMacro = null,
modelPropertyMacro = null,
} = options;
// determining BaseURI
const defaultBaseURI = 'https://smartbear.com/';
const retrievalURI = optionsUtil.retrievalURI(options) ?? url.cwd();
const baseURI = url.isHttpUrl(retrievalURI) ? retrievalURI : defaultBaseURI;
try {
// determining BaseURI
const defaultBaseURI = 'https://smartbear.com/';
const retrievalURI = optionsUtil.retrievalURI(options) ?? url.cwd();
const baseURI = url.isHttpUrl(retrievalURI) ? retrievalURI : defaultBaseURI;

// prepare spec for dereferencing
const openApiElement = OpenApi3_1Element.refract(spec);
openApiElement.classes.push('result');
const openApiParseResultElement = new ParseResultElement([openApiElement]);
// prepare spec for dereferencing
const openApiElement = OpenApi3_1Element.refract(spec);
openApiElement.classes.push('result');
const openApiParseResultElement = new ParseResultElement([openApiElement]);

// prepare fragment for dereferencing
const jsonPointer = jsonPointerCompile(pathDiscriminator);
const jsonPointerURI = jsonPointer === '' ? '' : `#${jsonPointer}`;
const fragmentElement = jsonPointerEvaluate(jsonPointer, openApiElement);
// prepare fragment for dereferencing
const jsonPointer = jsonPointerCompile(pathDiscriminator);
const jsonPointerURI = jsonPointer === '' ? '' : `#${jsonPointer}`;
const fragmentElement = jsonPointerEvaluate(jsonPointer, openApiElement);

// prepare reference set for dereferencing
const openApiElementReference = Reference({ uri: baseURI, value: openApiParseResultElement });
const refSet = ReferenceSet({ refs: [openApiElementReference] });
if (jsonPointer !== '') refSet.rootRef = null; // reset root reference as we want fragment to become the root reference
// prepare reference set for dereferencing
const openApiElementReference = Reference({ uri: baseURI, value: openApiParseResultElement });
const refSet = ReferenceSet({ refs: [openApiElementReference] });
if (jsonPointer !== '') refSet.rootRef = null; // reset root reference as we want fragment to become the root reference

const dereferenced = await dereferenceApiDOM(fragmentElement, {
resolve: {
/**
* swagger-client only supports resolving HTTP(S) URLs or spec objects.
* If runtime env is detected as non-browser one,
* and baseURI was not provided as part of resolver options,
* then below baseURI check will make sure that constant HTTPS URL is used as baseURI.
*/
baseURI: `${baseURI}${jsonPointerURI}`,
resolvers: [
HttpResolverSwaggerClient({
timeout: timeout || 10000,
redirects: redirects || 10,
}),
],
resolverOpts: {
swaggerHTTPClientConfig: {
requestInterceptor,
responseInterceptor,
const dereferenced = await dereferenceApiDOM(fragmentElement, {
resolve: {
/**
* swagger-client only supports resolving HTTP(S) URLs or spec objects.
* If runtime env is detected as non-browser one,
* and baseURI was not provided as part of resolver options,
* then below baseURI check will make sure that constant HTTPS URL is used as baseURI.
*/
baseURI: `${baseURI}${jsonPointerURI}`,
resolvers: [
HttpResolverSwaggerClient({
timeout: timeout || 10000,
redirects: redirects || 10,
}),
],
resolverOpts: {
swaggerHTTPClientConfig: {
requestInterceptor,
responseInterceptor,
},
},
strategies: [OpenApi3_1ResolveStrategy()],
},
strategies: [OpenApi3_1ResolveStrategy()],
},
parse: {
mediaType: mediaTypes.latest(),
parsers: [
OpenApiJson3_1Parser({ allowEmpty: false, sourceMap: false }),
OpenApiYaml3_1Parser({ allowEmpty: false, sourceMap: false }),
JsonParser({ allowEmpty: false, sourceMap: false }),
YamlParser({ allowEmpty: false, sourceMap: false }),
BinaryParser({ allowEmpty: false, sourceMap: false }),
],
},
dereference: {
maxDepth: 100,
strategies: [
OpenApi3_1SwaggerClientDereferenceStrategy({
allowMetaPatches,
useCircularStructures,
parameterMacro,
modelPropertyMacro,
}),
],
refSet,
},
});
parse: {
mediaType: mediaTypes.latest(),
parsers: [
OpenApiJson3_1Parser({ allowEmpty: false, sourceMap: false }),
OpenApiYaml3_1Parser({ allowEmpty: false, sourceMap: false }),
JsonParser({ allowEmpty: false, sourceMap: false }),
YamlParser({ allowEmpty: false, sourceMap: false }),
BinaryParser({ allowEmpty: false, sourceMap: false }),
],
},
dereference: {
maxDepth: 100,
strategies: [
OpenApi3_1SwaggerClientDereferenceStrategy({
allowMetaPatches,
useCircularStructures,
parameterMacro,
modelPropertyMacro,
}),
],
refSet,
},
});

const transcluded = transclude(fragmentElement, dereferenced, openApiElement);
const normalized = skipNormalization ? transcluded : normalizeOpenAPI31(transcluded);
const transcluded = transclude(fragmentElement, dereferenced, openApiElement);
const normalized = skipNormalization ? transcluded : normalizeOpenAPI31(transcluded);

return { spec: toValue(normalized), errors: [] };
return { spec: toValue(normalized), errors: [] };
} catch (error) {
if (error instanceof InvalidJsonPointerError || error instanceof EvaluationJsonPointerError) {
return { spec: null, errors: [] };
}
throw error;
}
};

export default resolveOpenAPI31Strategy;
Expand Down
17 changes: 11 additions & 6 deletions src/subtree-resolver/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
// future versions.
//
// TODO: move the remarks above into project documentation

import get from 'lodash/get';

import { isOpenAPI31 } from '../helpers/openapi-predicates.js';
import resolve from '../resolver/index.js';
// eslint-disable-next-line camelcase
import normalizeOpenAPI2__30 from '../helpers/normalize/openapi-2--3-0.js';
import normalizeOpenAPI2__30 from '../helpers/normalize/openapi-2--3-0.js'; // eslint-disable-line camelcase
import normalizeOpenAPI31, { pojoAdapter } from '../helpers/normalize/openapi-3-1.js';

export default async function resolveSubtree(obj, path, opts = {}) {
const {
Expand All @@ -48,9 +48,14 @@ export default async function resolveSubtree(obj, path, opts = {}) {
useCircularStructures,
};

const { spec: normalized } = normalizeOpenAPI2__30({
spec: obj,
});
let normalized;
if (isOpenAPI31(obj)) {
normalized = pojoAdapter(normalizeOpenAPI31)(obj);
} else {
({ spec: normalized } = normalizeOpenAPI2__30({
spec: obj,
}));
}

const result = await resolve({
...resolveOptions,
Expand Down
14 changes: 6 additions & 8 deletions test/resolver/strategies/openapi-3-1/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from 'node:path';
import fetchMock from 'fetch-mock';
import { EvaluationJsonPointerError } from '@swagger-api/apidom-json-pointer';

import SwaggerClient from '../../../../src/index.js';

Expand Down Expand Up @@ -183,15 +182,14 @@ describe('resolve', () => {
});

describe('and pathDiscriminator compiles into invalid JSON Pointer', () => {
test('should throw error', async () => {
test('should return spec as null', async () => {
const spec = globalThis.loadJsonFile(path.join(fixturePath, 'petstore.json'));
const resolveThunk = () =>
SwaggerClient.resolve({
spec,
pathDiscriminator: ['path', 'to', 'nothing'],
});
const resolvedSpec = await SwaggerClient.resolve({
spec,
pathDiscriminator: ['path', 'to', 'nothing'],
});

await expect(resolveThunk()).rejects.toThrow(EvaluationJsonPointerError);
expect(resolvedSpec).toEqual({ spec: null, errors: [] });
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import xmock from 'xmock';

import resolve from '../src/subtree-resolver/index.js';
import resolve from '../../../src/subtree-resolver/index.js';

describe('subtree $ref resolver', () => {
let xapp;
Expand Down
Loading

0 comments on commit 72fe8ec

Please sign in to comment.