Skip to content

Commit

Permalink
feat(reference): change dereference visitors from stamps to TypeScrip…
Browse files Browse the repository at this point in the history
…t classes (#4104)

Refs #3481

BREAKING CHANGE: all visitors from apidom-reference package became a class and requires to be instantiated with new operator.
  • Loading branch information
char0n committed May 14, 2024
1 parent 7e6dad4 commit a0657fb
Show file tree
Hide file tree
Showing 13 changed files with 2,511 additions and 2,444 deletions.
8 changes: 1 addition & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/apidom-reference/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,7 @@
"minimatch": "^7.4.3",
"process": "^0.11.10",
"ramda": "~0.30.0",
"ramda-adjunct": "^5.0.0",
"stampit": "^4.3.2"
"ramda-adjunct": "^5.0.0"
},
"optionalDependencies": {
"@swagger-api/apidom-error": "^0.99.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/apidom-reference/src/ReferenceSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ReferenceSet {

public readonly refs: Reference[];

public readonly circular: boolean;
public circular: boolean;

constructor({ refs = [], circular = false }: ReferenceSetOptions = {}) {
this.refs = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ApiDOMDereferenceStrategy extends DereferenceStrategy {
refSet = mutableRefSet;
}

const visitor = ApiDOMDereferenceVisitor({ reference, options });
const visitor = new ApiDOMDereferenceVisitor({ reference: reference!, options });
const dereferencedElement = await visitAsync(refSet.rootRef!.value, visitor);

/**
Expand All @@ -74,8 +74,6 @@ class ApiDOMDereferenceStrategy extends DereferenceStrategy {
}),
)
.forEach((ref) => immutableRefSet.add(ref));
reference = immutableRefSet.find((ref) => ref.uri === file.uri);
refSet = immutableRefSet;
}

/**
Expand All @@ -92,4 +90,5 @@ class ApiDOMDereferenceStrategy extends DereferenceStrategy {
}
}

export { ApiDOMDereferenceVisitor };
export default ApiDOMDereferenceStrategy;
295 changes: 150 additions & 145 deletions packages/apidom-reference/src/dereference/strategies/apidom/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import stampit from 'stampit';
import { propEq } from 'ramda';
import { ApiDOMError } from '@swagger-api/apidom-error';
import {
Expand All @@ -20,7 +19,9 @@ import MaximumResolveDepthError from '../../../errors/MaximumResolveDepthError';
import * as url from '../../../util/url';
import parse from '../../../parse';
import Reference from '../../../Reference';
import ReferenceSet from '../../../ReferenceSet';
import { evaluate } from './selectors/element-id';
import type { ReferenceOptions } from '../../../options';

// @ts-ignore
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];
Expand All @@ -38,159 +39,163 @@ const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];
* WARNING: this implementation only supports referencing elements in the local document. Points 2-4 are not supported.
*/

const ApiDOMDereferenceVisitor = stampit({
props: {
reference: null,
options: null,
},
init({ reference, options }) {
export interface ApiDOMDereferenceVisitorOptions {
readonly reference: Reference;
readonly options: ReferenceOptions;
}

class ApiDOMDereferenceVisitor {
protected readonly reference: Reference;

protected readonly options: ReferenceOptions;

constructor({ reference, options }: ApiDOMDereferenceVisitorOptions) {
this.reference = reference;
this.options = options;
},
methods: {
toBaseURI(uri: string): string {
return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
},

async toReference(uri: string): Promise<Reference> {
// 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;

// 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' },
});
}

protected toBaseURI(uri: string): string {
return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
}

// register new mutable reference with a refSet
const mutableReference = new Reference({
uri: baseURI,
value: cloneDeep(parseResult),
protected async toReference(uri: string): Promise<Reference> {
// 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(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;
},

async RefElement(
refElement: RefElement,
key: string | number,
parent: Element | undefined,
path: (string | number)[],
ancestors: [Element | Element[]],
refSet.add(immutableReference);
}

return mutableReference;
}

public async RefElement(
refElement: RefElement,
key: string | number,
parent: Element | undefined,
path: (string | number)[],
ancestors: [Element | Element[]],
) {
const refURI = toValue(refElement);
const refNormalizedURI = refURI.includes('#') ? refURI : `#${refURI}`;
const retrievalURI = this.toBaseURI(refNormalizedURI);
const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
const isExternalReference = !isInternalReference;

// ignore resolving internal RefElements
if (!this.options.resolve.internal && isInternalReference) {
// skip traversing this ref element
return false;
}
// ignore resolving external RefElements
if (!this.options.resolve.external && isExternalReference) {
// skip traversing this ref element
return false;
}

const reference = await this.toReference(refNormalizedURI);
const refBaseURI = url.resolve(retrievalURI, refNormalizedURI);
const elementID = uriToElementID(refBaseURI);
let referencedElement: unknown | Element | undefined = evaluate(
elementID,
reference.value.result as Element,
);

if (!isElement(referencedElement)) {
throw new ApiDOMError(`Referenced element with id="${elementID}" was not found`);
}

if (refElement === referencedElement) {
throw new ApiDOMError('RefElement cannot reference itself');
}

if (isRefElement(referencedElement)) {
throw new ApiDOMError('RefElement cannot reference another RefElement');
}

if (isExternalReference) {
// dive deep into the fragment
const visitor = new ApiDOMDereferenceVisitor({ reference, options: this.options });
referencedElement = await visitAsync(referencedElement, visitor);
}

/**
* When path is used, it references the given property of the referenced element
*/
const referencedElementPath: string = toValue(refElement.path);
if (referencedElementPath !== 'element' && isElement(referencedElement)) {
referencedElement = refract(referencedElement[referencedElementPath]);
}

/**
* Transclusion of a Ref Element SHALL be defined in the if/else block below.
*/
if (
isObjectElement(referencedElement) &&
isObjectElement(ancestors[ancestors.length - 1]) &&
Array.isArray(parent) &&
typeof key === 'number'
) {
const refURI = toValue(refElement);
const refNormalizedURI = refURI.includes('#') ? refURI : `#${refURI}`;
const retrievalURI = this.toBaseURI(refNormalizedURI);
const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
const isExternalReference = !isInternalReference;

// ignore resolving internal RefElements
if (!this.options.resolve.internal && isInternalReference) {
// skip traversing this ref element
return false;
}
// ignore resolving external RefElements
if (!this.options.resolve.external && isExternalReference) {
// skip traversing this ref element
return false;
}

const reference = await this.toReference(refNormalizedURI);
const refBaseURI = url.resolve(retrievalURI, refNormalizedURI);
const elementID = uriToElementID(refBaseURI);
let referencedElement: unknown | Element | undefined = evaluate(
elementID,
reference.value.result,
);

if (!isElement(referencedElement)) {
throw new ApiDOMError(`Referenced element with id="${elementID}" was not found`);
}

if (refElement === referencedElement) {
throw new ApiDOMError('RefElement cannot reference itself');
}

if (isRefElement(referencedElement)) {
throw new ApiDOMError('RefElement cannot reference another RefElement');
}

if (isExternalReference) {
// dive deep into the fragment
const visitor = ApiDOMDereferenceVisitor({ reference, options: this.options });
referencedElement = await visitAsync(referencedElement, visitor);
}

/**
* When path is used, it references the given property of the referenced element
* If the Ref Element is held by an Object Element and references an Object Element,
* its content entries SHALL be inserted in place of the Ref Element.
*/
const referencedElementPath: string = toValue(refElement.path);
if (referencedElementPath !== 'element' && isElement(referencedElement)) {
referencedElement = refract(referencedElement[referencedElementPath]);
}

parent.splice(key, 1, ...referencedElement.content);
} else if (
isArrayElement(referencedElement) &&
Array.isArray(parent) &&
typeof key === 'number'
) {
/**
* Transclusion of a Ref Element SHALL be defined in the if/else block below.
* If the Ref Element is held by an Array Element and references an Array Element,
* its content entries SHALL be inserted in place of the Ref Element.
*/
if (
isObjectElement(referencedElement) &&
isObjectElement(ancestors[ancestors.length - 1]) &&
Array.isArray(parent) &&
typeof key === 'number'
) {
/**
* If the Ref Element is held by an Object Element and references an Object Element,
* its content entries SHALL be inserted in place of the Ref Element.
*/
parent.splice(key, 1, ...referencedElement.content);
} else if (
isArrayElement(referencedElement) &&
Array.isArray(parent) &&
typeof key === 'number'
) {
/**
* If the Ref Element is held by an Array Element and references an Array Element,
* its content entries SHALL be inserted in place of the Ref Element.
*/
parent.splice(key, 1, ...referencedElement.content);
} else if (isMemberElement(parent)) {
/**
* The Ref Element is substituted by the Element it references.
*/
parent.value = referencedElement; // eslint-disable-line no-param-reassign
} else if (Array.isArray(parent)) {
/**
* The Ref Element is substituted by the Element it references.
*/
parent[key] = referencedElement; // eslint-disable-line no-param-reassign
}

return !parent ? referencedElement : false;
},
},
});
parent.splice(key, 1, ...referencedElement.content);
} else if (isMemberElement(parent)) {
/**
* The Ref Element is substituted by the Element it references.
*/
parent.value = referencedElement; // eslint-disable-line no-param-reassign
} else if (Array.isArray(parent)) {
/**
* The Ref Element is substituted by the Element it references.
*/
parent[key] = referencedElement; // eslint-disable-line no-param-reassign
}

return !parent ? referencedElement : false;
}
}

export default ApiDOMDereferenceVisitor;

0 comments on commit a0657fb

Please sign in to comment.