Skip to content

Commit a0657fb

Browse files
committed
feat(reference): change dereference visitors from stamps to TypeScript classes (#4104)
Refs #3481 BREAKING CHANGE: all visitors from apidom-reference package became a class and requires to be instantiated with new operator.
1 parent 7e6dad4 commit a0657fb

File tree

13 files changed

+2511
-2444
lines changed

13 files changed

+2511
-2444
lines changed

package-lock.json

+1-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/apidom-reference/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,7 @@
264264
"minimatch": "^7.4.3",
265265
"process": "^0.11.10",
266266
"ramda": "~0.30.0",
267-
"ramda-adjunct": "^5.0.0",
268-
"stampit": "^4.3.2"
267+
"ramda-adjunct": "^5.0.0"
269268
},
270269
"optionalDependencies": {
271270
"@swagger-api/apidom-error": "^0.99.0",

packages/apidom-reference/src/ReferenceSet.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class ReferenceSet {
1212

1313
public readonly refs: Reference[];
1414

15-
public readonly circular: boolean;
15+
public circular: boolean;
1616

1717
constructor({ refs = [], circular = false }: ReferenceSetOptions = {}) {
1818
this.refs = [];

packages/apidom-reference/src/dereference/strategies/apidom/index.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class ApiDOMDereferenceStrategy extends DereferenceStrategy {
5757
refSet = mutableRefSet;
5858
}
5959

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

6363
/**
@@ -74,8 +74,6 @@ class ApiDOMDereferenceStrategy extends DereferenceStrategy {
7474
}),
7575
)
7676
.forEach((ref) => immutableRefSet.add(ref));
77-
reference = immutableRefSet.find((ref) => ref.uri === file.uri);
78-
refSet = immutableRefSet;
7977
}
8078

8179
/**
@@ -92,4 +90,5 @@ class ApiDOMDereferenceStrategy extends DereferenceStrategy {
9290
}
9391
}
9492

93+
export { ApiDOMDereferenceVisitor };
9594
export default ApiDOMDereferenceStrategy;
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import stampit from 'stampit';
21
import { propEq } from 'ramda';
32
import { ApiDOMError } from '@swagger-api/apidom-error';
43
import {
@@ -20,7 +19,9 @@ import MaximumResolveDepthError from '../../../errors/MaximumResolveDepthError';
2019
import * as url from '../../../util/url';
2120
import parse from '../../../parse';
2221
import Reference from '../../../Reference';
22+
import ReferenceSet from '../../../ReferenceSet';
2323
import { evaluate } from './selectors/element-id';
24+
import type { ReferenceOptions } from '../../../options';
2425

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

41-
const ApiDOMDereferenceVisitor = stampit({
42-
props: {
43-
reference: null,
44-
options: null,
45-
},
46-
init({ reference, options }) {
42+
export interface ApiDOMDereferenceVisitorOptions {
43+
readonly reference: Reference;
44+
readonly options: ReferenceOptions;
45+
}
46+
47+
class ApiDOMDereferenceVisitor {
48+
protected readonly reference: Reference;
49+
50+
protected readonly options: ReferenceOptions;
51+
52+
constructor({ reference, options }: ApiDOMDereferenceVisitorOptions) {
4753
this.reference = reference;
4854
this.options = options;
49-
},
50-
methods: {
51-
toBaseURI(uri: string): string {
52-
return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
53-
},
54-
55-
async toReference(uri: string): Promise<Reference> {
56-
// detect maximum depth of resolution
57-
if (this.reference.depth >= this.options.resolve.maxDepth) {
58-
throw new MaximumResolveDepthError(
59-
`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`,
60-
);
61-
}
62-
63-
const baseURI = this.toBaseURI(uri);
64-
const { refSet } = this.reference;
65-
66-
// we've already processed this Reference in past
67-
if (refSet.has(baseURI)) {
68-
return refSet.find(propEq(baseURI, 'uri'));
69-
}
70-
71-
const parseResult = await parse(url.unsanitize(baseURI), {
72-
...this.options,
73-
parse: { ...this.options.parse, mediaType: 'text/plain' },
74-
});
55+
}
56+
57+
protected toBaseURI(uri: string): string {
58+
return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
59+
}
7560

76-
// register new mutable reference with a refSet
77-
const mutableReference = new Reference({
78-
uri: baseURI,
79-
value: cloneDeep(parseResult),
61+
protected async toReference(uri: string): Promise<Reference> {
62+
// detect maximum depth of resolution
63+
if (this.reference.depth >= this.options.resolve.maxDepth) {
64+
throw new MaximumResolveDepthError(
65+
`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`,
66+
);
67+
}
68+
69+
const baseURI = this.toBaseURI(uri);
70+
const { refSet } = this.reference as { refSet: ReferenceSet };
71+
72+
// we've already processed this Reference in past
73+
if (refSet.has(baseURI)) {
74+
return refSet.find(propEq(baseURI, 'uri'))!;
75+
}
76+
77+
const parseResult = await parse(url.unsanitize(baseURI), {
78+
...this.options,
79+
parse: { ...this.options.parse, mediaType: 'text/plain' },
80+
});
81+
82+
// register new mutable reference with a refSet
83+
const mutableReference = new Reference({
84+
uri: baseURI,
85+
value: cloneDeep(parseResult),
86+
depth: this.reference.depth + 1,
87+
});
88+
refSet.add(mutableReference);
89+
90+
if (this.options.dereference.immutable) {
91+
// register new immutable reference with a refSet
92+
const immutableReference = new Reference({
93+
uri: `immutable://${baseURI}`,
94+
value: parseResult,
8095
depth: this.reference.depth + 1,
8196
});
82-
refSet.add(mutableReference);
83-
84-
if (this.options.dereference.immutable) {
85-
// register new immutable reference with a refSet
86-
const immutableReference = new Reference({
87-
uri: `immutable://${baseURI}`,
88-
value: parseResult,
89-
depth: this.reference.depth + 1,
90-
});
91-
refSet.add(immutableReference);
92-
}
93-
94-
return mutableReference;
95-
},
96-
97-
async RefElement(
98-
refElement: RefElement,
99-
key: string | number,
100-
parent: Element | undefined,
101-
path: (string | number)[],
102-
ancestors: [Element | Element[]],
97+
refSet.add(immutableReference);
98+
}
99+
100+
return mutableReference;
101+
}
102+
103+
public async RefElement(
104+
refElement: RefElement,
105+
key: string | number,
106+
parent: Element | undefined,
107+
path: (string | number)[],
108+
ancestors: [Element | Element[]],
109+
) {
110+
const refURI = toValue(refElement);
111+
const refNormalizedURI = refURI.includes('#') ? refURI : `#${refURI}`;
112+
const retrievalURI = this.toBaseURI(refNormalizedURI);
113+
const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
114+
const isExternalReference = !isInternalReference;
115+
116+
// ignore resolving internal RefElements
117+
if (!this.options.resolve.internal && isInternalReference) {
118+
// skip traversing this ref element
119+
return false;
120+
}
121+
// ignore resolving external RefElements
122+
if (!this.options.resolve.external && isExternalReference) {
123+
// skip traversing this ref element
124+
return false;
125+
}
126+
127+
const reference = await this.toReference(refNormalizedURI);
128+
const refBaseURI = url.resolve(retrievalURI, refNormalizedURI);
129+
const elementID = uriToElementID(refBaseURI);
130+
let referencedElement: unknown | Element | undefined = evaluate(
131+
elementID,
132+
reference.value.result as Element,
133+
);
134+
135+
if (!isElement(referencedElement)) {
136+
throw new ApiDOMError(`Referenced element with id="${elementID}" was not found`);
137+
}
138+
139+
if (refElement === referencedElement) {
140+
throw new ApiDOMError('RefElement cannot reference itself');
141+
}
142+
143+
if (isRefElement(referencedElement)) {
144+
throw new ApiDOMError('RefElement cannot reference another RefElement');
145+
}
146+
147+
if (isExternalReference) {
148+
// dive deep into the fragment
149+
const visitor = new ApiDOMDereferenceVisitor({ reference, options: this.options });
150+
referencedElement = await visitAsync(referencedElement, visitor);
151+
}
152+
153+
/**
154+
* When path is used, it references the given property of the referenced element
155+
*/
156+
const referencedElementPath: string = toValue(refElement.path);
157+
if (referencedElementPath !== 'element' && isElement(referencedElement)) {
158+
referencedElement = refract(referencedElement[referencedElementPath]);
159+
}
160+
161+
/**
162+
* Transclusion of a Ref Element SHALL be defined in the if/else block below.
163+
*/
164+
if (
165+
isObjectElement(referencedElement) &&
166+
isObjectElement(ancestors[ancestors.length - 1]) &&
167+
Array.isArray(parent) &&
168+
typeof key === 'number'
103169
) {
104-
const refURI = toValue(refElement);
105-
const refNormalizedURI = refURI.includes('#') ? refURI : `#${refURI}`;
106-
const retrievalURI = this.toBaseURI(refNormalizedURI);
107-
const isInternalReference = url.stripHash(this.reference.uri) === retrievalURI;
108-
const isExternalReference = !isInternalReference;
109-
110-
// ignore resolving internal RefElements
111-
if (!this.options.resolve.internal && isInternalReference) {
112-
// skip traversing this ref element
113-
return false;
114-
}
115-
// ignore resolving external RefElements
116-
if (!this.options.resolve.external && isExternalReference) {
117-
// skip traversing this ref element
118-
return false;
119-
}
120-
121-
const reference = await this.toReference(refNormalizedURI);
122-
const refBaseURI = url.resolve(retrievalURI, refNormalizedURI);
123-
const elementID = uriToElementID(refBaseURI);
124-
let referencedElement: unknown | Element | undefined = evaluate(
125-
elementID,
126-
reference.value.result,
127-
);
128-
129-
if (!isElement(referencedElement)) {
130-
throw new ApiDOMError(`Referenced element with id="${elementID}" was not found`);
131-
}
132-
133-
if (refElement === referencedElement) {
134-
throw new ApiDOMError('RefElement cannot reference itself');
135-
}
136-
137-
if (isRefElement(referencedElement)) {
138-
throw new ApiDOMError('RefElement cannot reference another RefElement');
139-
}
140-
141-
if (isExternalReference) {
142-
// dive deep into the fragment
143-
const visitor = ApiDOMDereferenceVisitor({ reference, options: this.options });
144-
referencedElement = await visitAsync(referencedElement, visitor);
145-
}
146-
147170
/**
148-
* When path is used, it references the given property of the referenced element
171+
* If the Ref Element is held by an Object Element and references an Object Element,
172+
* its content entries SHALL be inserted in place of the Ref Element.
149173
*/
150-
const referencedElementPath: string = toValue(refElement.path);
151-
if (referencedElementPath !== 'element' && isElement(referencedElement)) {
152-
referencedElement = refract(referencedElement[referencedElementPath]);
153-
}
154-
174+
parent.splice(key, 1, ...referencedElement.content);
175+
} else if (
176+
isArrayElement(referencedElement) &&
177+
Array.isArray(parent) &&
178+
typeof key === 'number'
179+
) {
155180
/**
156-
* Transclusion of a Ref Element SHALL be defined in the if/else block below.
181+
* If the Ref Element is held by an Array Element and references an Array Element,
182+
* its content entries SHALL be inserted in place of the Ref Element.
157183
*/
158-
if (
159-
isObjectElement(referencedElement) &&
160-
isObjectElement(ancestors[ancestors.length - 1]) &&
161-
Array.isArray(parent) &&
162-
typeof key === 'number'
163-
) {
164-
/**
165-
* If the Ref Element is held by an Object Element and references an Object Element,
166-
* its content entries SHALL be inserted in place of the Ref Element.
167-
*/
168-
parent.splice(key, 1, ...referencedElement.content);
169-
} else if (
170-
isArrayElement(referencedElement) &&
171-
Array.isArray(parent) &&
172-
typeof key === 'number'
173-
) {
174-
/**
175-
* If the Ref Element is held by an Array Element and references an Array Element,
176-
* its content entries SHALL be inserted in place of the Ref Element.
177-
*/
178-
parent.splice(key, 1, ...referencedElement.content);
179-
} else if (isMemberElement(parent)) {
180-
/**
181-
* The Ref Element is substituted by the Element it references.
182-
*/
183-
parent.value = referencedElement; // eslint-disable-line no-param-reassign
184-
} else if (Array.isArray(parent)) {
185-
/**
186-
* The Ref Element is substituted by the Element it references.
187-
*/
188-
parent[key] = referencedElement; // eslint-disable-line no-param-reassign
189-
}
190-
191-
return !parent ? referencedElement : false;
192-
},
193-
},
194-
});
184+
parent.splice(key, 1, ...referencedElement.content);
185+
} else if (isMemberElement(parent)) {
186+
/**
187+
* The Ref Element is substituted by the Element it references.
188+
*/
189+
parent.value = referencedElement; // eslint-disable-line no-param-reassign
190+
} else if (Array.isArray(parent)) {
191+
/**
192+
* The Ref Element is substituted by the Element it references.
193+
*/
194+
parent[key] = referencedElement; // eslint-disable-line no-param-reassign
195+
}
196+
197+
return !parent ? referencedElement : false;
198+
}
199+
}
195200

196201
export default ApiDOMDereferenceVisitor;

0 commit comments

Comments
 (0)