Skip to content

Commit a54461a

Browse files
authored
Do not create additional root for already rooted $refs (#7)
* Inline $refs placed under default root * Remove option we will not need
1 parent 3ee2ac5 commit a54461a

File tree

10 files changed

+98
-77
lines changed

10 files changed

+98
-77
lines changed

docs/options.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,5 @@ The `bundle` options control how JSON Schema $Ref Parser will bundle `$ref` poin
8585
|Option(s) |Type |Description
8686
|:---------------------|:-------------------|:------------
8787
|`generateKey`|`(value: any, file: string, hash: string or null) => string or null`|Used to generate $ref.
88-
|`shouldInline`|`(pathFromRoot: string) => boolean`|Determines whether a value of given reference should be inlined in the resulting output.
8988
|`defaultRoot`|`string`|The default root to optimize for.
9089

lib/bundle/defaults.js

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ function getGenericDefaults (generator) {
66
return {
77
defaultRoot: generator.root,
88

9-
generateKey (schema, file, hash) {
9+
generateKey (schema, file, hash, pathFromRoot) {
10+
if (generator.isUnderDirectRoot(pathFromRoot)) {
11+
return null;
12+
}
13+
1014
if (!url.isFileSystemPath(file) && !url.isHttp(file)) {
1115
return null;
1216
}
@@ -22,7 +26,7 @@ function getGenericDefaults (generator) {
2226
return null;
2327
}
2428

25-
return generator.generateKeyForPointer(schema, existingGeneratedKey + hash.slice(1));
29+
return generator.generateKeyForPointer(schema, existingGeneratedKey === undefined ? pathFromRoot : existingGeneratedKey + hash.slice(1));
2630
}
2731

2832
if (url.isHttp(file)) {
@@ -31,9 +35,6 @@ function getGenericDefaults (generator) {
3135

3236
return generator.generateKeyForFilepath(schema, file);
3337
},
34-
shouldInline () {
35-
return false;
36-
},
3738
};
3839
}
3940

@@ -50,39 +51,22 @@ module.exports.getDefaultsForNewJsonSchema = function (defaults = getGenericDefa
5051
module.exports.getDefaultsForOAS2 = function (defaults = getGenericDefaults(new KeyGenerator("#/definitions"))) {
5152
return {
5253
...defaults,
53-
generateKey (schema, file, hash) {
54+
generateKey (schema, file, hash, pathFromRoot) {
55+
pathFromRoot = normalizeOasSchemasHash(pathFromRoot, defaults.defaultRoot);
56+
5457
if (hash !== "#" && hash !== null) {
55-
return defaults.generateKey(schema, file, hash.replace(/\/components\/schemas\//g, "/definitions/"));
58+
return defaults.generateKey(schema, file, normalizeOasSchemasHash(hash, defaults.defaultRoot), pathFromRoot);
5659
}
5760

58-
return defaults.generateKey(schema, file, hash);
61+
return defaults.generateKey(schema, file, hash, pathFromRoot);
5962
},
60-
shouldInline (pathFromRoot) {
61-
const parsed = url.safePointerToPath(pathFromRoot);
62-
return parsed.length === 0 || (parsed[0] !== "definitions" && !parsed.includes("schema"));
63-
}
6463
};
6564
};
6665

6766
module.exports.getDefaultsForOAS3 = function (defaults = getGenericDefaults(new KeyGenerator("#/components/schemas"))) {
68-
return {
69-
...defaults,
70-
generateKey (schema, file, hash) {
71-
if (hash !== "#" && hash !== null) {
72-
return defaults.generateKey(schema, file, hash.replace(/\/definitions\//g, "/components/schemas/"));
73-
}
74-
75-
return defaults.generateKey(schema, file, hash);
76-
},
77-
shouldInline (pathFromRoot) {
78-
if (pathFromRoot.startsWith("#/components/schemas")) {
79-
return false;
80-
}
81-
82-
const parsed = url.safePointerToPath(pathFromRoot);
83-
84-
return parsed.length === 0 || !parsed.includes("schema");
85-
}
86-
};
67+
return module.exports.getDefaultsForOAS2(defaults);
8768
};
8869

70+
function normalizeOasSchemasHash (hash, root) {
71+
return hash.replace(/\/(?:components\/schemas|definitions)\//g, root.slice(1) + "/");
72+
}

lib/bundle/index.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,15 @@ function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, i
141141
}
142142
}
143143

144-
let inlineable = options.bundle.shouldInline(pathFromRoot);
145-
if (!inlineable && file !== $refs._root$Ref.path) {
144+
if (options.bundle.generateKey && file !== $refs._root$Ref.path) {
146145
if (!customRoots[file]) {
147146
customRoots[file] = {
148-
"#": options.bundle.generateKey($refs._root$Ref.value, file, null)
147+
"#": options.bundle.generateKey($refs._root$Ref.value, file, null, pathFromRoot)
149148
};
150149
}
151150

152151
if (!(hash in customRoots[file])) {
153-
customRoots[file][hash] = options.bundle.generateKey($refs._root$Ref.value, file, hash);
152+
customRoots[file][hash] = options.bundle.generateKey($refs._root$Ref.value, file, hash, pathFromRoot);
154153
}
155154

156155
pathFromRoot = findClosestRoot(customRoots[file], hash, pathFromRoot);
@@ -170,7 +169,6 @@ function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, i
170169
extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
171170
external, // Does this $ref pointer point to a file other than the main JSON Schema file?
172171
indirections, // The number of indirect references that were traversed to resolve the value
173-
inlineable,
174172
});
175173

176174
// Recursively crawl the resolved value
@@ -216,10 +214,6 @@ function remap (schema, inventory, options, customRoots) {
216214
// Group all the $refs that point to the same part of the file
217215
return a.hash < b.hash ? -1 : +1;
218216
}
219-
else if (a.inlineable !== b.inlineable) {
220-
// Group all the $refs that should be inlined. Inlined go last.
221-
return a.inlineable ? +1 : -1;
222-
}
223217
else if (a.circular !== b.circular) {
224218
// If the $ref points to itself, then sort it higher than other $refs that point to this $ref
225219
return a.circular ? -1 : +1;
@@ -258,7 +252,7 @@ function remap (schema, inventory, options, customRoots) {
258252
// console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
259253

260254
// if entry is not inlineable and has a custom root linked, we need to remap the properties of the object
261-
if (!entry.inlineable && customRoots[entry.file] && entry.hash in customRoots[entry.file] && customRoots[entry.file]["#"] !== null) {
255+
if (customRoots[entry.file] && entry.hash in customRoots[entry.file] && customRoots[entry.file]["#"] !== null) {
262256
if (entry.hash === "#") {
263257
// the whole file is referenced for the first time
264258
// we need to inject its entire content here

lib/bundle/stoplight/generator.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ const KeyGenerator = require("../util/key-generator");
55

66
function normalizeOpts (opts) {
77
if (typeof opts.cwd === "string") {
8-
opts.cwd = StoplightKeyGenerator.appendSlash(opts.cwd);
8+
opts.cwd = KeyGenerator.appendSlash(opts.cwd);
99
}
1010
else {
1111
opts.cwd = null;
1212
}
1313

1414
if (typeof opts.endpointUrl === "string") {
15-
opts.endpointUrl = StoplightKeyGenerator.appendSlash(opts.endpointUrl);
15+
opts.endpointUrl = KeyGenerator.appendSlash(opts.endpointUrl);
1616
}
1717
else {
1818
opts.endpointUrl = null;
@@ -26,10 +26,6 @@ function StoplightKeyGenerator (root, opts) {
2626
this._opts = normalizeOpts({ ...opts });
2727
}
2828

29-
StoplightKeyGenerator.appendSlash = function (str) {
30-
return str.replace(/([^/])\/?$/, "$1/");
31-
};
32-
3329
module.exports = StoplightKeyGenerator;
3430

3531
StoplightKeyGenerator.prototype = Object.create(KeyGenerator.prototype, {

lib/bundle/util/key-generator.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ function suggestKey (takenKeys, key) {
3232

3333
KeyGenerator.suggestKey = suggestKey;
3434

35+
KeyGenerator.appendSlash = function (str) {
36+
return str.replace(/([^/])\/?$/, "$1/");
37+
};
38+
3539
function KeyGenerator (root) {
3640
this.root = root;
3741
this._parsedRoot = safePointerToPath(root);
@@ -142,7 +146,8 @@ Object.assign(KeyGenerator.prototype, {
142146

143147
generateKeyForPointer (schema, pointer) {
144148
if (!this.hasExistingGeneratedKey(schema, pointer)) {
145-
let actualPath = pointer.split(this.root.slice(1)).slice(1);
149+
let fragment = KeyGenerator.appendSlash(this.root.slice(1));
150+
let actualPath = pointer.split(fragment).slice(1);
146151
let key = this.generateUniqueKey(schema, prettify(actualPath.join("/")));
147152

148153
this.registerNewGeneratedKey(schema, pointer, key);
@@ -165,5 +170,15 @@ Object.assign(KeyGenerator.prototype, {
165170
}
166171

167172
return true;
173+
},
174+
175+
isUnderDirectRoot (pointer) {
176+
let parsedPointer = safePointerToPath(pointer);
177+
178+
if (parsedPointer.length !== this._parsedRoot.length + 1) {
179+
return false;
180+
}
181+
182+
return this.isInRoot(pointer);
168183
}
169184
});

lib/index.d.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,6 @@ declare namespace $RefParser {
245245
*/
246246
generateKey?(value: unknown, file: string, hash: string | null): string | null;
247247

248-
/**
249-
* Determines whether a value of given reference should be inlined in the resulting output.
250-
*
251-
* @param {string} pathFromRoot
252-
* @return boolean
253-
*/
254-
shouldInline?(pathFromRoot: string): boolean;
255-
256248
/**
257249
* The default root to optimize for.
258250
*/

lib/options.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,10 @@ $RefParserOptions.defaults = {
8484
* @param {*} value
8585
* @param {string} file
8686
* @param {string|null} hash
87-
*/
88-
// eslint-disable-next-line no-unused-vars
89-
generateKey (value, file, hash) { return null; },
90-
91-
/**
92-
* Determines whether a value of given reference should be inlined in the resulting output
93-
*
9487
* @param {string} pathFromRoot
9588
*/
9689
// eslint-disable-next-line no-unused-vars
97-
shouldInline (pathFromRoot) { return false; },
90+
generateKey: null,
9891

9992
/**
10093
* The default root to optimize for.

test/specs/custom-bundling-roots-legacy-stoplight/reference/openapi-2-bundled.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ module.exports = {
1010
"/flight/{id}": {
1111
parameters: [
1212
{
13-
type: "number",
14-
name: "id",
15-
in: "path",
16-
required: true
13+
$ref: "#/definitions/Id"
1714
}
1815
],
1916
get: {
@@ -155,6 +152,12 @@ module.exports = {
155152
maxLength: 100,
156153
example: "747"
157154
},
155+
Id: {
156+
in: "path",
157+
name: "id",
158+
required: true,
159+
type: "number"
160+
},
158161
Manufacturer: {
159162
definitions: {},
160163
title: "Manufacturer",

test/specs/custom-bundling-roots-legacy-stoplight/reference/openapi-3.bundled.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ module.exports = {
1414
"/flight/{id}": {
1515
parameters: [
1616
{
17-
type: "number",
18-
name: "id",
19-
in: "path",
20-
required: true
17+
$ref: "#/components/schemas/Id"
2118
}
2219
],
2320
get: {
@@ -169,6 +166,12 @@ module.exports = {
169166
maxLength: 100,
170167
example: "747"
171168
},
169+
Id: {
170+
in: "path",
171+
name: "id",
172+
required: true,
173+
type: "number"
174+
},
172175
Manufacturer: {
173176
definitions: {},
174177
title: "Manufacturer",

test/specs/custom-bundling-roots/custom-bundling-roots.spec.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@ describe("Custom bundling roots", () => {
4646
},
4747
parameters: [
4848
{
49-
in: "path",
50-
name: "id",
51-
required: true,
52-
type: "number"
49+
$ref: "#/definitions/Id"
5350
}
5451
]
5552
}
@@ -224,4 +221,49 @@ describe("Custom bundling roots", () => {
224221
}
225222
});
226223
});
224+
225+
it("should not create redundant roots", async () => {
226+
let parser = new $RefParser();
227+
const model = {
228+
properties: {
229+
user: {
230+
properties: {
231+
id: {
232+
$ref: "#/definitions/Id"
233+
}
234+
},
235+
}
236+
},
237+
definitions: {
238+
Id: {
239+
$ref: path.rel("specs/custom-bundling-roots/id.json")
240+
}
241+
}
242+
};
243+
244+
const schema = await parser.bundle(model, {
245+
bundle: getDefaultsForOldJsonSchema(),
246+
});
247+
248+
expect(schema).to.equal(parser.schema);
249+
expect(schema).to.deep.equal({
250+
properties: {
251+
user: {
252+
properties: {
253+
id: {
254+
$ref: "#/definitions/Id"
255+
}
256+
},
257+
}
258+
},
259+
definitions: {
260+
Id: {
261+
type: "number",
262+
name: "id",
263+
in: "path",
264+
required: true
265+
}
266+
}
267+
});
268+
});
227269
});

0 commit comments

Comments
 (0)