diff --git a/packages/apidom-ns-openapi-3-1/src/refractor/plugins/normalize-servers.ts b/packages/apidom-ns-openapi-3-1/src/refractor/plugins/normalize-servers.ts index aee1be917..7f73b40a0 100644 --- a/packages/apidom-ns-openapi-3-1/src/refractor/plugins/normalize-servers.ts +++ b/packages/apidom-ns-openapi-3-1/src/refractor/plugins/normalize-servers.ts @@ -1,4 +1,4 @@ -import type { Namespace } from '@swagger-api/apidom-core'; +import { Element } from '@swagger-api/apidom-core'; import { PathItemServersElement, OperationServersElement, @@ -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. @@ -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); @@ -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(vladimir.gorej@gmail.com): can be replaced by Array.prototype.findLast in future const parentPathItemElement = [...ancestors].reverse().find(predicates.isPathItemElement); const isServersUndefined = typeof operationElement.servers === 'undefined'; @@ -102,6 +139,7 @@ const plugin = operationElement.servers!.push(server); }); } + storage!.append(operationJSONPointer); } }, }, diff --git a/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/idempotence.ts.snap b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/idempotence.ts.snap new file mode 100644 index 000000000..94f93e7d7 --- /dev/null +++ b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/idempotence.ts.snap @@ -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/, + }, + ], +} +`; diff --git a/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/index.ts.snap b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/index.ts.snap index f4265c393..9bc06374b 100644 --- a/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/index.ts.snap +++ b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/__snapshots__/index.ts.snap @@ -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`] = ` @@ -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" + } + ] + } + } + } + ] + } + } } ] } @@ -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)))))) `; @@ -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)))))) `; diff --git a/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/idempotence.ts b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/idempotence.ts new file mode 100644 index 000000000..dae12e868 --- /dev/null +++ b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/idempotence.ts @@ -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(); + }); + }); + }); +}); diff --git a/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/storage-fields.ts b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/storage-fields.ts new file mode 100644 index 000000000..b9ead5b28 --- /dev/null +++ b/packages/apidom-ns-openapi-3-1/test/refractor/plugins/normalize-servers/storage-fields.ts @@ -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'], + }); + }); + }); + }); + }); +});