diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/errors/index.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/errors/index.js new file mode 100644 index 000000000..79bc6b94e --- /dev/null +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/errors/index.js @@ -0,0 +1,7 @@ +import createError from '../../../../../../../specmap/lib/create-error.js'; + +// eslint-disable-next-line import/prefer-default-export +export const SchemaRefError = createError('SchemaRefError', function cb(message, extra, oriError) { + this.originalError = oriError; + Object.assign(this, extra || {}); +}); diff --git a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js index df3d91e6f..63978e45a 100644 --- a/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js +++ b/src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/dereference.js @@ -41,6 +41,7 @@ import { import toPath from '../utils/to-path.js'; import getRootCause from '../utils/get-root-cause.js'; import specMapMod from '../../../../../../../specmap/lib/refs.js'; +import { SchemaRefError } from '../errors/index.js'; const { wrapError } = specMapMod; const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; @@ -344,75 +345,52 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c }, async SchemaElement(referencingElement, key, parent, path, ancestors) { - const [ancestorsLineage, directAncestors] = this.toAncestorLineage(ancestors); + try { + 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( - 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( + 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) ); @@ -425,111 +403,156 @@ const OpenApi3_1SwaggerClientDereferenceVisitor = OpenApi3_1DereferenceVisitor.c jsonPointerEvaluate(selector, reference.value.result) ); } - } else { - throw error; + } 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 direct or indirect reference + if (this.indirections.includes(referencedElement)) { + throw new Error('Recursive Schema Object reference 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}"` + // 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}"` + ); + } + + // 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, + ancestors: ancestorsLineage, + basePath: this.basePath ?? [ + ...toPath([...ancestors, parent, referencingElement]), + '$ref', + ], + }); + referencedElement = await visitAsync(referencedElement, mergeVisitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); + + // remove referencing schema from ancestors lineage + directAncestors.delete(referencingElement); + + this.indirections.pop(); + + 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); + + 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); + 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'); - // 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, - ancestors: ancestorsLineage, - basePath: this.basePath ?? toPath([...ancestors, parent, referencingElement]), - }); - referencedElement = await visitAsync(referencedElement, mergeVisitor, { - keyMap, - nodeTypeGetter: getNodeType, - }); - - // remove referencing schema from ancestors lineage - directAncestors.delete(referencingElement); - - this.indirections.pop(); - - if (isBooleanJsonSchemaElement(referencedElement)) { - // Boolean JSON Schema - const jsonSchemaBooleanElement = referencedElement.clone(); // annotate referenced element with info about original referencing element - jsonSchemaBooleanElement.setMetaProperty('ref-fields', { + mergedSchemaElement.setMetaProperty('ref-fields', { $ref: referencingElement.$ref?.toValue(), }); - // annotate referenced element with info about origin - jsonSchemaBooleanElement.setMetaProperty('ref-origin', retrievalURI); - - return jsonSchemaBooleanElement; - } + // annotate fragment with info about origin + mergedSchemaElement.setMetaProperty('ref-origin', retrievalURI); - // 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 + // 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); - const cycledSchemaElement = new SchemaElement( - { $ref: baseURI }, - referencingElement.meta.clone(), - referencingElement.attributes.clone() - ); - cycledSchemaElement.get('$ref').classes.push('cycle'); - return cycledSchemaElement; + mergedSchemaElement.set('$$ref', baseURI); } - // 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'); - - // 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); - - // 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); - } - } + // transclude referencing element with merged referenced element + return mergedSchemaElement; + } catch (error) { + const rootCause = getRootCause(error); + const wrappedError = new SchemaRefError( + `Could not resolve reference: ${rootCause.message}`, + { + baseDoc: this.reference.uri, + $ref: referencingElement.$ref.toValue(), + fullPath: this.basePath ?? [ + ...toPath([...ancestors, parent, referencingElement]), + '$ref', + ], + }, + rootCause + ); + this.options.dereference.dereferenceOpts?.errors?.push?.(wrappedError); - // transclude referencing element with merged referenced element - return mergedSchemaElement; + return undefined; + } }, async LinkElement() { diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$anchor-not-found/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$anchor-not-found/dereferenced.json new file mode 100644 index 000000000..4ea8a161f --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$anchor-not-found/dereferenced.json @@ -0,0 +1,23 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "password": { + "type": "string" + }, + "profile": { + "$ref": "#user-profile" + } + } + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$id-unresolvable/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$id-unresolvable/dereferenced.json new file mode 100644 index 000000000..c725959ee --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$id-unresolvable/dereferenced.json @@ -0,0 +1,25 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "$id": "./schemas/", + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "password": { + "type": "string" + }, + "profile": { + "$id": "./nested/", + "$ref": "./ex.json" + } + } + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-unresolvable/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-unresolvable/dereferenced.json new file mode 100644 index 000000000..f72f5cba5 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-url-unresolvable/dereferenced.json @@ -0,0 +1,23 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "password": { + "type": "string" + }, + "profile": { + "$ref": "./ex.json" + } + } + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-unresolvable/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-unresolvable/dereferenced.json new file mode 100644 index 000000000..e2aaddd2f --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/$ref-urn-unresolvable/dereferenced.json @@ -0,0 +1,23 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "login": { + "type": "string" + }, + "password": { + "type": "string" + }, + "profile": { + "$ref": "urn:uuid:3" + } + } + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/direct-external-circular/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/direct-external-circular/dereferenced.json new file mode 100644 index 000000000..f7435c858 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/direct-external-circular/dereferenced.json @@ -0,0 +1,10 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": {} + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/direct-internal-circular/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/direct-internal-circular/dereferenced.json new file mode 100644 index 000000000..5a6c516f9 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/direct-internal-circular/dereferenced.json @@ -0,0 +1,12 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "$ref": "#/components/schemas/User" + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-external-circular/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-external-circular/dereferenced.json new file mode 100644 index 000000000..f7435c858 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-external-circular/dereferenced.json @@ -0,0 +1,10 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": {} + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/dereferenced.json new file mode 100644 index 000000000..8ad97be91 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/dereferenced.json @@ -0,0 +1,13 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": {}, + "Indirection1": {}, + "Indirection2": {}, + "Indirection3": {} + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/root.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/root.json index 460612050..2b97cfa71 100644 --- a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/root.json +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/indirect-internal-circular/root.json @@ -8,10 +8,10 @@ "Indirection1": { "$ref": "#/components/schemas/Indirection2" }, - "Indirection3": { + "Indirection2": { "$ref": "#/components/schemas/Indirection3" }, - "Indirection4": { + "Indirection3": { "$ref": "#/components/schemas/User" } } diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/infinite-recursion/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/infinite-recursion/dereferenced.json new file mode 100644 index 000000000..1f279c9a8 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/infinite-recursion/dereferenced.json @@ -0,0 +1,15 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "type": "object" + }, + "UserProfile": { + "type": "object" + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/invalid-pointer/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/invalid-pointer/dereferenced.json new file mode 100644 index 000000000..f15ffdec3 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/invalid-pointer/dereferenced.json @@ -0,0 +1,13 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "type": "object", + "$ref": "#/components/schemas/invalid-pointer" + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/max-depth/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/max-depth/dereferenced.json new file mode 100644 index 000000000..b0bba7d62 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/max-depth/dereferenced.json @@ -0,0 +1,12 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "type": "object" + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/unresolvable-reference/dereferenced.json b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/unresolvable-reference/dereferenced.json new file mode 100644 index 000000000..109563210 --- /dev/null +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/__fixtures__/unresolvable-reference/dereferenced.json @@ -0,0 +1,16 @@ +[ + { + "openapi": "3.1.0", + "components": { + "schemas": { + "User": { + "properties": { + "profile": { + "$ref": "#/components/schemas/UserProfile" + } + } + } + } + } + } +] diff --git a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js index b51182bd1..c4e1820a2 100644 --- a/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js +++ b/test/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/index.js @@ -2,16 +2,7 @@ import path from 'node:path'; import { toValue } from '@swagger-api/apidom-core'; import { isSchemaElement, mediaTypes } from '@swagger-api/apidom-ns-openapi-3-1'; import { evaluate } from '@swagger-api/apidom-json-pointer'; -import { - DereferenceError, - MaximumDereferenceDepthError, - MaximumResolverDepthError, - ResolverError, - dereference, - resolve, -} from '@swagger-api/apidom-reference/configuration/empty'; -import { EvaluationJsonSchema$anchorError } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1/selectors/$anchor'; -import { EvaluationJsonSchemaUriError } from '@swagger-api/apidom-reference/dereference/strategies/openapi-3-1/selectors/uri'; +import { dereference, resolve } from '@swagger-api/apidom-reference/configuration/empty'; // eslint-disable-next-line camelcase import OpenApi3_1SwaggerClientDereferenceStrategy from '../../../../../../../../src/helpers/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/index.js'; @@ -690,24 +681,32 @@ describe('dereference', () => { describe('given Schema Objects with unresolvable $id values', () => { const fixturePath = path.join(rootFixturePath, '$id-unresolvable'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); - await expect(dereferenceThunk()).rejects.toMatchObject({ - cause: { - cause: expect.any(ResolverError), - }, + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching(/^Could not resolve reference: ENOENT/), + baseDoc: expect.stringMatching(/\$id-unresolvable\/root\.json$/), + $ref: './ex.json', + fullPath: ['components', 'schemas', 'User', 'properties', 'profile', '$ref'], }); - await expect(dereferenceThunk()).rejects.toHaveProperty( - 'cause.cause.message', - expect.stringMatching(/\/schemas\/nested\/ex\.json"$/) - ); }); }); @@ -997,19 +996,31 @@ describe('dereference', () => { describe('given Schema Objects with $ref keyword containing unresolvable URL', () => { const fixturePath = path.join(rootFixturePath, '$ref-url-unresolvable'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); - await expect(dereferenceThunk()).rejects.toMatchObject({ - cause: { - cause: expect.any(ResolverError), - }, + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching(/^Could not resolve reference: ENOENT/), + baseDoc: expect.stringMatching(/\$ref-url-unresolvable\/root\.json$/), + $ref: './ex.json', + fullPath: ['components', 'schemas', 'User', 'properties', 'profile', '$ref'], }); }); }); @@ -1124,19 +1135,34 @@ describe('dereference', () => { describe('given Schema Objects with $ref keyword containing unresolvable Uniform Resource Name', () => { const fixturePath = path.join(rootFixturePath, '$ref-urn-unresolvable'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + resolve: { maxDepth: 2 }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); - await expect(dereferenceThunk()).rejects.toMatchObject({ - cause: { - cause: expect.any(EvaluationJsonSchemaUriError), - }, + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Evaluation failed on URI:/ + ), + baseDoc: expect.stringMatching(/\$ref-urn-unresolvable\/root\.json$/), + $ref: 'urn:uuid:3', + fullPath: ['components', 'schemas', 'User', 'properties', 'profile', '$ref'], }); }); }); @@ -1285,19 +1311,33 @@ describe('dereference', () => { describe('given Schema Objects with not found $anchor', () => { const fixturePath = path.join(rootFixturePath, '$anchor-not-found'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); - await expect(dereferenceThunk()).rejects.toMatchObject({ - cause: { - cause: expect.any(EvaluationJsonSchema$anchorError), - }, + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Evaluation failed on token:/ + ), + baseDoc: expect.stringMatching(/\$anchor-not-found\/root\.json$/), + $ref: '#user-profile', + fullPath: ['components', 'schemas', 'User', 'properties', 'profile', '$ref'], }); }); }); @@ -1318,180 +1358,378 @@ describe('dereference', () => { describe('given Schema Objects and maxDepth of dereference', () => { const fixturePath = path.join(rootFixturePath, 'max-depth'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { maxDepth: 2 }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { maxDepth: 2 }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); - await expect(dereferenceThunk()).rejects.toMatchObject({ - cause: { - cause: expect.any(MaximumDereferenceDepthError), + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { + maxDepth: 2, + dereferenceOpts: { errors }, }, }); - await expect(dereferenceThunk()).rejects.toHaveProperty( - 'cause.cause.message', - expect.stringMatching(/__fixtures__\/max-depth\/ex2.json"$/) - ); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Maximum dereference depth/ + ), + baseDoc: expect.stringMatching(/max-depth\/ex2\.json$/), + $ref: './ex3.json', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); }); describe('given Schema Objects and maxDepth of resolution', () => { const fixturePath = path.join(rootFixturePath, 'max-depth'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - resolve: { maxDepth: 2 }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + resolve: { maxDepth: 2 }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); - await expect(dereferenceThunk()).rejects.toMatchObject({ - cause: { - cause: expect.any(MaximumResolverDepthError), - }, + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + resolve: { maxDepth: 2 }, + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Maximum resolution depth/ + ), + baseDoc: expect.stringMatching(/max-depth\/ex2\.json$/), + $ref: './ex3.json', + fullPath: ['components', 'schemas', 'User', '$ref'], }); - await expect(dereferenceThunk()).rejects.toHaveProperty( - 'cause.cause.message', - expect.stringMatching(/__fixtures__\/max-depth\/ex2.json"$/) - ); }); }); describe('given Schema Objects with unresolvable reference', () => { const fixturePath = path.join(rootFixturePath, 'unresolvable-reference'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Evaluation failed on token/ + ), + baseDoc: expect.stringMatching(/unresolvable-reference\/root\.json$/), + $ref: '#/components/schemas/UserProfile', + fullPath: ['components', 'schemas', 'User', 'properties', 'profile', '$ref'], + }); }); }); describe('given Schema Objects with invalid JSON Pointer', () => { const fixturePath = path.join(rootFixturePath, 'invalid-pointer'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Evaluation failed on token/ + ), + baseDoc: expect.stringMatching(/invalid-pointer\/root\.json$/), + $ref: '#/components/schemas/invalid-pointer', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); }); describe('given Schema Objects with infinite recursion', () => { const fixturePath = path.join(rootFixturePath, 'infinite-recursion'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/infinite-recursion\/root\.json$/), + $ref: '#/components/schemas/User', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); }); describe('given Schema Objects with direct circular external reference', () => { const fixturePath = path.join(rootFixturePath, 'direct-external-circular'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/direct-external-circular\/ex\.json$/), + $ref: './root.json#/components/schemas/User', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); }); describe('given Schema Objects with direct circular internal reference', () => { const fixturePath = path.join(rootFixturePath, 'direct-internal-circular'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/direct-internal-circular\/root\.json$/), + $ref: '#/components/schemas/User', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); }); describe('given Schema Objects with indirect circular external reference', () => { const fixturePath = path.join(rootFixturePath, 'indirect-external-circular'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/indirect-external-circular\/ex3\.json$/), + $ref: './root.json#/components/schemas/User', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); describe('and useCircularStructures=false', () => { - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], - }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { + strategies: [ + OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), + ], + }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { + dereferenceOpts: { errors }, + strategies: [ + OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), + ], + }, + }); - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/indirect-external-circular\/ex3\.json$/), + $ref: './root.json#/components/schemas/User', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); }); }); describe('given Schema Objects with indirect circular internal reference', () => { const fixturePath = path.join(rootFixturePath, 'indirect-internal-circular'); + const rootFilePath = path.join(fixturePath, 'root.json'); - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - }); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { dereferenceOpts: { errors } }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), + $ref: '#/components/schemas/User', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); describe('and useCircularStructures=false', () => { - test('should throw error', async () => { - const rootFilePath = path.join(fixturePath, 'root.json'); - const dereferenceThunk = () => - dereference(rootFilePath, { - parse: { mediaType: mediaTypes.latest('json') }, - dereference: { - strategies: [ - OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), - ], - }, - }); - - await expect(dereferenceThunk()).rejects.toThrow(DereferenceError); + test('should dereference', async () => { + const actual = await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + }); + const expected = globalThis.loadJsonFile(path.join(fixturePath, 'dereferenced.json')); + + expect(toValue(actual)).toEqual(expected); + }); + + test('should collect error', async () => { + const errors = []; + + await dereference(rootFilePath, { + parse: { mediaType: mediaTypes.latest('json') }, + dereference: { + dereferenceOpts: { errors }, + strategies: [ + OpenApi3_1SwaggerClientDereferenceStrategy({ useCircularStructures: false }), + ], + }, + }); + + expect(errors).toHaveLength(1); + expect(errors[0]).toMatchObject({ + message: expect.stringMatching( + /^Could not resolve reference: Recursive Schema Object reference detected/ + ), + baseDoc: expect.stringMatching(/indirect-internal-circular\/root\.json$/), + $ref: '#/components/schemas/User', + fullPath: ['components', 'schemas', 'User', '$ref'], + }); }); }); });