Skip to content

Commit

Permalink
fix(reference): fix handling cycles in all dereference strategies (#3361
Browse files Browse the repository at this point in the history
  • Loading branch information
char0n authored Nov 7, 2023
1 parent 65dcd0e commit d84397c
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import stampit from 'stampit';
import { propEq } from 'ramda';
import {
cloneDeep,
cloneShallow,
Element,
isElement,
isMemberElement,
isPrimitiveElement,
isStringElement,
IdentityManager,
cloneDeep,
cloneShallow,
visit,
toValue,
Element,
} from '@swagger-api/apidom-core';
import { ApiDOMError } from '@swagger-api/apidom-error';
import { evaluate, uriToPointer } from '@swagger-api/apidom-json-pointer';
Expand All @@ -19,6 +20,7 @@ import {
isChannelItemElementExternal,
isReferenceElementExternal,
isReferenceLikeElement,
isBooleanJsonSchemaElement,
keyMap,
ReferenceElement,
} from '@swagger-api/apidom-ns-asyncapi-2';
Expand All @@ -34,6 +36,21 @@ import Reference from '../../../Reference';
// @ts-ignore
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];

// initialize element identity manager
const identityManager = IdentityManager();

/**
* Predicate for detecting if element was created by merging referencing
* element with particular element identity with a referenced element.
*/
const wasReferencedBy =
<T extends Element, U extends Element>(referencingElement: T) =>
(element: U) =>
element.meta.hasKey('ref-referencing-element-id') &&
element.meta
.get('ref-referencing-element-id')
.equals(toValue(identityManager.identify(referencingElement)));

const AsyncApi2DereferenceVisitor = stampit({
props: {
indirections: [],
Expand All @@ -55,7 +72,7 @@ const AsyncApi2DereferenceVisitor = stampit({
* Compute full ancestors lineage.
* Ancestors are flatten to unwrap all Element instances.
*/
const directAncestors = new WeakSet(ancestors.filter(isElement));
const directAncestors = new Set<Element>(ancestors.filter(isElement));
const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);

return [ancestorsLineage, directAncestors];
Expand Down Expand Up @@ -174,6 +191,24 @@ const AsyncApi2DereferenceVisitor = stampit({

this.indirections.pop();

// Boolean JSON Schemas
if (isBooleanJsonSchemaElement(referencedElement)) {
const booleanJsonSchemaElement = cloneDeep(referencedElement);
// 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)),
);

return booleanJsonSchemaElement;
}

const mergeAndAnnotateReferencedElement = <T extends Element>(refedElement: T): T => {
const copy = cloneShallow(refedElement);

Expand All @@ -183,18 +218,28 @@ const AsyncApi2DereferenceVisitor = stampit({
});
// annotate fragment with info about origin
copy.setMetaProperty('ref-origin', reference.uri);
// annotate fragment with info about referencing element
copy.setMetaProperty(
'ref-referencing-element-id',
cloneDeep(identityManager.identify(referencingElement)),
);

return copy;
};

// attempting to create cycle
if (ancestorsLineage.includes(referencedElement)) {
if (
ancestorsLineage.includes(referencingElement) ||
ancestorsLineage.includes(referencedElement)
) {
const replaceWith =
ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ??
mergeAndAnnotateReferencedElement(referencedElement);
if (isMemberElement(parent)) {
parent.value = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent.value = replaceWith; // eslint-disable-line no-param-reassign
} else if (Array.isArray(parent)) {
parent[key] = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent[key] = replaceWith; // eslint-disable-line no-param-reassign
}

return false;
}

Expand Down Expand Up @@ -297,18 +342,28 @@ const AsyncApi2DereferenceVisitor = stampit({
});
// annotate referenced 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)),
);

return mergedElement;
};

// attempting to create cycle
if (ancestorsLineage.includes(referencedElement)) {
if (
ancestorsLineage.includes(referencingElement) ||
ancestorsLineage.includes(referencedElement)
) {
const replaceWith =
ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ??
mergeAndAnnotateReferencedElement(referencedElement);
if (isMemberElement(parent)) {
parent.value = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent.value = replaceWith; // eslint-disable-line no-param-reassign
} else if (Array.isArray(parent)) {
parent[key] = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent[key] = replaceWith; // eslint-disable-line no-param-reassign
}

return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import stampit from 'stampit';
import { propEq } from 'ramda';
import { isUndefined } from 'ramda-adjunct';
import {
Element,
isPrimitiveElement,
isStringElement,
isMemberElement,
isElement,
IdentityManager,
visit,
find,
isElement,
cloneShallow,
cloneDeep,
toValue,
isMemberElement,
Element,
} from '@swagger-api/apidom-core';
import { ApiDOMError } from '@swagger-api/apidom-error';
import { evaluate, uriToPointer } from '@swagger-api/apidom-json-pointer';
Expand Down Expand Up @@ -41,6 +42,21 @@ import Reference from '../../../Reference';
// @ts-ignore
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];

// initialize element identity manager
const identityManager = IdentityManager();

/**
* Predicate for detecting if element was created by merging referencing
* element with particular element identity with a referenced element.
*/
const wasReferencedBy =
<T extends Element, U extends Element>(referencingElement: T) =>
(element: U) =>
element.meta.hasKey('ref-referencing-element-id') &&
element.meta
.get('ref-referencing-element-id')
.equals(toValue(identityManager.identify(referencingElement)));

// eslint-disable-next-line @typescript-eslint/naming-convention
const OpenApi3_0DereferenceVisitor = stampit({
props: {
Expand Down Expand Up @@ -97,7 +113,7 @@ const OpenApi3_0DereferenceVisitor = stampit({
* Compute full ancestors lineage.
* Ancestors are flatten to unwrap all Element instances.
*/
const directAncestors = new WeakSet(ancestors.filter(isElement));
const directAncestors = new Set<Element>(ancestors.filter(isElement));
const ancestorsLineage = new AncestorLineage(...this.ancestors, directAncestors);

return [ancestorsLineage, directAncestors];
Expand Down Expand Up @@ -192,18 +208,28 @@ const OpenApi3_0DereferenceVisitor = stampit({
});
// annotate fragment with info about origin
copy.setMetaProperty('ref-origin', reference.uri);
// annotate fragment with info about referencing element
copy.setMetaProperty(
'ref-referencing-element-id',
cloneDeep(identityManager.identify(referencingElement)),
);

return copy;
};

// attempting to create cycle
if (ancestorsLineage.includes(referencedElement)) {
if (
ancestorsLineage.includes(referencingElement) ||
ancestorsLineage.includes(referencedElement)
) {
const replaceWith =
ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ??
mergeAndAnnotateReferencedElement(referencedElement);
if (isMemberElement(parent)) {
parent.value = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent.value = replaceWith; // eslint-disable-line no-param-reassign
} else if (Array.isArray(parent)) {
parent[key] = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent[key] = replaceWith; // eslint-disable-line no-param-reassign
}

return false;
}

Expand Down Expand Up @@ -306,18 +332,28 @@ const OpenApi3_0DereferenceVisitor = stampit({
});
// annotate referenced element 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)),
);

return mergedElement;
};

// attempting to create cycle
if (ancestorsLineage.includes(referencedElement)) {
if (
ancestorsLineage.includes(referencingElement) ||
ancestorsLineage.includes(referencedElement)
) {
const replaceWith =
ancestorsLineage.findItem(wasReferencedBy(referencingElement)) ??
mergeAndAnnotateReferencedElement(referencedElement);
if (isMemberElement(parent)) {
parent.value = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent.value = replaceWith; // eslint-disable-line no-param-reassign
} else if (Array.isArray(parent)) {
parent[key] = mergeAndAnnotateReferencedElement(referencedElement); // eslint-disable-line no-param-reassign
parent[key] = replaceWith; // eslint-disable-line no-param-reassign
}

return false;
}

Expand Down
Loading

0 comments on commit d84397c

Please sign in to comment.