Skip to content

Commit bbb9a25

Browse files
authored
feat(reference): apply dereferencing architecture 2.0 to ApiDOM (#3930)
1 parent 8078ea6 commit bbb9a25

File tree

8 files changed

+125
-87
lines changed

8 files changed

+125
-87
lines changed

packages/apidom-reference/src/ReferenceSet.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ const ReferenceSet: stampit.Stamp<IReferenceSet> = stampit({
5151

5252
clean() {
5353
this.refs.forEach((ref: IReference) => {
54-
// eslint-disable-next-line no-param-reassign
55-
ref.refSet = null;
54+
ref.refSet = null; // eslint-disable-line no-param-reassign
5655
});
56+
this.rootRef = null;
5757
this.refs = [];
5858
},
5959
},

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

+43-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import stampit from 'stampit';
2-
import { defaultTo, propEq } from 'ramda';
2+
import { propEq } from 'ramda';
33
import { Element, isElement, cloneDeep, visit } from '@swagger-api/apidom-core';
44

55
import DereferenceStrategy from '../DereferenceStrategy';
@@ -29,7 +29,7 @@ const ApiDOMDereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampit(
2929
},
3030

3131
async dereference(file: IFile, options: IReferenceOptions): Promise<Element> {
32-
let refSet = defaultTo(ReferenceSet(), options.dereference.refSet);
32+
const refSet = options.dereference.refSet ?? ReferenceSet();
3333
let reference;
3434

3535
// determine the initial reference
@@ -41,27 +41,54 @@ const ApiDOMDereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampit(
4141
reference = refSet.find(propEq(file.uri, 'uri'));
4242
}
4343

44-
// clone reference set due the dereferencing process being mutable
45-
if (
46-
typeof options.dereference.strategyOpts.apidom?.clone === 'undefined' ||
47-
options.dereference.strategyOpts.apidom?.clone
48-
) {
49-
const refsCopy = [...refSet.refs].map((ref) => {
50-
return Reference({ ...ref, value: cloneDeep(ref.value) });
51-
});
52-
refSet = ReferenceSet({ refs: refsCopy });
53-
reference = refSet.find(propEq(file.uri, 'uri'));
44+
/**
45+
* Clone refSet due the dereferencing process being mutable.
46+
* We don't want to mutate the original refSet and the references.
47+
*/
48+
if (options.dereference.immutable) {
49+
const immutableRefs = refSet.refs.map((ref) =>
50+
Reference({
51+
...ref,
52+
uri: `immutable://${ref.uri}`,
53+
}),
54+
);
55+
const mutableRefs = refSet.refs.map((ref) =>
56+
Reference({
57+
...ref,
58+
value: cloneDeep(ref.value),
59+
}),
60+
);
61+
62+
refSet.clean();
63+
mutableRefs.forEach((ref) => refSet.add(ref));
64+
immutableRefs.forEach((ref) => refSet.add(ref));
65+
reference = refSet.find((ref) => ref.uri === file.uri);
5466
}
5567

5668
const visitor = ApiDOMDereferenceVisitor({ reference, options });
5769
const dereferencedElement = await visitAsync(refSet.rootRef.value, visitor);
5870

59-
/**
60-
* Release all memory if this refSet was not provided as an configuration option.
61-
* If provided as configuration option, then provider is responsible for cleanup.
62-
*/
6371
if (options.dereference.refSet === null) {
72+
/**
73+
* Release all memory if this refSet was not provided as a configuration option.
74+
* If provided as configuration option, then provider is responsible for cleanup.
75+
*/
76+
refSet.clean();
77+
} else if (options.dereference.immutable) {
78+
/**
79+
* If immutable option is set, then we need to remove mutable refs from the refSet.
80+
*/
81+
const immutableRefs = refSet.refs
82+
.filter((ref) => ref.uri.startsWith('immutable://'))
83+
.map((ref) =>
84+
Reference({
85+
...ref,
86+
uri: ref.uri.replace(/^immutable:\/\//, ''),
87+
}),
88+
);
89+
6490
refSet.clean();
91+
immutableRefs.forEach((ref) => refSet.add(ref));
6592
}
6693

6794
return dereferencedElement;

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

+36-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import stampit from 'stampit';
22
import { propEq } from 'ramda';
33
import { ApiDOMError } from '@swagger-api/apidom-error';
44
import {
5+
Element,
56
RefElement,
67
isElement,
78
isMemberElement,
@@ -11,6 +12,7 @@ import {
1112
toValue,
1213
refract,
1314
visit,
15+
cloneDeep,
1416
} from '@swagger-api/apidom-core';
1517
import { uriToPointer as uriToElementID } from '@swagger-api/apidom-json-pointer';
1618

@@ -72,19 +74,34 @@ const ApiDOMDereferenceVisitor = stampit({
7274
parse: { ...this.options.parse, mediaType: 'text/plain' },
7375
});
7476

75-
// register new Reference with ReferenceSet
76-
const reference = Reference({
77+
// register new mutable reference with a refSet
78+
const mutableReference = Reference({
7779
uri: baseURI,
78-
value: parseResult,
80+
value: cloneDeep(parseResult),
7981
depth: this.reference.depth + 1,
8082
});
83+
refSet.add(mutableReference);
84+
85+
if (this.options.dereference.immutable) {
86+
// register new immutable reference with a refSet
87+
const immutableReference = Reference({
88+
uri: `immutable://${baseURI}`,
89+
value: parseResult,
90+
depth: this.reference.depth + 1,
91+
});
92+
refSet.add(immutableReference);
93+
}
8194

82-
refSet.add(reference);
83-
84-
return reference;
95+
return mutableReference;
8596
},
8697

87-
async RefElement(refElement: RefElement, key: any, parent: any, path: any, ancestors: any[]) {
98+
async RefElement(
99+
refElement: RefElement,
100+
key: string | number,
101+
parent: Element | undefined,
102+
path: (string | number)[],
103+
ancestors: [Element | Element[]],
104+
) {
88105
const refURI = toValue(refElement);
89106
const refNormalizedURI = refURI.includes('#') ? refURI : `#${refURI}`;
90107
const retrievalURI = this.toBaseURI(refNormalizedURI);
@@ -139,13 +156,22 @@ const ApiDOMDereferenceVisitor = stampit({
139156
/**
140157
* Transclusion of a Ref Element SHALL be defined in the if/else block below.
141158
*/
142-
if (isObjectElement(referencedElement) && isObjectElement(ancestors[ancestors.length - 1])) {
159+
if (
160+
isObjectElement(referencedElement) &&
161+
isObjectElement(ancestors[ancestors.length - 1]) &&
162+
Array.isArray(parent) &&
163+
typeof key === 'number'
164+
) {
143165
/**
144166
* If the Ref Element is held by an Object Element and references an Object Element,
145167
* its content entries SHALL be inserted in place of the Ref Element.
146168
*/
147169
parent.splice(key, 1, ...referencedElement.content);
148-
} else if (isArrayElement(referencedElement) && Array.isArray(parent)) {
170+
} else if (
171+
isArrayElement(referencedElement) &&
172+
Array.isArray(parent) &&
173+
typeof key === 'number'
174+
) {
149175
/**
150176
* If the Ref Element is held by an Array Element and references an Array Element,
151177
* its content entries SHALL be inserted in place of the Ref Element.
@@ -163,7 +189,7 @@ const ApiDOMDereferenceVisitor = stampit({
163189
parent[key] = referencedElement; // eslint-disable-line no-param-reassign
164190
}
165191

166-
return false;
192+
return !parent ? referencedElement : false;
167193
},
168194
},
169195
});

packages/apidom-reference/src/dereference/strategies/openapi-3-0/index.ts

+9-11
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const OpenApi3_0DereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stamp
4343
const refSet = options.dereference.refSet ?? ReferenceSet();
4444
let reference;
4545

46+
// determine the initial reference
4647
if (!refSet.has(file.uri)) {
4748
reference = Reference({ uri: file.uri, value: file.parseResult });
4849
refSet.add(reference);
@@ -72,7 +73,6 @@ const OpenApi3_0DereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stamp
7273
refSet.clean();
7374
mutableRefs.forEach((ref) => refSet.add(ref));
7475
immutableRefs.forEach((ref) => refSet.add(ref));
75-
7676
reference = refSet.find((ref) => ref.uri === file.uri);
7777
}
7878

@@ -82,18 +82,16 @@ const OpenApi3_0DereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stamp
8282
nodeTypeGetter: getNodeType,
8383
});
8484

85-
/**
86-
* Release all memory if this refSet was not provided as a configuration option.
87-
* If provided as configuration option, then provider is responsible for cleanup.
88-
*/
8985
if (options.dereference.refSet === null) {
86+
/**
87+
* Release all memory if this refSet was not provided as a configuration option.
88+
* If provided as configuration option, then provider is responsible for cleanup.
89+
*/
9090
refSet.clean();
91-
}
92-
93-
/**
94-
* If immutable option is set, then we need to remove mutable refs from the refSet.
95-
*/
96-
if (options.dereference.immutable) {
91+
} else if (options.dereference.immutable) {
92+
/**
93+
* If immutable option is set, then we need to remove mutable refs from the refSet.
94+
*/
9795
const immutableRefs = refSet.refs
9896
.filter((ref) => ref.uri.startsWith('immutable://'))
9997
.map((ref) =>

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

+25-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import stampit from 'stampit';
2-
import { isElement, visit, cloneDeep } from '@swagger-api/apidom-core';
32

43
import ResolveStrategy from '../ResolveStrategy';
54
import {
@@ -8,35 +7,44 @@ import {
87
ResolveStrategy as IResolveStrategy,
98
} from '../../../types';
109
import ReferenceSet from '../../../ReferenceSet';
11-
import Reference from '../../../Reference';
1210
import { merge as mergeOptions } from '../../../options/util';
13-
import ApiDOMResolveVisitor from './visitor';
14-
15-
// @ts-ignore
16-
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];
11+
import UnmatchedDereferenceStrategyError from '../../../errors/UnmatchedDereferenceStrategyError';
1712

1813
const ApiDOMResolveStrategy: stampit.Stamp<IResolveStrategy> = stampit(ResolveStrategy, {
1914
init() {
2015
this.name = 'apidom';
2116
},
2217
methods: {
23-
canResolve(file: IFile) {
24-
return (
25-
file.mediaType.startsWith('application/vnd.apidom') && isElement(file.parseResult?.result)
18+
canResolve(file: IFile, options: IReferenceOptions): boolean {
19+
const dereferenceStrategy = options.dereference.strategies.find(
20+
(strategy: any) => strategy.name === 'apidom',
2621
);
22+
23+
if (dereferenceStrategy === undefined) {
24+
return false;
25+
}
26+
27+
return dereferenceStrategy.canDereference(file, options);
2728
},
2829

2930
async resolve(file: IFile, options: IReferenceOptions) {
30-
const referenceValue = options.resolve.strategyOpts.apidom?.clone
31-
? cloneDeep(file.parseResult)
32-
: file.parseResult;
33-
const reference = Reference({ uri: file.uri, value: referenceValue });
34-
const mergedOptions = mergeOptions(options, { resolve: { internal: false } });
35-
const visitor = ApiDOMResolveVisitor({ reference, options: mergedOptions });
31+
const dereferenceStrategy = options.dereference.strategies.find(
32+
(strategy: any) => strategy.name === 'apidom',
33+
);
34+
35+
if (dereferenceStrategy === undefined) {
36+
throw new UnmatchedDereferenceStrategyError(
37+
'"apidom" dereference strategy is not available.',
38+
);
39+
}
40+
3641
const refSet = ReferenceSet();
37-
refSet.add(reference);
42+
const mergedOptions = mergeOptions(options, {
43+
resolve: { internal: false },
44+
dereference: { refSet },
45+
});
3846

39-
await visitAsync(refSet.rootRef.value, visitor);
47+
await dereferenceStrategy.dereference(file, mergedOptions);
4048

4149
return refSet;
4250
},

packages/apidom-reference/src/resolve/strategies/apidom/visitor.ts

-7
This file was deleted.

packages/apidom-reference/test/dereference/strategies/apidom/local/index.ts

+8-22
Original file line numberDiff line numberDiff line change
@@ -132,46 +132,32 @@ describe('dereference', function () {
132132
assert.strictEqual(expected.level1, expected.level1.level2a.level3.ref2);
133133
});
134134

135-
context('given clone option', function () {
136-
specify(
137-
'should not mutate the original element when clone options is not specified',
138-
async function () {
139-
const element = new ObjectElement({
140-
element: new StringElement('test', { id: 'unique-id' }),
141-
ref: new RefElement('unique-id'),
142-
});
143-
const actual = await dereferenceApiDOM(element, {
144-
parse: { mediaType: 'application/vnd.apidom' },
145-
dereference: { strategyOpts: { apidom: { clone: true } } },
146-
});
147-
148-
assert.isTrue(isRefElement(element.get('ref')));
149-
assert.isFalse(isRefElement(actual.get('ref')));
150-
},
151-
);
152-
153-
specify('should not mutate the original element when clone=true', async function () {
135+
context('given immutable=true', function () {
136+
specify('should not mutate original ApiDOM tree', async function () {
154137
const element = new ObjectElement({
155138
element: new StringElement('test', { id: 'unique-id' }),
156139
ref: new RefElement('unique-id'),
157140
});
141+
element.freeze();
158142
const actual = await dereferenceApiDOM(element, {
159143
parse: { mediaType: 'application/vnd.apidom' },
160-
dereference: { strategyOpts: { apidom: { clone: true } } },
144+
dereference: { immutable: true },
161145
});
162146

163147
assert.isTrue(isRefElement(element.get('ref')));
164148
assert.isFalse(isRefElement(actual.get('ref')));
165149
});
150+
});
166151

167-
specify('should mutate the original element on clone=false', async function () {
152+
context('given immutable=false', function () {
153+
specify('should mutate original ApiDOM tree', async function () {
168154
const element = new ObjectElement({
169155
element: new StringElement('test', { id: 'unique-id' }),
170156
ref: new RefElement('unique-id'),
171157
});
172158
const actual = await dereferenceApiDOM(element, {
173159
parse: { mediaType: 'application/vnd.apidom' },
174-
dereference: { strategyOpts: { apidom: { clone: false } } },
160+
dereference: { immutable: false },
175161
});
176162

177163
assert.isFalse(isRefElement(element.get('ref')));

packages/apidom-reference/test/dereference/strategies/openapi-3-0/reference-object/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ describe('dereference', function () {
222222
});
223223

224224
context('given immutable=true', function () {
225-
specify('should dereference frozen ApiDOM tree', async function () {
225+
specify('should not mutate original ApiDOM tree', async function () {
226226
const rootFilePath = path.join(fixturePath, 'root.json');
227227
const refSet = await resolve(rootFilePath, {
228228
parse: { mediaType: mediaTypes.latest('json') },
@@ -242,7 +242,7 @@ describe('dereference', function () {
242242
});
243243

244244
context('given immutable=false', function () {
245-
specify('should throw', async function () {
245+
specify('should mutate original ApiDOM tree', async function () {
246246
const rootFilePath = path.join(fixturePath, 'root.json');
247247
const refSet = await resolve(rootFilePath, {
248248
parse: { mediaType: mediaTypes.latest('json') },

0 commit comments

Comments
 (0)