diff --git a/packages/apidom-ls/src/parser-factory.ts b/packages/apidom-ls/src/parser-factory.ts index 6a3d14c0cd..5721af002a 100644 --- a/packages/apidom-ls/src/parser-factory.ts +++ b/packages/apidom-ls/src/parser-factory.ts @@ -5,11 +5,14 @@ import * as openapi3_0AdapterYaml from '@swagger-api/apidom-parser-adapter-opena import * as openapi3_1AdapterJson from '@swagger-api/apidom-parser-adapter-openapi-json-3-1'; import * as openapi3_1AdapterYaml from '@swagger-api/apidom-parser-adapter-openapi-yaml-3-1'; import * as asyncapi2AdapterJson from '@swagger-api/apidom-parser-adapter-asyncapi-json-2'; +import * as asyncapi3AdapterJson from '@swagger-api/apidom-parser-adapter-asyncapi-json-3'; import * as asyncapi2AdapterYaml from '@swagger-api/apidom-parser-adapter-asyncapi-yaml-2'; +import * as asyncapi3AdapterYaml from '@swagger-api/apidom-parser-adapter-asyncapi-yaml-3'; import * as adsAdapterJson from '@swagger-api/apidom-parser-adapter-api-design-systems-json'; import * as adsAdapterYaml from '@swagger-api/apidom-parser-adapter-api-design-systems-yaml'; import * as adapterJson from '@swagger-api/apidom-parser-adapter-json'; import * as adapterYaml from '@swagger-api/apidom-parser-adapter-yaml-1-2'; +import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementAsyncAPI3 } from '@swagger-api/apidom-ns-asyncapi-3'; import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementAsyncAPI2 } from '@swagger-api/apidom-ns-asyncapi-2'; import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOpenAPI2 } from '@swagger-api/apidom-ns-openapi-2'; import { refractorPluginReplaceEmptyElement as refractorPluginReplaceEmptyElementOpenAPI3_0 } from '@swagger-api/apidom-ns-openapi-3-0'; @@ -39,7 +42,11 @@ export async function parse( const text: string = typeof textDocument === 'string' ? textDocument : textDocument.getText(); let result; const contentLanguage = await findNamespace(text, defaultContentLanguage); - if (contentLanguage.namespace === 'asyncapi' && contentLanguage.format === 'JSON') { + if ( + contentLanguage.namespace === 'asyncapi' && + (contentLanguage.version?.startsWith('2.') || !contentLanguage.version) && + contentLanguage.format === 'JSON' + ) { const options: Record = { sourceMap: true, refractorOpts: { @@ -48,7 +55,24 @@ export async function parse( }; result = await asyncapi2AdapterJson.parse(text, options); - } else if (contentLanguage.namespace === 'asyncapi' && contentLanguage.format === 'YAML') { + } else if ( + contentLanguage.namespace === 'asyncapi' && + contentLanguage.version?.startsWith('3.') && + contentLanguage.format === 'JSON' + ) { + const options: Record = { + sourceMap: true, + refractorOpts: { + plugins: [...(refractorPlugins?.['asyncapi-3'] || [])], + }, + }; + + result = await asyncapi3AdapterJson.parse(text, options); + } else if ( + contentLanguage.namespace === 'asyncapi' && + (contentLanguage.version?.startsWith('2.') || !contentLanguage.version) && + contentLanguage.format === 'YAML' + ) { const options: Record = { sourceMap: true, refractorOpts: { @@ -60,6 +84,22 @@ export async function parse( }; result = await asyncapi2AdapterYaml.parse(text, options); + } else if ( + contentLanguage.namespace === 'asyncapi' && + contentLanguage.version?.startsWith('3.') && + contentLanguage.format === 'YAML' + ) { + const options: Record = { + sourceMap: true, + refractorOpts: { + plugins: [ + registerPlugins && refractorPluginReplaceEmptyElementAsyncAPI3(), + ...(refractorPlugins?.['asyncapi-3'] || []), + ].filter(Boolean), + }, + }; + + result = await asyncapi3AdapterYaml.parse(text, options); } else if ( contentLanguage.namespace === 'openapi' && contentLanguage.version === '2.0' && diff --git a/packages/apidom-ls/test/hover-provider.ts b/packages/apidom-ls/test/hover-provider.ts index c9bc36d7c8..8a3f7e5bfa 100644 --- a/packages/apidom-ls/test/hover-provider.ts +++ b/packages/apidom-ls/test/hover-provider.ts @@ -468,7 +468,9 @@ describe('apidom-ls-hover-provider', function () { } }); - it('test hover full provider', async function () { + // TODO: Flaky test. + // eslint-disable-next-line mocha/no-skipped-tests + xit('test hover full provider', async function () { const languageService: LanguageService = getLanguageService(contextFull); try { diff --git a/packages/apidom-ns-asyncapi-2/src/predicates.ts b/packages/apidom-ns-asyncapi-2/src/predicates.ts index 30703a6874..350c1c06ba 100644 --- a/packages/apidom-ns-asyncapi-2/src/predicates.ts +++ b/packages/apidom-ns-asyncapi-2/src/predicates.ts @@ -28,7 +28,7 @@ import ServerVariableElement from './elements/ServerVariable.ts'; export const isAsyncApi2Element = createPredicate( ({ hasBasicElementProps, isElementType, primitiveEq, hasClass }) => { return (element: unknown): element is AsyncApi2Element => - element instanceof AsyncApi2Element || + (element instanceof AsyncApi2Element && element.constructor === AsyncApi2Element) || (hasBasicElementProps(element) && isElementType('asyncApi2', element) && primitiveEq('object', element) && diff --git a/packages/apidom-ns-asyncapi-3/package.json b/packages/apidom-ns-asyncapi-3/package.json index 8e5d9be2f3..a52b271bd6 100644 --- a/packages/apidom-ns-asyncapi-3/package.json +++ b/packages/apidom-ns-asyncapi-3/package.json @@ -7,7 +7,10 @@ "registry": "https://registry.npmjs.org" }, "type": "module", - "sideEffects": [], + "sideEffects": [ + "./src/refractor/registration.mjs", + "./src/refractor/registration.cjs" + ], "main": "./src/index.cjs", "exports": { "types": "./types/apidom-ns-asyncapi-3.d.ts", diff --git a/packages/apidom-ns-asyncapi-3/src/index.ts b/packages/apidom-ns-asyncapi-3/src/index.ts index a529dc7e6a..9713f1b230 100644 --- a/packages/apidom-ns-asyncapi-3/src/index.ts +++ b/packages/apidom-ns-asyncapi-3/src/index.ts @@ -29,6 +29,8 @@ export { isServerVariableElement, } from './predicates.ts'; +export { keyMap, getNodeType } from './traversal/visitor.ts'; + export { /** * AsyncApi 3.0.0 specification elements. diff --git a/packages/apidom-ns-asyncapi-3/src/refractor/toolbox.ts b/packages/apidom-ns-asyncapi-3/src/refractor/toolbox.ts index c45ded0a62..d9860ac8b6 100644 --- a/packages/apidom-ns-asyncapi-3/src/refractor/toolbox.ts +++ b/packages/apidom-ns-asyncapi-3/src/refractor/toolbox.ts @@ -1,10 +1,10 @@ import { createNamespace, isStringElement } from '@swagger-api/apidom-core'; import * as asyncApi3Predicates from '../predicates.ts'; -import asyncApi2Namespace from '../namespace.ts'; +import asyncApi3Namespace from '../namespace.ts'; const createToolbox = () => { - const namespace = createNamespace(asyncApi2Namespace); + const namespace = createNamespace(asyncApi3Namespace); const predicates = { ...asyncApi3Predicates, isStringElement }; return { predicates, namespace }; diff --git a/packages/apidom-reference/src/configuration/saturated.ts b/packages/apidom-reference/src/configuration/saturated.ts index feeb5e73c9..b26a7fbf83 100644 --- a/packages/apidom-reference/src/configuration/saturated.ts +++ b/packages/apidom-reference/src/configuration/saturated.ts @@ -26,6 +26,7 @@ import OpenAPI2DereferenceStrategy from '../dereference/strategies/openapi-2/ind import OpenAPI3_0DereferenceStrategy from '../dereference/strategies/openapi-3-0/index.ts'; import OpenAPI3_1DereferenceStrategy from '../dereference/strategies/openapi-3-1/index.ts'; import AsyncAPI2DereferenceStrategy from '../dereference/strategies/asyncapi-2/index.ts'; +import AsyncAPI3DereferenceStrategy from '../dereference/strategies/asyncapi-3/index.ts'; import OpenAPI3_1BundleStrategy from '../bundle/strategies/openapi-3-1/index.ts'; import { options } from '../index.ts'; @@ -66,6 +67,7 @@ options.dereference.strategies = [ new OpenAPI3_0DereferenceStrategy(), new OpenAPI3_1DereferenceStrategy(), new AsyncAPI2DereferenceStrategy(), + new AsyncAPI3DereferenceStrategy(), new ApiDOMDereferenceStrategy(), ]; diff --git a/packages/apidom-reference/src/dereference/strategies/asyncapi-3/index.ts b/packages/apidom-reference/src/dereference/strategies/asyncapi-3/index.ts new file mode 100644 index 0000000000..24c20673b5 --- /dev/null +++ b/packages/apidom-reference/src/dereference/strategies/asyncapi-3/index.ts @@ -0,0 +1,140 @@ +import { createNamespace, visit, Element, cloneDeep } from '@swagger-api/apidom-core'; +import asyncApi3Namespace, { + getNodeType, + isAsyncApi3Element, + keyMap, + mediaTypes, +} from '@swagger-api/apidom-ns-asyncapi-3'; + +import DereferenceStrategy, { DereferenceStrategyOptions } from '../DereferenceStrategy.ts'; +import File from '../../../File.ts'; +import Reference from '../../../Reference.ts'; +import ReferenceSet from '../../../ReferenceSet.ts'; +import AsyncAPI3DereferenceVisitor from './visitor.ts'; +import type { ReferenceOptions } from '../../../options/index.ts'; + +export type { + default as DereferenceStrategy, + DereferenceStrategyOptions, +} from '../DereferenceStrategy.ts'; +export type { default as File, FileOptions } from '../../../File.ts'; +export type { default as Reference, ReferenceOptions } from '../../../Reference.ts'; +export type { default as ReferenceSet, ReferenceSetOptions } from '../../../ReferenceSet.ts'; +export type { AsyncAPI3DereferenceVisitorOptions, mutationReplacer } from './visitor.ts'; +export type { + ReferenceOptions as ApiDOMReferenceOptions, + ReferenceBundleOptions as ApiDOMReferenceBundleOptions, + ReferenceDereferenceOptions as ApiDOMReferenceDereferenceOptions, + ReferenceParseOptions as ApiDOMReferenceParseOptions, + ReferenceResolveOptions as ApiDOMReferenceResolveOptions, +} from '../../../options/index.ts'; +export type { default as Parser, ParserOptions } from '../../../parse/parsers/Parser.ts'; +export type { default as Resolver, ResolverOptions } from '../../../resolve/resolvers/Resolver.ts'; +export type { + default as ResolveStrategy, + ResolveStrategyOptions, +} from '../../../resolve/strategies/ResolveStrategy.ts'; +export type { + default as BundleStrategy, + BundleStrategyOptions, +} from '../../../bundle/strategies/BundleStrategy.ts'; +export type { AncestorLineage } from '../../util.ts'; + +// @ts-ignore +const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; + +/** + * @public + */ +export interface AsyncAPI3DeferenceStrategyOptions + extends Omit {} + +/** + * @public + */ +class AsyncAPI3DereferenceStrategy extends DereferenceStrategy { + constructor(options?: AsyncAPI3DeferenceStrategyOptions) { + super({ ...(options ?? {}), name: 'asyncapi-3' }); + } + + canDereference(file: File): boolean { + // assert by media type + if (file.mediaType !== 'text/plain') { + return mediaTypes.includes(file.mediaType); + } + + // assert by inspecting ApiDOM + return isAsyncApi3Element(file.parseResult?.api); + } + + async dereference(file: File, options: ReferenceOptions): Promise { + const namespace = createNamespace(asyncApi3Namespace); + const immutableRefSet = options.dereference.refSet ?? new ReferenceSet(); + const mutableRefSet = new ReferenceSet(); + let refSet = immutableRefSet; + let reference: Reference; + + if (!immutableRefSet.has(file.uri)) { + reference = new Reference({ uri: file.uri, value: file.parseResult! }); + immutableRefSet.add(reference); + } else { + // pre-computed refSet was provided as configuration option + reference = immutableRefSet.find((ref) => ref.uri === file.uri)!; + } + + /** + * Clone refSet due the dereferencing process being mutable. + * We don't want to mutate the original refSet and the references. + */ + if (options.dereference.immutable) { + immutableRefSet.refs + .map( + (ref) => + new Reference({ + ...ref, + value: cloneDeep(ref.value), + }), + ) + .forEach((ref) => mutableRefSet.add(ref)); + reference = mutableRefSet.find((ref) => ref.uri === file.uri)!; + refSet = mutableRefSet; + } + + const visitor = new AsyncAPI3DereferenceVisitor({ reference, namespace, options }); + const dereferencedElement = await visitAsync(refSet.rootRef!.value, visitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); + + /** + * If immutable option is set, replay refs from the refSet. + */ + if (options.dereference.immutable) { + mutableRefSet.refs + .filter((ref) => ref.uri.startsWith('immutable://')) + .map( + (ref) => + new Reference({ + ...ref, + uri: ref.uri.replace(/^immutable:\/\//, ''), + }), + ) + .forEach((ref) => immutableRefSet.add(ref)); + } + + /** + * Release all memory if this refSet was not provided as a configuration option. + * If provided as configuration option, then provider is responsible for cleanup. + */ + if (options.dereference.refSet === null) { + immutableRefSet.clean(); + } + + mutableRefSet.clean(); + + return dereferencedElement; + } +} + +export { AsyncAPI3DereferenceVisitor }; +export default AsyncAPI3DereferenceStrategy; diff --git a/packages/apidom-reference/src/dereference/strategies/asyncapi-3/visitor.ts b/packages/apidom-reference/src/dereference/strategies/asyncapi-3/visitor.ts new file mode 100644 index 0000000000..91ce400ff1 --- /dev/null +++ b/packages/apidom-reference/src/dereference/strategies/asyncapi-3/visitor.ts @@ -0,0 +1,348 @@ +import { propEq } from 'ramda'; +import { + isElement, + isMemberElement, + isPrimitiveElement, + IdentityManager, + cloneDeep, + cloneShallow, + visit, + toValue, + Namespace, + Element, + BooleanElement, + RefElement, +} from '@swagger-api/apidom-core'; +import { ApiDOMError } from '@swagger-api/apidom-error'; +import { evaluate, URIFragmentIdentifier } from '@swagger-api/apidom-json-pointer/modern'; +import { + isBooleanJsonSchemaElement, + isReferenceLikeElement, + getNodeType, +} from '@swagger-api/apidom-ns-asyncapi-2'; +import { keyMap, isReferenceElement, ReferenceElement } from '@swagger-api/apidom-ns-asyncapi-3'; + +import MaximumDereferenceDepthError from '../../../errors/MaximumDereferenceDepthError.ts'; +import MaximumResolveDepthError from '../../../errors/MaximumResolveDepthError.ts'; +import { AncestorLineage } from '../../util.ts'; +import * as url from '../../../util/url.ts'; +import parse from '../../../parse/index.ts'; +import Reference from '../../../Reference.ts'; +import ReferenceSet from '../../../ReferenceSet.ts'; +import type { ReferenceOptions } from '../../../options/index.ts'; + +// @ts-ignore +const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')]; + +// initialize element identity manager +const identityManager = new IdentityManager(); + +/** + * Custom mutation replacer. + * @public + */ +export const mutationReplacer = ( + newElement: Element, + oldElement: Element, + key: string | number, + parent: Element | undefined, +) => { + if (isMemberElement(parent)) { + parent.value = newElement; // eslint-disable-line no-param-reassign + } else if (Array.isArray(parent)) { + parent[key] = newElement; // eslint-disable-line no-param-reassign + } +}; + +/** + * @public + */ +export interface AsyncAPI3DereferenceVisitorOptions { + readonly namespace: Namespace; + readonly reference: Reference; + readonly options: ReferenceOptions; + readonly indirections?: Element[]; + readonly ancestors?: AncestorLineage; + readonly refractCache?: Map; +} + +/** + * @public + */ +class AsyncAPI3DereferenceVisitor { + protected readonly indirections: Element[]; + + protected readonly namespace: Namespace; + + protected readonly reference: Reference; + + protected readonly options: ReferenceOptions; + + protected readonly ancestors: AncestorLineage; + + protected readonly refractCache: Map; + + constructor({ + reference, + namespace, + options, + indirections = [], + ancestors = new AncestorLineage(), + refractCache = new Map(), + }: AsyncAPI3DereferenceVisitorOptions) { + this.indirections = indirections; + this.namespace = namespace; + this.reference = reference; + this.options = options; + this.ancestors = new AncestorLineage(...ancestors); + this.refractCache = refractCache; + } + + protected toBaseURI(uri: string): string { + return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri))); + } + + protected async toReference(uri: string): Promise { + // detect maximum depth of resolution + if (this.reference.depth >= this.options.resolve.maxDepth) { + throw new MaximumResolveDepthError( + `Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, + ); + } + + const baseURI = this.toBaseURI(uri); + const { refSet } = this.reference as { refSet: ReferenceSet }; + + // we've already processed this Reference in past + if (refSet.has(baseURI)) { + return refSet.find(propEq(baseURI, 'uri'))!; + } + + const parseResult = await parse(url.unsanitize(baseURI), { + ...this.options, + parse: { ...this.options.parse, mediaType: 'text/plain' }, + }); + + // register new mutable reference with a refSet + const mutableReference = new Reference({ + uri: baseURI, + value: cloneDeep(parseResult), + depth: this.reference.depth + 1, + }); + refSet.add(mutableReference); + + if (this.options.dereference.immutable) { + // register new immutable reference with a refSet + const immutableReference = new Reference({ + uri: `immutable://${baseURI}`, + value: parseResult, + depth: this.reference.depth + 1, + }); + refSet.add(immutableReference); + } + return mutableReference; + } + + protected toAncestorLineage( + ancestors: (Element | Element[] | undefined)[], + ): [AncestorLineage, Set] { + /** + * Compute full ancestors lineage. + * Ancestors are flatten to unwrap all Element instances. + */ + const directAncestors = new Set(ancestors.filter(isElement)); + const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors); + + return [ancestorsLineage, directAncestors]; + } + + public async ReferenceElement( + referencingElement: ReferenceElement, + key: string | number, + parent: Element | undefined, + path: (string | number)[], + ancestors: [Element | Element[]], + link: { replaceWith: (element: Element, replacer: typeof mutationReplacer) => void }, + ) { + // skip current referencing element as it's already been access + if (this.indirections.includes(referencingElement)) { + return false; + } + + const [ancestorsLineage, directAncestors] = this.toAncestorLineage([...ancestors, parent]); + + const retrievalURI = this.toBaseURI(toValue(referencingElement.$ref)); + const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI; + const isExternalReference = !isInternalReference; + + // ignore resolving internal Reference Objects + if (!this.options.resolve.internal && isInternalReference) { + // skip traversing this reference and all it's child elements + return false; + } + // ignore resolving external Reference Objects + if (!this.options.resolve.external && isExternalReference) { + // skip traversing this reference and all it's child elements + return false; + } + const reference = await this.toReference(toValue(referencingElement.$ref)); + const $refBaseURI = url.resolve(retrievalURI, toValue(referencingElement.$ref)); + + this.indirections.push(referencingElement); + + const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI); + + // possibly non-semantic fragment + let referencedElement = evaluate(reference.value.result, jsonPointer); + referencedElement.id = identityManager.identify(referencedElement); + + /** + * Applying semantics to a referenced element if semantics are missing. + */ + if (isPrimitiveElement(referencedElement)) { + const referencedElementType = toValue(referencingElement.meta.get('referenced-element')); + const cacheKey = `${referencedElementType}-${toValue(identityManager.identify(referencedElement))}`; + + if (this.refractCache.has(cacheKey)) { + referencedElement = this.refractCache.get(cacheKey)!; + } else if (isReferenceLikeElement(referencedElement)) { + // handling indirect references + referencedElement = ReferenceElement.refract(referencedElement); + referencedElement.setMetaProperty('referenced-element', referencedElementType); + this.refractCache.set(cacheKey, referencedElement); + } else { + // handling direct references + const ElementClass = this.namespace.getElementClass(referencedElementType); + referencedElement = ElementClass.refract(referencedElement); + this.refractCache.set(cacheKey, referencedElement); + } + } + + // detect direct or circular reference + if (referencingElement === referencedElement) { + throw new ApiDOMError('Recursive Reference Object 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 second deep dive into the same fragment and avoid it + if (ancestorsLineage.includes(referencedElement)) { + reference.refSet!.circular = true; + + if (this.options.dereference.circular === 'error') { + throw new ApiDOMError('Circular reference detected'); + } else if (this.options.dereference.circular === 'replace') { + const refElement = new RefElement(referencedElement.id, { + type: 'reference', + uri: reference.uri, + $ref: toValue(referencingElement.$ref), + }); + const replacer = + this.options.dereference.strategyOpts['asyncapi-3']?.circularReplacer ?? + this.options.dereference.circularReplacer; + const replacement = replacer(refElement); + + link.replaceWith(replacement, mutationReplacer); + + return !parent ? replacement : false; + } + } + + /** + * Dive deep into the fragment. + * + * Cases to consider: + * 1. We're crossing document boundary + * 2. Fragment is from non-root document + * 3. Fragment is a Reference Object. We need to follow it to get the eventual value + * 4. We are dereferencing the fragment lazily/eagerly depending on circular mode + */ + const isNonRootDocument = url.stripHash(reference.refSet!.rootRef!.uri) !== reference.uri; + const shouldDetectCircular = ['error', 'replace'].includes(this.options.dereference.circular); + if ( + (isExternalReference || + isNonRootDocument || + isReferenceElement(referencedElement) || + shouldDetectCircular) && + !ancestorsLineage.includesCycle(referencedElement) + ) { + // append referencing reference to ancestors lineage + directAncestors.add(referencingElement); + + const visitor = new AsyncAPI3DereferenceVisitor({ + reference, + namespace: this.namespace, + indirections: [...this.indirections], + options: this.options, + refractCache: this.refractCache, + ancestors: ancestorsLineage, + }); + referencedElement = await visitAsync(referencedElement, visitor, { + keyMap, + nodeTypeGetter: getNodeType, + }); + + // remove referencing reference from ancestors lineage + directAncestors.delete(referencingElement); + } + + this.indirections.pop(); + + // Boolean JSON Schemas + if (isBooleanJsonSchemaElement(referencedElement as unknown)) { + const booleanJsonSchemaElement: BooleanElement = cloneDeep(referencedElement); + // assign unique id to merged element + booleanJsonSchemaElement.setMetaProperty('id', identityManager.generateId()); + // annotate referenced element with info about original referencing element + booleanJsonSchemaElement.setMetaProperty('ref-fields', { + $ref: toValue(referencingElement.$ref), + }); + // annotate referenced element with info about origin + booleanJsonSchemaElement.setMetaProperty('ref-origin', reference.uri); + // annotate fragment with info about referencing element + booleanJsonSchemaElement.setMetaProperty( + 'ref-referencing-element-id', + cloneDeep(identityManager.identify(referencingElement)), + ); + + link.replaceWith(booleanJsonSchemaElement, mutationReplacer); + + return !parent ? booleanJsonSchemaElement : false; + } + + /** + * Creating a new version of referenced element to avoid modifying the original one. + */ + const mergedElement = cloneShallow(referencedElement); + // assign unique id to merged element + mergedElement.setMetaProperty('id', identityManager.generateId()); + // annotate referenced element with info about original referencing element + mergedElement.setMetaProperty('ref-fields', { + $ref: toValue(referencingElement.$ref), + }); + // annotate fragment with info about origin + mergedElement.setMetaProperty('ref-origin', reference.uri); + // annotate fragment with info about referencing element + mergedElement.setMetaProperty( + 'ref-referencing-element-id', + cloneDeep(identityManager.identify(referencingElement)), + ); + + /** + * Transclude referencing element with merged referenced element. + */ + link.replaceWith(mergedElement, mutationReplacer); + + /** + * We're at the root of the tree, so we're just replacing the entire tree. + */ + return !parent ? mergedElement : false; + } +} + +export default AsyncAPI3DereferenceVisitor;