Skip to content

Commit

Permalink
feat: add initial OpenAPI 3.1.0 dereference strategy
Browse files Browse the repository at this point in the history
Refs #2717
  • Loading branch information
char0n committed Dec 29, 2022
1 parent 33b892c commit 41e6bf1
Show file tree
Hide file tree
Showing 19 changed files with 246 additions and 12 deletions.
6 changes: 5 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
"import/no-unresolved": [
2,
{
"ignore": ["^@swagger-api/apidom-reference/configuration/empty$"]
"ignore": [
"^@swagger-api/apidom-reference/configuration/empty$",
"^@swagger-api/apidom-reference/dereference/strategies/openapi-3-1$",
"^@swagger-api/apidom-reference/resolve/resolvers/file$"
]
}
],
"prettier/prettier": "error",
Expand Down
2 changes: 2 additions & 0 deletions config/jest/jest.unit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ module.exports = {
'<rootDir>/test/jest.setup.js',
'<rootDir>/test/specmap/data/',
'<rootDir>/test/build-artifacts/',
'/__fixtures__/',
'/__utils__/',
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-disable camelcase */
import { createNamespace, visit } from '@swagger-api/apidom-core';
import { ReferenceSet, Reference } from '@swagger-api/apidom-reference/configuration/empty';
import OpenApi3_1DereferenceStrategy from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1';
import openApi3_1Namespace, { getNodeType, keyMap } from '@swagger-api/apidom-ns-openapi-3-1';

import OpenApi3_1SwaggerClientDereferenceVisitor from './visitor.js';

const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];

const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy.compose({
props: {
useCircularStructures: true,
allowMetaPatches: false,
},
init({
useCircularStructures = this.useCircularStructures,
allowMetaPatches = this.allowMetaPatches,
} = {}) {
this.name = 'openapi-3-1-swagger-client';
this.useCircularStructures = useCircularStructures;
this.allowMetaPatches = allowMetaPatches;
},
methods: {
async dereference(file, options) {
const namespace = createNamespace(openApi3_1Namespace);
const refSet = options.dereference.refSet ?? ReferenceSet();
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((ref) => ref.uri === file.uri);
}

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

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

return dereferencedElement;
},
},
});

export default OpenApi3_1SwaggerClientDereferenceStrategy;
/* eslint-enable camelcase */
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable camelcase */
import { OpenApi3_1DereferenceVisitor } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1';

const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.compose({});

export default OpenApi3_1SwaggerClientDereferenceVisitor;
/* eslint-enable camelcase */
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable camelcase */
import { options } from '@swagger-api/apidom-reference/configuration/empty';
import FileResolver from '@swagger-api/apidom-reference/resolve/resolvers/file';

import JsonParser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/json/index.js';
import YamlParser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/yaml-1-2/index.js';
import OpenApiJson3_1Parser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/openapi-json-3-1/index.js';
import OpenApiYaml3_1Parser from '../../../../../../../../src/helpers/apidom/reference/parse/parsers/openapi-yaml-3-1/index.js';
import HttpResolverSwaggerClient from '../../../../../../../../src/helpers/apidom/reference/resolve/resolvers/http-swagger-client/index.js';
import OpenApi3_1SwaggerClientDereferenceStrategy from '../../../../../../../../src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js';

export const beforeAll = () => {
// configure custom parser plugins globally
options.parse.parsers = [
OpenApiJson3_1Parser({ allowEmpty: false, sourceMap: false }),
OpenApiYaml3_1Parser({ allowEmpty: false, sourceMap: false }),
JsonParser({ allowEmpty: false, sourceMap: false }),
YamlParser({ allowEmpty: false, sourceMap: false }),
];

// configure custom resolver plugins globally
options.resolve.resolvers = [FileResolver({ fileAllowList: ['*'] }), HttpResolverSwaggerClient()];

// configure custom dereference strategy globally
options.dereference.strategies = [OpenApi3_1SwaggerClientDereferenceStrategy()];
};

export const afterAll = () => {
options.parse.parsers = [];
options.resolve.resolvers = [];
options.dereference.strategies = [];
};
/* eslint-enable camelcase */
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"openapi": "3.1.0",
"components": {
"callbacks": {
"callback1": {
"{$method}": {
"description": "description of callback2"
}
},
"callback2": {
"{$method}": {
"description": "description of callback2"
}
}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
openapi: 3.1.0
components:
callbacks:
callback1:
"$ref": "#/components/callbacks/callback2"
callback2:
"{$method}":
description: description of callback2
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[
{
"openapi": "3.1.0",
"paths": {
"/path": {
"get": {
"callbacks": {
"callback": {
"{$method}": {
"description": "description of callback2"
}
}
}
}
}
},
"components": {
"callbacks": {
"callback": {
"{$method}": {
"description": "description of callback2"
}
}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
openapi: 3.1.0
paths:
"/path":
get:
callbacks:
callback:
"$ref": "#/components/callbacks/callback"
components:
callbacks:
callback:
"{$method}":
description: description of callback2
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import path from 'node:path';
import { toValue } from '@swagger-api/apidom-core';
import { mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1';
import { dereference } from '@swagger-api/apidom-reference/configuration/empty';

import * as jestSetup from '../__utils__/jest.local.setup.js';

const rootFixturePath = path.join(__dirname, '__fixtures__');

describe('dereference', () => {
beforeAll(() => {
jestSetup.beforeAll();
});

afterAll(() => {
jestSetup.afterAll();
});

describe('strategies', () => {
describe('openapi-3-1swagger-client', () => {
describe('Callback Object', () => {
describe('given in components/callbacks field', () => {
const fixturePath = path.join(rootFixturePath, 'components-callbacks');

test('should dereference', async () => {
const rootFilePath = path.join(fixturePath, 'root.yaml');
const actual = await dereference(rootFilePath, {
parse: { mediaType: mediaTypes.latest('yaml') },
});
const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json'));

expect(toValue(actual)).toEqual(expected);
});
});

describe('given in Operation Object', () => {
const fixturePath = path.join(rootFixturePath, 'operation-object');

test('should dereference', async () => {
const rootFilePath = path.join(fixturePath, 'root.yaml');
const actual = await dereference(rootFilePath, {
parse: { mediaType: mediaTypes.latest('yaml') },
});
const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json'));

expect(toValue(actual)).toEqual(expected);
});
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ describe('OpenApiJson3_1Parser', () => {
describe('given file with supported extension', () => {
describe('and file data is buffer and can be detected as OpenAPI 3.1.0', () => {
test('should return true', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.json');
const url = path.join(__dirname, '__fixtures__', 'sample-api.json');
const file = File({
uri: '/path/to/open-api.json',
data: fs.readFileSync(url),
Expand All @@ -80,7 +80,7 @@ describe('OpenApiJson3_1Parser', () => {

describe('and file data is string and can be detected as OpenAPI 3.1.0', () => {
test('should return true', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.json');
const url = path.join(__dirname, '__fixtures__', 'sample-api.json');
const file = File({
uri: '/path/to/open-api.json',
data: fs.readFileSync(url).toString(),
Expand All @@ -96,7 +96,7 @@ describe('OpenApiJson3_1Parser', () => {
describe('parse', () => {
describe('given OpenApi 3.1.x JSON data', () => {
test('should return parse result', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.json');
const url = path.join(__dirname, '__fixtures__', 'sample-api.json');
const data = fs.readFileSync(url).toString();
const file = File({
url,
Expand All @@ -112,7 +112,7 @@ describe('OpenApiJson3_1Parser', () => {

describe('given OpenApi 3.1.x JSON data as buffer', () => {
test('should return parse result', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.json');
const url = path.join(__dirname, '__fixtures__', 'sample-api.json');
const data = fs.readFileSync(url);
const file = File({
url,
Expand Down Expand Up @@ -174,7 +174,7 @@ describe('OpenApiJson3_1Parser', () => {

describe('given sourceMap disabled', () => {
test('should not decorate ApiDOM with source maps', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.json');
const url = path.join(__dirname, '__fixtures__', 'sample-api.json');
const data = fs.readFileSync(url).toString();
const file = File({
url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('OpenApiYaml3_1Parser', () => {
describe('given file with supported extension', () => {
describe('and file data is buffer and can be detected as OpenAPI 3.1.0', () => {
test('should return true', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.yaml');
const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml');
const file = File({
uri: '/path/to/open-api.yaml',
data: fs.readFileSync(url),
Expand All @@ -111,7 +111,7 @@ describe('OpenApiYaml3_1Parser', () => {

describe('and file data is string and can be detected as OpenAPI 3.1.0', () => {
test('should return true', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.yaml');
const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml');
const file = File({
uri: '/path/to/open-api.yaml',
data: fs.readFileSync(url).toString(),
Expand All @@ -127,7 +127,7 @@ describe('OpenApiYaml3_1Parser', () => {
describe('parse', () => {
describe('given OpenApi 3.1.x YAML data', () => {
test('should return parse result', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.yaml');
const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml');
const data = fs.readFileSync(url).toString();
const file = File({
url,
Expand All @@ -143,7 +143,7 @@ describe('OpenApiYaml3_1Parser', () => {

describe('given OpenApi 3.1.x YAML data as buffer', () => {
test('should return parse result', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.yaml');
const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml');
const data = fs.readFileSync(url);
const file = File({
url,
Expand Down Expand Up @@ -205,7 +205,7 @@ describe('OpenApiYaml3_1Parser', () => {

describe('given sourceMap disabled', () => {
test('should not decorate ApiDOM with source maps', async () => {
const url = path.join(__dirname, 'fixtures', 'sample-api.yaml');
const url = path.join(__dirname, '__fixtures__', 'sample-api.yaml');
const data = fs.readFileSync(url).toString();
const file = File({
url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('HttpResolverSwaggerClient', () => {
test('should throw on timeout', async () => {
resolver = HttpResolverSwaggerClient({ timeout: 1 });
const url = 'http://localhost:8123/local-file.txt';
const cwd = path.join(__dirname, 'fixtures');
const cwd = path.join(__dirname, '__fixtures__');
const server = globalThis.createHTTPServer({ port: 8123, cwd });

expect.assertions(3);
Expand Down
4 changes: 4 additions & 0 deletions test/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ fetchMock.config.Headers = Headers;
// provide AbortController for older Node.js versions
globalThis.AbortController = globalThis.AbortController ?? AbortController;

// helpers for reading local files
globalThis.loadFile = (uri) => fs.readFileSync(uri).toString();
globalThis.loadJsonFile = (uri) => JSON.parse(globalThis.loadFile(uri));

// helper for providing HTTP server instance for testing
globalThis.createHTTPServer = ({ port = 8123, cwd = process.cwd() } = {}) => {
const server = http.createServer((req, res) => {
Expand Down

0 comments on commit 41e6bf1

Please sign in to comment.