From a5c6ea8863f2a2fb2a8057aea4bde5f1de7a3f71 Mon Sep 17 00:00:00 2001 From: Vladimir Gorej Date: Wed, 18 Jan 2023 14:29:10 +0100 Subject: [PATCH] refactor(resolver): apply SRP for OpenAPI 3.1 deref strategy SRP = Single Responsibility Principle Refs #2750 --- .../openapi-3-1-swagger-client/index.js | 38 +- .../{visitor.js => visitors/dereference.js} | 393 ++++++++---------- .../visitors/parameters.js | 27 ++ .../visitors/properties.js | 18 + 4 files changed, 244 insertions(+), 232 deletions(-) rename src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/{visitor.js => visitors/dereference.js} (56%) create mode 100644 src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/parameters.js create mode 100644 src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/properties.js diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js index acd911bf3..9559067c5 100644 --- a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js @@ -1,10 +1,12 @@ /* eslint-disable camelcase */ -import { createNamespace, visit } from '@swagger-api/apidom-core'; +import { createNamespace, visit, mergeAllVisitors } 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'; +import OpenApi3_1SwaggerClientDereferenceVisitor from './visitors/dereference.js'; +import ParameterMacroVisitor from './visitors/parameters.js'; +import ModelPropertyMacroVisitor from './visitors/properties.js'; const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; @@ -29,6 +31,7 @@ const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy }, methods: { async dereference(file, options) { + const visitors = []; const namespace = createNamespace(openApi3_1Namespace); const refSet = options.dereference.refSet ?? ReferenceSet(); let reference; @@ -41,16 +44,39 @@ const OpenApi3_1SwaggerClientDereferenceStrategy = OpenApi3_1DereferenceStrategy reference = refSet.find((ref) => ref.uri === file.uri); } - const visitor = OpenApi3_1SwaggerClientDereferenceVisitor({ + // create main dereference visitor + const dereferenceVisitor = OpenApi3_1SwaggerClientDereferenceVisitor({ reference, namespace, options, useCircularStructures: this.useCircularStructures, allowMetaPatches: this.allowMetaPatches, - parameterMacro: this.parameterMacro, - modelPropertyMacro: this.modelPropertyMacro, }); - const dereferencedElement = await visitAsync(refSet.rootRef.value, visitor, { + visitors.push(dereferenceVisitor); + + // create parameter macro visitor (if necessary) + if (typeof this.parameterMacro === 'function') { + const parameterMacroVisitor = ParameterMacroVisitor({ + parameterMacro: this.parameterMacro, + }); + visitors.push(parameterMacroVisitor); + } + + // create model property macro visitor (if necessary) + if (typeof this.modelPropertyMacro === 'function') { + const modelPropertyMacroVisitor = ModelPropertyMacroVisitor({ + modelPropertyMacro: this.modelPropertyMacro, + }); + visitors.push(modelPropertyMacroVisitor); + } + + // determine the root visitor + const rootVisitor = + visitors.length === 1 + ? visitors[0] + : mergeAllVisitors(visitors, { nodeTypeGetter: getNodeType }); + + const dereferencedElement = await visitAsync(refSet.rootRef.value, rootVisitor, { keyMap, nodeTypeGetter: getNodeType, }); diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitor.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js similarity index 56% rename from src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitor.js rename to src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js index 1da7103af..d51f1eff2 100644 --- a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitor.js +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js @@ -4,7 +4,6 @@ import { isPrimitiveElement, isStringElement, visit, - toValue, includesClasses, } from '@swagger-api/apidom-core'; import { @@ -41,29 +40,20 @@ import { const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; -const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.init( - function _OpenApi3_1SwaggerClientDereferenceVisitor({ - useCircularStructures = true, - allowMetaPatches = false, - parameterMacro = null, - modelPropertyMacro = null, +const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.compose({ + props: { + useCircularStructures: true, + allowMetaPatches: false, + }, + init({ + allowMetaPatches = this.allowMetaPatches, + useCircularStructures = this.useCircularStructures, }) { - let parameterMacroOperation = null; - - // props - this.useCircularStructures = useCircularStructures; this.allowMetaPatches = allowMetaPatches; - this.parameterMacro = parameterMacro; - this.modelPropertyMacro = modelPropertyMacro; - - // methods - this.ReferenceElement = async function _ReferenceElement( - referenceElement, - key, - parent, - path, - ancestors - ) { + this.useCircularStructures = useCircularStructures; + }, + methods: { + async ReferenceElement(referenceElement, key, parent, path, ancestors) { const [ancestorsLineage, directAncestors] = this.toAncestorLineage(ancestors); // skip already identified cycled Path Item Objects @@ -132,8 +122,6 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.i ancestors: ancestorsLineage, allowMetaPatches: this.allowMetaPatches, useCircularStructures: this.useCircularStructures, - parameterMacro: this.parameterMacro, - modelPropertyMacro: this.modelPropertyMacro, }); fragment = await visitAsync(fragment, visitor, { keyMap, nodeTypeGetter: getNodeType }); @@ -191,15 +179,8 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.i // transclude the element for a fragment return fragment; - }; - - this.PathItemElement = async function _PathItemElement( - pathItemElement, - key, - parent, - path, - ancestors - ) { + }, + async PathItemElement(pathItemElement, key, parent, path, ancestors) { const [ancestorsLineage, directAncestors] = this.toAncestorLineage(ancestors); // ignore PathItemElement without $ref field @@ -263,8 +244,6 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.i ancestors: ancestorsLineage, allowMetaPatches: this.allowMetaPatches, useCircularStructures: this.useCircularStructures, - parameterMacro: this.parameterMacro, - modelPropertyMacro: this.modelPropertyMacro, }); referencedElement = await visitAsync(referencedElement, visitor, { keyMap, @@ -325,55 +304,77 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.i // transclude referencing element with merged referenced element return mergedPathItemElement; - }; - - this.SchemaElement = { - enter: async (referencingElement, key, parent, path, ancestors) => { - const [ancestorsLineage, directAncestors] = this.toAncestorLineage(ancestors); + }, + async SchemaElement(referencingElement, key, parent, path, ancestors) { + const [ancestorsLineage, directAncestors] = this.toAncestorLineage(ancestors); - // skip current referencing schema as $ref keyword was not defined - if (!isStringElement(referencingElement.$ref)) { - // skip traversing this schema but traverse all it's child schemas - return undefined; - } + // skip current referencing schema as $ref keyword was not defined + if (!isStringElement(referencingElement.$ref)) { + // skip traversing this schema but traverse all it's child schemas + return undefined; + } - // skip already identified cycled schemas - if (includesClasses(['cycle'], referencingElement.$ref)) { - return false; - } + // skip already identified cycled schemas + if (includesClasses(['cycle'], referencingElement.$ref)) { + return false; + } - // detect possible cycle in traversal and avoid it - if (ancestorsLineage.some((ancs) => ancs.has(referencingElement))) { - // skip processing this schema and all it's child schemas - return false; - } + // detect possible cycle in traversal and avoid it + if (ancestorsLineage.some((ancs) => ancs.has(referencingElement))) { + // skip processing this schema and all it's child schemas + return false; + } - // compute baseURI using rules around $id and $ref keywords - let { reference } = this; - let { uri: retrievalURI } = reference; - const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement); - const $refBaseURIStrippedHash = url.stripHash($refBaseURI); - const file = File({ uri: $refBaseURIStrippedHash }); - const isUnknownURI = !this.options.resolve.resolvers.some((r) => r.canRead(file)); - const isURL = !isUnknownURI; - const isExternal = isURL && retrievalURI !== $refBaseURIStrippedHash; - - // ignore resolving external Schema Objects - if (!this.options.resolve.external && isExternal) { - // skip traversing this schema but traverse all it's child schemas - return undefined; - } + // compute baseURI using rules around $id and $ref keywords + let { reference } = this; + let { uri: retrievalURI } = reference; + const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement); + const $refBaseURIStrippedHash = url.stripHash($refBaseURI); + const file = File({ uri: $refBaseURIStrippedHash }); + const isUnknownURI = !this.options.resolve.resolvers.some((r) => r.canRead(file)); + const isURL = !isUnknownURI; + const isExternal = isURL && retrievalURI !== $refBaseURIStrippedHash; + + // ignore resolving external Schema Objects + if (!this.options.resolve.external && isExternal) { + // skip traversing this schema but traverse all it's child schemas + return undefined; + } - this.indirections.push(referencingElement); + this.indirections.push(referencingElement); - // determining reference, proper evaluation and selection mechanism - let referencedElement; + // determining reference, proper evaluation and selection mechanism + let referencedElement; - try { - if (isUnknownURI || isURL) { - // we're dealing with canonical URI or URL with possible fragment - const selector = $refBaseURI; - referencedElement = uriEvaluate( + try { + if (isUnknownURI || isURL) { + // we're dealing with canonical URI or URL with possible fragment + const selector = $refBaseURI; + referencedElement = uriEvaluate( + selector, + maybeRefractToSchemaElement(reference.value.result) + ); + } else { + // we're assuming here that we're dealing with JSON Pointer here + reference = await this.toReference(url.unsanitize($refBaseURI)); + retrievalURI = reference.uri; + const selector = uriToPointer($refBaseURI); + referencedElement = maybeRefractToSchemaElement( + jsonPointerEvaluate(selector, reference.value.result) + ); + } + } catch (error) { + /** + * No SchemaElement($id=URL) was not found, so we're going to try to resolve + * the URL and assume the returned response is a JSON Schema. + */ + if (isURL && error instanceof EvaluationJsonSchemaUriError) { + if (isAnchor(uriToAnchor($refBaseURI))) { + // we're dealing with JSON Schema $anchor here + reference = await this.toReference(url.unsanitize($refBaseURI)); + retrievalURI = reference.uri; + const selector = uriToAnchor($refBaseURI); + referencedElement = $anchorEvaluate( selector, maybeRefractToSchemaElement(reference.value.result) ); @@ -386,173 +387,113 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.i jsonPointerEvaluate(selector, reference.value.result) ); } - } catch (error) { - /** - * No SchemaElement($id=URL) was not found, so we're going to try to resolve - * the URL and assume the returned response is a JSON Schema. - */ - if (isURL && error instanceof EvaluationJsonSchemaUriError) { - if (isAnchor(uriToAnchor($refBaseURI))) { - // we're dealing with JSON Schema $anchor here - reference = await this.toReference(url.unsanitize($refBaseURI)); - retrievalURI = reference.uri; - const selector = uriToAnchor($refBaseURI); - referencedElement = $anchorEvaluate( - selector, - maybeRefractToSchemaElement(reference.value.result) - ); - } else { - // we're assuming here that we're dealing with JSON Pointer here - reference = await this.toReference(url.unsanitize($refBaseURI)); - retrievalURI = reference.uri; - const selector = uriToPointer($refBaseURI); - referencedElement = maybeRefractToSchemaElement( - jsonPointerEvaluate(selector, reference.value.result) - ); - } - } else { - throw error; - } - } - - // detect direct or indirect reference - if (this.indirections.includes(referencedElement)) { - throw new Error('Recursive JSON Pointer detected'); - } - - // detect maximum depth of dereferencing - if (this.indirections.length > this.options.dereference.maxDepth) { - throw new MaximumDereferenceDepthError( - `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` - ); + } else { + throw error; } + } - // append referencing schema to ancestors lineage - directAncestors.add(referencingElement); - - // dive deep into the fragment - const mergeVisitor = OpenApi3_1SwaggerClientDereferenceVisitor({ - reference, - namespace: this.namespace, - indirections: [...this.indirections], - options: this.options, - useCircularStructures: this.useCircularStructures, - allowMetaPatches: this.allowMetaPatches, - parameterMacro: this.parameterMacro, - modelPropertyMacro: this.modelPropertyMacro, - ancestors: ancestorsLineage, - }); - referencedElement = await visitAsync(referencedElement, mergeVisitor, { - keyMap, - nodeTypeGetter: getNodeType, - }); - - // remove referencing schema from ancestors lineage - directAncestors.delete(referencingElement); + // detect direct or indirect reference + if (this.indirections.includes(referencedElement)) { + throw new Error('Recursive JSON Pointer detected'); + } - this.indirections.pop(); + // detect maximum depth of dereferencing + if (this.indirections.length > this.options.dereference.maxDepth) { + throw new MaximumDereferenceDepthError( + `Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"` + ); + } - if (isBooleanJsonSchemaElement(referencedElement)) { - // Boolean JSON Schema - const jsonSchemaBooleanElement = referencedElement.clone(); - // annotate referenced element with info about original referencing element - jsonSchemaBooleanElement.setMetaProperty('ref-fields', { - $ref: referencingElement.$ref?.toValue(), - }); - // annotate referenced element with info about origin - jsonSchemaBooleanElement.setMetaProperty('ref-origin', retrievalURI); + // append referencing schema to ancestors lineage + directAncestors.add(referencingElement); - return jsonSchemaBooleanElement; - } + // dive deep into the fragment + const mergeVisitor = OpenApi3_1SwaggerClientDereferenceVisitor({ + reference, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + useCircularStructures: this.useCircularStructures, + allowMetaPatches: this.allowMetaPatches, + ancestors: ancestorsLineage, + }); + referencedElement = await visitAsync(referencedElement, mergeVisitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); - // useCircularStructures option processing - if (!this.useCircularStructures) { - const hasCycles = ancestorsLineage.some((ancs) => ancs.has(referencedElement)); - if (hasCycles) { - if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { - // make the referencing URL or file system path absolute - const baseURI = url.resolve(retrievalURI, $refBaseURI); - const cycledSchemaElement = new SchemaElement( - { $ref: baseURI }, - referencingElement.meta.clone(), - referencingElement.attributes.clone() - ); - cycledSchemaElement.get('$ref').classes.push('cycle'); - return cycledSchemaElement; - } - // skip processing this schema but traverse all it's child schemas - return false; - } - } + // remove referencing schema from ancestors lineage + directAncestors.delete(referencingElement); - // Schema Object - merge keywords from referenced schema with referencing schema - const mergedSchemaElement = new SchemaElement( - [...referencedElement.content], - referencedElement.meta.clone(), - referencedElement.attributes.clone() - ); - // existing keywords from referencing schema overrides ones from referenced schema - referencingElement.forEach((memberValue, memberKey, member) => { - mergedSchemaElement.remove(memberKey.toValue()); - mergedSchemaElement.content.push(member); - }); - mergedSchemaElement.remove('$ref'); + this.indirections.pop(); + if (isBooleanJsonSchemaElement(referencedElement)) { + // Boolean JSON Schema + const jsonSchemaBooleanElement = referencedElement.clone(); // annotate referenced element with info about original referencing element - mergedSchemaElement.setMetaProperty('ref-fields', { + jsonSchemaBooleanElement.setMetaProperty('ref-fields', { $ref: referencingElement.$ref?.toValue(), }); - // annotate fragment with info about origin - mergedSchemaElement.setMetaProperty('ref-origin', retrievalURI); + // annotate referenced element with info about origin + jsonSchemaBooleanElement.setMetaProperty('ref-origin', retrievalURI); - // allowMetaPatches option processing - if (this.allowMetaPatches) { - // apply meta patch only when not already applied - if (typeof mergedSchemaElement.get('$$ref') === 'undefined') { + return jsonSchemaBooleanElement; + } + + // useCircularStructures option processing + if (!this.useCircularStructures) { + const hasCycles = ancestorsLineage.some((ancs) => ancs.has(referencedElement)); + if (hasCycles) { + if (url.isHttpUrl(retrievalURI) || url.isFileSystemPath(retrievalURI)) { + // make the referencing URL or file system path absolute const baseURI = url.resolve(retrievalURI, $refBaseURI); - mergedSchemaElement.set('$$ref', baseURI); + const cycledSchemaElement = new SchemaElement( + { $ref: baseURI }, + referencingElement.meta.clone(), + referencingElement.attributes.clone() + ); + cycledSchemaElement.get('$ref').classes.push('cycle'); + return cycledSchemaElement; } + // skip processing this schema but traverse all it's child schemas + return false; } + } + + // Schema Object - merge keywords from referenced schema with referencing schema + const mergedSchemaElement = new SchemaElement( + [...referencedElement.content], + referencedElement.meta.clone(), + referencedElement.attributes.clone() + ); + // existing keywords from referencing schema overrides ones from referenced schema + referencingElement.forEach((memberValue, memberKey, member) => { + mergedSchemaElement.remove(memberKey.toValue()); + mergedSchemaElement.content.push(member); + }); + mergedSchemaElement.remove('$ref'); - // transclude referencing element with merged referenced element - return mergedSchemaElement; - }, - leave: (schemaElement) => { - if (typeof this.modelPropertyMacro !== 'function') return; - if (typeof schemaElement.properties === 'undefined') return; - if (!isObjectElement(schemaElement.properties)) return; + // annotate referenced element with info about original referencing element + mergedSchemaElement.setMetaProperty('ref-fields', { + $ref: referencingElement.$ref?.toValue(), + }); + // annotate fragment with info about origin + mergedSchemaElement.setMetaProperty('ref-origin', retrievalURI); - schemaElement.properties.forEach((property) => { - if (!isObjectElement(property)) return; + // allowMetaPatches option processing + if (this.allowMetaPatches) { + // apply meta patch only when not already applied + if (typeof mergedSchemaElement.get('$$ref') === 'undefined') { + const baseURI = url.resolve(retrievalURI, $refBaseURI); + mergedSchemaElement.set('$$ref', baseURI); + } + } - property.set('default', this.modelPropertyMacro(toValue(property))); - }); - }, - }; - - this.OperationElement = { - enter(operationElement) { - parameterMacroOperation = operationElement; - }, - leave() { - parameterMacroOperation = null; - }, - }; - - this.ParameterElement = { - leave: (parameterElement) => { - if (typeof this.parameterMacro !== 'function') return; - - const pojoOperation = - parameterMacroOperation === null ? null : toValue(parameterMacroOperation); - const pojoParameter = toValue(parameterElement); - const defaultValue = this.parameterMacro(pojoOperation, pojoParameter); - - parameterElement.set('default', defaultValue); - }, - }; - } -); + // transclude referencing element with merged referenced element + return mergedSchemaElement; + }, + }, +}); export default OpenApi3_1SwaggerClientDereferenceVisitor; /* eslint-enable camelcase */ diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/parameters.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/parameters.js new file mode 100644 index 000000000..ac529a144 --- /dev/null +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/parameters.js @@ -0,0 +1,27 @@ +import { toValue } from '@swagger-api/apidom-core'; + +const ParameterMacroVisitor = ({ parameterMacro }) => { + let macroOperation = null; + + return { + OperationElement: { + enter(operationElement) { + macroOperation = operationElement; + }, + leave() { + macroOperation = null; + }, + }, + ParameterElement: { + leave: (parameterElement) => { + const pojoOperation = macroOperation === null ? null : toValue(macroOperation); + const pojoParameter = toValue(parameterElement); + const defaultValue = parameterMacro(pojoOperation, pojoParameter); + + parameterElement.set('default', defaultValue); + }, + }, + }; +}; + +export default ParameterMacroVisitor; diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/properties.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/properties.js new file mode 100644 index 000000000..f0f392ce7 --- /dev/null +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/properties.js @@ -0,0 +1,18 @@ +import { isObjectElement, toValue } from '@swagger-api/apidom-core'; + +const ModelPropertyMacroVisitor = ({ modelPropertyMacro }) => ({ + SchemaElement: { + leave: (schemaElement) => { + if (typeof schemaElement.properties === 'undefined') return; + if (!isObjectElement(schemaElement.properties)) return; + + schemaElement.properties.forEach((property) => { + if (!isObjectElement(property)) return; + + property.set('default', modelPropertyMacro(toValue(property))); + }); + }, + }, +}); + +export default ModelPropertyMacroVisitor;