Skip to content

Commit

Permalink
feat(ns-openapi-3-1): make servers refractor plugin idempotent (#4152)
Browse files Browse the repository at this point in the history
Refs #4134
  • Loading branch information
glowcloud authored May 29, 2024
1 parent 858cec6 commit 93e2d0f
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Namespace } from '@swagger-api/apidom-core';
import { Element } from '@swagger-api/apidom-core';
import {
PathItemServersElement,
OperationServersElement,
Expand All @@ -9,7 +9,8 @@ import type OpenApi3_1Element from '../../elements/OpenApi3-1';
import type PathItemElement from '../../elements/PathItem';
import type ServerElement from '../../elements/Server';
import type OperationElement from '../../elements/Operation';
import type { Predicates } from '../toolbox';
import type { Toolbox } from '../toolbox';
import NormalizeStorage from './normalize-header-examples/NormalizeStorage';

/**
* Override of Server Objects.
Expand All @@ -24,36 +25,60 @@ import type { Predicates } from '../toolbox';
* If an alternative server object is specified at the Operation Object level, it will override PathItem.servers and OpenAPI.servers respectively.
*/

interface PluginOptions {
storageField?: string;
}

/* eslint-disable no-param-reassign */
const plugin =
() =>
({ predicates, namespace }: { predicates: Predicates; namespace: Namespace }) => {
({ storageField = 'x-normalized' }: PluginOptions = {}) =>
(toolbox: Toolbox) => {
const { namespace, ancestorLineageToJSONPointer, predicates } = toolbox;
let storage: NormalizeStorage | undefined;

return {
visitor: {
OpenApi3_1Element(openapiElement: OpenApi3_1Element) {
const isServersUndefined = typeof openapiElement.servers === 'undefined';
const isServersArrayElement = predicates.isArrayElement(openapiElement.servers);
const isServersEmpty = isServersArrayElement && openapiElement.servers!.length === 0;
// @ts-ignore
const defaultServer = namespace.elements.Server.refract({ url: '/' });

if (isServersUndefined || !isServersArrayElement) {
openapiElement.servers = new ServersElement([defaultServer]);
} else if (isServersArrayElement && isServersEmpty) {
openapiElement.servers!.push(defaultServer);
}
OpenApi3_1Element: {
enter(openapiElement: OpenApi3_1Element) {
const isServersUndefined = typeof openapiElement.servers === 'undefined';
const isServersArrayElement = predicates.isArrayElement(openapiElement.servers);
const isServersEmpty = isServersArrayElement && openapiElement.servers!.length === 0;
// @ts-ignore
const defaultServer = namespace.elements.Server.refract({ url: '/' });

if (isServersUndefined || !isServersArrayElement) {
openapiElement.servers = new ServersElement([defaultServer]);
} else if (isServersArrayElement && isServersEmpty) {
openapiElement.servers!.push(defaultServer);
}
storage = new NormalizeStorage(openapiElement, storageField, 'servers');
},
leave() {
storage = undefined;
},
},
PathItemElement(
pathItemElement: PathItemElement,
key: any,
parent: any,
path: any,
ancestors: any[],
key: string | number,
parent: Element | undefined,
path: (string | number)[],
ancestors: [Element | Element[]],
) {
// skip visiting this Path Item
if (ancestors.some(predicates.isComponentsElement)) return;
if (!ancestors.some(predicates.isOpenApi3_1Element)) return;

const pathItemJSONPointer = ancestorLineageToJSONPointer([
...ancestors,
parent!,
pathItemElement,
]);

// skip visiting this Path Item Object if it's already normalized
if (storage!.includes(pathItemJSONPointer)) {
return;
}

const parentOpenapiElement = ancestors.find(predicates.isOpenApi3_1Element);
const isServersUndefined = typeof pathItemElement.servers === 'undefined';
const isServersArrayElement = predicates.isArrayElement(pathItemElement.servers);
Expand All @@ -71,19 +96,31 @@ const plugin =
pathItemElement.servers!.push(server);
});
}
storage!.append(pathItemJSONPointer);
}
},
OperationElement(
operationElement: OperationElement,
key: any,
parent: any,
path: any,
ancestors: any[],
key: string | number,
parent: Element | undefined,
path: (string | number)[],
ancestors: [Element | Element[]],
) {
// skip visiting this Operation
if (ancestors.some(predicates.isComponentsElement)) return;
if (!ancestors.some(predicates.isOpenApi3_1Element)) return;

const operationJSONPointer = ancestorLineageToJSONPointer([
...ancestors,
parent!,
operationElement,
]);

// skip visiting this Operation Object if it's already normalized
if (storage!.includes(operationJSONPointer)) {
return;
}

// @TODO([email protected]): can be replaced by Array.prototype.findLast in future
const parentPathItemElement = [...ancestors].reverse().find(predicates.isPathItemElement);
const isServersUndefined = typeof operationElement.servers === 'undefined';
Expand All @@ -102,6 +139,7 @@ const plugin =
operationElement.servers!.push(server);
});
}
storage!.append(operationJSONPointer);
}
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`refractor plugins normalize-servers should have idempotent characteristics 1`] = `
Object {
openapi: 3.1.0,
paths: Object {
/: Object {
get: Object {},
},
},
servers: Array [
Object {
description: production server,
url: https://example.com/,
},
],
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ exports[`refractor plugins normalize-servers given OpenAPI.server defined should
(StringElement))
(MemberElement
(StringElement)
(StringElement))))))))))
(StringElement)))))))))
(MemberElement
(StringElement)
(ObjectElement
(MemberElement
(StringElement)
(ArrayElement
(StringElement)
(StringElement))))))
`;

exports[`refractor plugins normalize-servers given OpenAPI.servers defined and PathItem.servers defined should duplicate Server Objects from PathItem.servers 1`] = `
Expand Down Expand Up @@ -517,6 +525,42 @@ exports[`refractor plugins normalize-servers given OpenAPI.servers defined and P
]
}
}
},
{
"element": "member",
"content": {
"key": {
"element": "string",
"content": "x-normalized"
},
"value": {
"element": "object",
"content": [
{
"element": "member",
"content": {
"key": {
"element": "string",
"content": "servers"
},
"value": {
"element": "array",
"content": [
{
"element": "string",
"content": "/paths/~1"
},
{
"element": "string",
"content": "/paths/~1/get"
}
]
}
}
}
]
}
}
}
]
}
Expand Down Expand Up @@ -561,6 +605,14 @@ exports[`refractor plugins normalize-servers given PathItem.servers defined shou
(ArrayElement
(ServerElement
(MemberElement
(StringElement)
(StringElement)))))
(MemberElement
(StringElement)
(ObjectElement
(MemberElement
(StringElement)
(ArrayElement
(StringElement)
(StringElement))))))
`;
Expand Down Expand Up @@ -598,6 +650,14 @@ exports[`refractor plugins normalize-servers given no servers field is defined s
(ArrayElement
(ServerElement
(MemberElement
(StringElement)
(StringElement)))))
(MemberElement
(StringElement)
(ObjectElement
(MemberElement
(StringElement)
(ArrayElement
(StringElement)
(StringElement))))))
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect } from 'chai';
import dedent from 'dedent';
import { toValue, dispatchRefractorPlugins } from '@swagger-api/apidom-core';
import { parse } from '@swagger-api/apidom-parser-adapter-yaml-1-2';

import {
createToolbox,
OpenApi3_1Element,
refractorPluginNormalizeServers,
keyMap,
getNodeType,
} from '../../../../src';

describe('refractor', function () {
context('plugins', function () {
context('normalize-servers', function () {
specify('should have idempotent characteristics', async function () {
const yamlDefinition = dedent`
openapi: 3.1.0
servers:
- url: https://example.com/
description: production server
paths:
/:
get: {}
`;
const apiDOM = await parse(yamlDefinition);
const openApiElement = OpenApi3_1Element.refract(apiDOM.result) as OpenApi3_1Element;
const options = {
toolboxCreator: createToolbox,
visitorOptions: { keyMap, nodeTypeGetter: getNodeType },
};

dispatchRefractorPlugins(openApiElement, [refractorPluginNormalizeServers()], options);
dispatchRefractorPlugins(openApiElement, [refractorPluginNormalizeServers()], options);
dispatchRefractorPlugins(openApiElement, [refractorPluginNormalizeServers()], options);

expect(toValue(apiDOM.result)).toMatchSnapshot();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { assert } from 'chai';
import dedent from 'dedent';
import { toValue } from '@swagger-api/apidom-core';
import { parse } from '@swagger-api/apidom-parser-adapter-yaml-1-2';

import { OpenApi3_1Element, refractorPluginNormalizeServers } from '../../../../src';

describe('refractor', function () {
context('plugins', function () {
context('normalize-servers', function () {
specify('should use sub-field to store normalized scopes', async function () {
const yamlDefinition = dedent`
openapi: 3.1.0
servers:
- url: https://example.com/
description: production server
paths:
/:
get: {}
`;
const apiDOM = await parse(yamlDefinition);
const openApiElement = OpenApi3_1Element.refract(apiDOM.result, {
plugins: [refractorPluginNormalizeServers()],
}) as OpenApi3_1Element;

assert.deepEqual(toValue(openApiElement.get('x-normalized')), {
servers: ['/paths/~1', '/paths/~1/get'],
});
});

context('given custom storage field', function () {
specify('should use custom storage field to store normalized scopes', async function () {
const yamlDefinition = dedent`
openapi: 3.1.0
servers:
- url: https://example.com/
description: production server
paths:
/:
get: {}
`;
const apiDOM = await parse(yamlDefinition);
const openApiElement = OpenApi3_1Element.refract(apiDOM.result, {
plugins: [
refractorPluginNormalizeServers({
storageField: '$$normalized',
}),
],
}) as OpenApi3_1Element;

assert.deepEqual(toValue(openApiElement.get('$$normalized')), {
servers: ['/paths/~1', '/paths/~1/get'],
});
});
});
});
});
});

0 comments on commit 93e2d0f

Please sign in to comment.