diff --git a/src/execute/index.js b/src/execute/index.js index 258c79fa7..064614b00 100755 --- a/src/execute/index.js +++ b/src/execute/index.js @@ -9,7 +9,7 @@ import SWAGGER2_PARAMETER_BUILDERS from './swagger2/parameter-builders.js'; import * as OAS3_PARAMETER_BUILDERS from './oas3/parameter-builders.js'; import oas3BuildRequest from './oas3/build-request.js'; import swagger2BuildRequest from './swagger2/build-request.js'; -import { getOperationRaw, legacyIdFromPathMethod } from '../helpers/index.js'; +import { getOperationRaw, idFromPathMethodLegacy } from '../helpers/index.js'; import { isOpenAPI3 } from '../helpers/openapi-predicates.js'; const arrayOrEmpty = (ar) => (Array.isArray(ar) ? ar : []); @@ -65,7 +65,7 @@ export function execute({ const http = userHttp || fetch || stockHttp; // Default to _our_ http if (pathName && method && !operationId) { - operationId = legacyIdFromPathMethod(pathName, method); + operationId = idFromPathMethodLegacy(pathName, method); } const request = self.buildRequest({ diff --git a/src/helpers/each-operation.js b/src/helpers/each-operation.js new file mode 100644 index 000000000..c78120fd2 --- /dev/null +++ b/src/helpers/each-operation.js @@ -0,0 +1,38 @@ +// iterate over each operation, and fire a callback with details +// `find=true` will stop iterating, when the cb returns truthy +export default function eachOperation(spec, cb, find) { + if (!spec || typeof spec !== 'object' || !spec.paths || typeof spec.paths !== 'object') { + return null; + } + + const { paths } = spec; + + // Iterate over the spec, collecting operations + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const pathName in paths) { + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const method in paths[pathName]) { + if (method.toUpperCase() === 'PARAMETERS') { + continue; // eslint-disable-line no-continue + } + const operation = paths[pathName][method]; + if (!operation || typeof operation !== 'object') { + continue; // eslint-disable-line no-continue + } + + const operationObj = { + spec, + pathName, + method: method.toUpperCase(), + operation, + }; + const cbValue = cb(operationObj); + + if (find && cbValue) { + return operationObj; + } + } + } + + return undefined; +} diff --git a/src/helpers/find-operation.js b/src/helpers/find-operation.js new file mode 100644 index 000000000..bd326cd58 --- /dev/null +++ b/src/helpers/find-operation.js @@ -0,0 +1,7 @@ +import eachOperation from './each-operation.js'; + +// Will stop iterating over the operations and return the operationObj +// as soon as predicate returns true +export default function findOperation(spec, predicate) { + return eachOperation(spec, predicate, true) || null; +} diff --git a/src/helpers/get-operation-raw.js b/src/helpers/get-operation-raw.js new file mode 100644 index 000000000..ca14ab816 --- /dev/null +++ b/src/helpers/get-operation-raw.js @@ -0,0 +1,21 @@ +import findOperation from './find-operation.js'; +import opId from './op-id.js'; +import idFromPathMethodLegacy from './id-from-path-method/legacy.js'; + +export default function getOperationRaw(spec, id) { + if (!spec || !spec.paths) { + return null; + } + + return findOperation(spec, ({ pathName, method, operation }) => { + if (!operation || typeof operation !== 'object') { + return false; + } + + const rawOperationId = operation.operationId; // straight from the source + const operationId = opId(operation, pathName, method); + const legacyOperationId = idFromPathMethodLegacy(pathName, method); + + return [operationId, legacyOperationId, rawOperationId].some((val) => val && val === id); + }); +} diff --git a/src/helpers/id-from-path-method/index.js b/src/helpers/id-from-path-method/index.js new file mode 100644 index 000000000..3013fba61 --- /dev/null +++ b/src/helpers/id-from-path-method/index.js @@ -0,0 +1,22 @@ +import replaceSpecialCharsWithUnderscore from '../replace-special-chars-with-underscore.js'; + +export default function idFromPathMethod( + pathName, + method, + { v2OperationIdCompatibilityMode } = {} +) { + if (v2OperationIdCompatibilityMode) { + let res = `${method.toLowerCase()}_${pathName}`.replace( + /[\s!@#$%^&*()_+=[{\]};:<>|./?,\\'""-]/g, + '_' + ); + + res = res || `${pathName.substring(1)}_${method}`; + + return res + .replace(/((_){2,})/g, '_') + .replace(/^(_)*/g, '') + .replace(/([_])*$/g, ''); + } + return `${method.toLowerCase()}${replaceSpecialCharsWithUnderscore(pathName)}`; +} diff --git a/src/helpers/id-from-path-method/legacy.js b/src/helpers/id-from-path-method/legacy.js new file mode 100644 index 000000000..aca04f2b5 --- /dev/null +++ b/src/helpers/id-from-path-method/legacy.js @@ -0,0 +1,3 @@ +export default function idFromPathMethodLegacy(pathName, method) { + return `${method.toLowerCase()}-${pathName}`; +} diff --git a/src/helpers/index.js b/src/helpers/index.js index 38184428b..51ee44d34 100755 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -1,211 +1,6 @@ -const toLower = (str) => String.prototype.toLowerCase.call(str); -const escapeString = (str) => str.replace(/[^\w]/gi, '_'); - -// Strategy for determining operationId -export function opId(operation, pathName, method = '', { v2OperationIdCompatibilityMode } = {}) { - if (!operation || typeof operation !== 'object') { - return null; - } - const idWithoutWhitespace = (operation.operationId || '').replace(/\s/g, ''); - if (idWithoutWhitespace.length) { - return escapeString(operation.operationId); - } - return idFromPathMethod(pathName, method, { v2OperationIdCompatibilityMode }); -} - -// Create a generated operationId from pathName + method -export function idFromPathMethod(pathName, method, { v2OperationIdCompatibilityMode } = {}) { - if (v2OperationIdCompatibilityMode) { - let res = `${method.toLowerCase()}_${pathName}`.replace( - /[\s!@#$%^&*()_+=[{\]};:<>|./?,\\'""-]/g, - '_' - ); - - res = res || `${pathName.substring(1)}_${method}`; - - return res - .replace(/((_){2,})/g, '_') - .replace(/^(_)*/g, '') - .replace(/([_])*$/g, ''); - } - return `${toLower(method)}${escapeString(pathName)}`; -} - -export function legacyIdFromPathMethod(pathName, method) { - return `${toLower(method)}-${pathName}`; -} - -// Get the operation, based on operationId ( just return the object, no inheritence ) -export function getOperationRaw(spec, id) { - if (!spec || !spec.paths) { - return null; - } - - return findOperation(spec, ({ pathName, method, operation }) => { - if (!operation || typeof operation !== 'object') { - return false; - } - - const rawOperationId = operation.operationId; // straight from the source - const operationId = opId(operation, pathName, method); - const legacyOperationId = legacyIdFromPathMethod(pathName, method); - - return [operationId, legacyOperationId, rawOperationId].some((val) => val && val === id); - }); -} - -// Will stop iterating over the operations and return the operationObj -// as soon as predicate returns true -export function findOperation(spec, predicate) { - return eachOperation(spec, predicate, true) || null; -} - -// iterate over each operation, and fire a callback with details -// `find=true` will stop iterating, when the cb returns truthy -export function eachOperation(spec, cb, find) { - if (!spec || typeof spec !== 'object' || !spec.paths || typeof spec.paths !== 'object') { - return null; - } - - const { paths } = spec; - - // Iterate over the spec, collecting operations - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const pathName in paths) { - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const method in paths[pathName]) { - if (method.toUpperCase() === 'PARAMETERS') { - continue; // eslint-disable-line no-continue - } - const operation = paths[pathName][method]; - if (!operation || typeof operation !== 'object') { - continue; // eslint-disable-line no-continue - } - - const operationObj = { - spec, - pathName, - method: method.toUpperCase(), - operation, - }; - const cbValue = cb(operationObj); - - if (find && cbValue) { - return operationObj; - } - } - } - - return undefined; -} - -// REVIEW: OAS3: identify normalization steps that need changes -// ...maybe create `normalizeOAS3`? - -export function normalizeSwagger(parsedSpec) { - const { spec } = parsedSpec; - const { paths } = spec; - const map = {}; - - if (!paths || spec.$$normalized) { - return parsedSpec; - } - - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const pathName in paths) { - const path = paths[pathName]; - - if (path == null || !['object', 'function'].includes(typeof path)) { - continue; // eslint-disable-line no-continue - } - - const pathParameters = path.parameters; - - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const method in path) { - const operation = path[method]; - if (operation == null || !['object', 'function'].includes(typeof operation)) { - continue; // eslint-disable-line no-continue - } - - const oid = opId(operation, pathName, method); - - if (oid) { - if (map[oid]) { - map[oid].push(operation); - } else { - map[oid] = [operation]; - } - - const opList = map[oid]; - if (opList.length > 1) { - opList.forEach((o, i) => { - // eslint-disable-next-line no-underscore-dangle - o.__originalOperationId = o.__originalOperationId || o.operationId; - o.operationId = `${oid}${i + 1}`; - }); - } else if (typeof operation.operationId !== 'undefined') { - // Ensure we always add the normalized operation ID if one already exists - // ( potentially different, given that we normalize our IDs) - // ... _back_ to the spec. Otherwise, they might not line up - const obj = opList[0]; - // eslint-disable-next-line no-underscore-dangle - obj.__originalOperationId = obj.__originalOperationId || operation.operationId; - obj.operationId = oid; - } - } - - if (method !== 'parameters') { - // Add inherited consumes, produces, parameters, securities - const inheritsList = []; - const toBeInherit = {}; - - // Global-levels - // eslint-disable-next-line no-restricted-syntax - for (const key in spec) { - if (key === 'produces' || key === 'consumes' || key === 'security') { - toBeInherit[key] = spec[key]; - inheritsList.push(toBeInherit); - } - } - - // Path-levels - if (pathParameters) { - toBeInherit.parameters = pathParameters; - inheritsList.push(toBeInherit); - } - - if (inheritsList.length) { - // eslint-disable-next-line no-restricted-syntax - for (const inherits of inheritsList) { - // eslint-disable-next-line no-restricted-syntax - for (const inheritName in inherits) { - if (!operation[inheritName]) { - operation[inheritName] = inherits[inheritName]; - } else if (inheritName === 'parameters') { - // eslint-disable-next-line no-restricted-syntax - for (const param of inherits[inheritName]) { - const exists = operation[inheritName].some( - (opParam) => - (opParam.name && opParam.name === param.name) || - (opParam.$ref && opParam.$ref === param.$ref) || - (opParam.$$ref && opParam.$$ref === param.$$ref) || - opParam === param - ); - - if (!exists) { - operation[inheritName].push(param); - } - } - } - } - } - } - } - } - } - - spec.$$normalized = true; - - return parsedSpec; -} +export { default as eachOperation } from './each-operation.js'; +export { default as findOperation } from './find-operation.js'; +export { default as getOperationRaw } from './get-operation-raw.js'; +export { default as opId } from './op-id.js'; +export { default as idFromPathMethod } from './id-from-path-method/index.js'; +export { default as idFromPathMethodLegacy } from './id-from-path-method/legacy.js'; diff --git a/src/helpers/normalize/openapi-2--3-0.js b/src/helpers/normalize/openapi-2--3-0.js new file mode 100644 index 000000000..a54986e1f --- /dev/null +++ b/src/helpers/normalize/openapi-2--3-0.js @@ -0,0 +1,109 @@ +import opId from '../op-id.js'; + +export default function normalize(parsedSpec) { + const { spec } = parsedSpec; + const { paths } = spec; + const map = {}; + + if (!paths || spec.$$normalized) { + return parsedSpec; + } + + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const pathName in paths) { + const path = paths[pathName]; + + if (path == null || !['object', 'function'].includes(typeof path)) { + continue; // eslint-disable-line no-continue + } + + const pathParameters = path.parameters; + + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const method in path) { + const operation = path[method]; + if (operation == null || !['object', 'function'].includes(typeof operation)) { + continue; // eslint-disable-line no-continue + } + + const oid = opId(operation, pathName, method); + + if (oid) { + if (map[oid]) { + map[oid].push(operation); + } else { + map[oid] = [operation]; + } + + const opList = map[oid]; + if (opList.length > 1) { + opList.forEach((o, i) => { + // eslint-disable-next-line no-underscore-dangle + o.__originalOperationId = o.__originalOperationId || o.operationId; + o.operationId = `${oid}${i + 1}`; + }); + } else if (typeof operation.operationId !== 'undefined') { + // Ensure we always add the normalized operation ID if one already exists + // ( potentially different, given that we normalize our IDs) + // ... _back_ to the spec. Otherwise, they might not line up + const obj = opList[0]; + // eslint-disable-next-line no-underscore-dangle + obj.__originalOperationId = obj.__originalOperationId || operation.operationId; + obj.operationId = oid; + } + } + + if (method !== 'parameters') { + // Add inherited consumes, produces, parameters, securities + const inheritsList = []; + const toBeInherit = {}; + + // Global-levels + // eslint-disable-next-line no-restricted-syntax + for (const key in spec) { + if (key === 'produces' || key === 'consumes' || key === 'security') { + toBeInherit[key] = spec[key]; + inheritsList.push(toBeInherit); + } + } + + // Path-levels + if (pathParameters) { + toBeInherit.parameters = pathParameters; + inheritsList.push(toBeInherit); + } + + if (inheritsList.length) { + // eslint-disable-next-line no-restricted-syntax + for (const inherits of inheritsList) { + // eslint-disable-next-line no-restricted-syntax + for (const inheritName in inherits) { + if (!operation[inheritName]) { + operation[inheritName] = inherits[inheritName]; + } else if (inheritName === 'parameters') { + // eslint-disable-next-line no-restricted-syntax + for (const param of inherits[inheritName]) { + const exists = operation[inheritName].some( + (opParam) => + (opParam.name && opParam.name === param.name) || + (opParam.$ref && opParam.$ref === param.$ref) || + (opParam.$$ref && opParam.$$ref === param.$$ref) || + opParam === param + ); + + if (!exists) { + operation[inheritName].push(param); + } + } + } + } + } + } + } + } + } + + spec.$$normalized = true; + + return parsedSpec; +} diff --git a/src/helpers/normalize/openapi-3-1.js b/src/helpers/normalize/openapi-3-1.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/helpers/op-id.js b/src/helpers/op-id.js new file mode 100644 index 000000000..5839d05a2 --- /dev/null +++ b/src/helpers/op-id.js @@ -0,0 +1,18 @@ +import idFromPathMethod from './id-from-path-method/index.js'; +import replaceSpecialCharsWithUnderscore from './replace-special-chars-with-underscore.js'; + +export default function opId( + operation, + pathName, + method = '', + { v2OperationIdCompatibilityMode } = {} +) { + if (!operation || typeof operation !== 'object') { + return null; + } + const idWithoutWhitespace = (operation.operationId || '').replace(/\s/g, ''); + if (idWithoutWhitespace.length) { + return replaceSpecialCharsWithUnderscore(operation.operationId); + } + return idFromPathMethod(pathName, method, { v2OperationIdCompatibilityMode }); +} diff --git a/src/helpers/replace-special-chars-with-underscore.js b/src/helpers/replace-special-chars-with-underscore.js new file mode 100644 index 000000000..4f6d752bf --- /dev/null +++ b/src/helpers/replace-special-chars-with-underscore.js @@ -0,0 +1,3 @@ +const replaceSpecialCharsWithUnderscore = (operationId) => operationId.replace(/\W/gi, '_'); + +export default replaceSpecialCharsWithUnderscore; diff --git a/src/resolver.js b/src/resolver.js index e2a326a71..6f2ab784c 100644 --- a/src/resolver.js +++ b/src/resolver.js @@ -1,6 +1,7 @@ import Http from './http/index.js'; import mapSpec, { plugins } from './specmap/index.js'; -import { normalizeSwagger } from './helpers/index.js'; +// eslint-disable-next-line camelcase +import normalizeOpenAPI2__30 from './helpers/normalize/openapi-2--3-0.js'; import { ACCEPT_HEADER_VALUE_FOR_DOCUMENTS } from './constants.js'; export function makeFetchJSON(http, opts = {}) { @@ -91,6 +92,7 @@ export default function resolve(obj) { parameterMacro, modelPropertyMacro, useCircularStructures, - }).then(skipNormalization ? async (a) => a : normalizeSwagger); + // eslint-disable-next-line camelcase + }).then(skipNormalization ? async (a) => a : normalizeOpenAPI2__30); } } diff --git a/src/subtree-resolver/index.js b/src/subtree-resolver/index.js index 210ef9c1a..be8343666 100644 --- a/src/subtree-resolver/index.js +++ b/src/subtree-resolver/index.js @@ -24,7 +24,8 @@ import get from 'lodash/get'; import resolve from '../resolver.js'; -import { normalizeSwagger } from '../helpers/index.js'; +// eslint-disable-next-line camelcase +import normalizeOpenAPI2__30 from '../helpers/normalize/openapi-2--3-0.js'; export default async function resolveSubtree(obj, path, opts = {}) { const { @@ -47,7 +48,7 @@ export default async function resolveSubtree(obj, path, opts = {}) { useCircularStructures, }; - const { spec: normalized } = normalizeSwagger({ + const { spec: normalized } = normalizeOpenAPI2__30({ spec: obj, }); diff --git a/test/execute/main.js b/test/execute/main.js index 27bef2d50..ae770d116 100644 --- a/test/execute/main.js +++ b/test/execute/main.js @@ -2,7 +2,8 @@ import { Readable } from 'stream'; import AbortController from 'abort-controller'; import { execute, buildRequest, self as stubs } from '../../src/execute/index.js'; -import { normalizeSwagger } from '../../src/helpers/index.js'; +// eslint-disable-next-line camelcase +import normalizeOpenAPI2__30 from '../../src/helpers/normalize/openapi-2--3-0.js'; // Supported shape... { spec, operationId, parameters, securities, fetch } // One can use operationId or pathItem + method @@ -2032,7 +2033,7 @@ describe('execute', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); const warnSpy = jest.spyOn(console, 'warn'); const req = buildRequest({ spec: resultSpec.spec, @@ -2086,7 +2087,7 @@ describe('execute', () => { const oriWarn = global.console.warn; global.console.warn = jest.fn(); - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); const req = buildRequest({ spec: resultSpec.spec, operationId: 'getPetsById', diff --git a/test/helpers/index.js b/test/helpers/index.js index 852f80db6..961ea50bc 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -1,4 +1,6 @@ -import { normalizeSwagger, getOperationRaw, idFromPathMethod } from '../../src/helpers/index.js'; +import { getOperationRaw, idFromPathMethod } from '../../src/helpers/index.js'; +// eslint-disable-next-line camelcase +import normalizeOpenAPI2__30 from '../../src/helpers/normalize/openapi-2--3-0.js'; describe('helpers', () => { describe('idFromPathMethod', () => { @@ -136,7 +138,7 @@ describe('helpers', () => { }); }); - describe('normalizeSwagger', () => { + describe('normalizeOpenAPI2__30', () => { describe('operationId', () => { test('should create unique operationIds when explicit operationIds are duplicates, and preserve originals', () => { const input = { @@ -161,7 +163,7 @@ describe('helpers', () => { }, }; - const res = normalizeSwagger(input); + const res = normalizeOpenAPI2__30(input); const fooRes = res.spec.paths['/foo'].get; const barRes = res.spec.paths['/bar'].get; const bazRes = res.spec.paths['/baz'].get; @@ -190,7 +192,7 @@ describe('helpers', () => { }; // When - const normalizedSpec = normalizeSwagger(spec); + const normalizedSpec = normalizeOpenAPI2__30(spec); const id = normalizedSpec.spec.paths['/foo'].get.operationId; // Then @@ -222,7 +224,7 @@ describe('helpers', () => { }; // When - normalizeSwagger(input); + normalizeOpenAPI2__30(input); const fooOperation = input.spec.paths['/foo'].get; const barOperation = input.spec.paths['/bar'].get; const bazOperation = input.spec.paths['/baz'].get; @@ -263,7 +265,7 @@ describe('helpers', () => { }; // When - normalizeSwagger(input); + normalizeOpenAPI2__30(input); const fooOperation = input.spec.paths['/foo'].get; const barOperation = input.spec.paths['/bar'].get; const bazOperation = input.spec.paths['/baz'].get; @@ -294,7 +296,7 @@ describe('helpers', () => { }; // When - const normalizedSpec = normalizeSwagger(spec); + const normalizedSpec = normalizeOpenAPI2__30(spec); const originalId = normalizedSpec.spec.paths['/foo'].get.__originalOperationId; // Then @@ -317,7 +319,7 @@ describe('helpers', () => { }; // When - const normalizedSpec = normalizeSwagger(spec); + const normalizedSpec = normalizeOpenAPI2__30(spec); const fooGet = normalizedSpec.spec.paths['/foo'].get; const fooPost = normalizedSpec.spec.paths['/foo'].post; @@ -349,7 +351,7 @@ describe('helpers', () => { }, }; - const id = normalizeSwagger(spec); + const id = normalizeOpenAPI2__30(spec); const id1 = id.spec.paths['/foo'].get.operationId; const id2 = id.spec.paths['/bar'].get.operationId; const id3 = id.spec.paths['/bat'].get.operationId; @@ -375,7 +377,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: { @@ -404,7 +406,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: { @@ -437,7 +439,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: { @@ -466,7 +468,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: { @@ -499,7 +501,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: { @@ -528,7 +530,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: { @@ -559,7 +561,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: { @@ -596,7 +598,7 @@ describe('helpers', () => { }, }; - const resultSpec = normalizeSwagger(spec); + const resultSpec = normalizeOpenAPI2__30(spec); expect(resultSpec).toEqual({ spec: {