Skip to content

Commit 32510a3

Browse files
Completely rewrote the bundling logic to fix APIDevTools/swagger-parser#16
1 parent 030f604 commit 32510a3

16 files changed

+1112
-701
lines changed

.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787

8888
"accessor-pairs": 2, // require corresponding getters for any setters
8989
"block-scoped-var": 2, // treat var statements as if they were block scoped (off by default)
90-
"complexity": [2, 8], // specify the maximum cyclomatic complexity allowed in a program (off by default)
90+
"complexity": [1, 8], // specify the maximum cyclomatic complexity allowed in a program (off by default)
9191
"consistent-return": 0, // require return statements to either always or never specify values
9292
"curly": 2, // specify curly brace conventions for all control statements
9393
"default-case": 0, // require default case in switch statements (off by default)

.jscsrc

-15
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,4 @@
7070
"disallowNewlineBeforeBlockStatements": true,
7171
"disallowSpaceBeforeComma": true,
7272
"disallowSpaceBeforeSemicolon": true,
73-
74-
"jsDoc": {
75-
"checkAnnotations": true,
76-
"checkParamNames": true,
77-
"requireParamTypes": true,
78-
"checkRedundantParams": true,
79-
"checkReturnTypes": true,
80-
"checkRedundantReturns": true,
81-
"requireReturnTypes": true,
82-
"checkTypes": true,
83-
"checkRedundantAccess": "enforceLeadingUnderscore",
84-
"leadingUnderscoreAccess": true,
85-
"requireHyphenBeforeDescription": true,
86-
"requireNewlineAfterDescription": true
87-
}
8873
}

dist/ref-parser.js

+545-427
Large diffs are not rendered by default.

dist/ref-parser.js.map

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

dist/ref-parser.min.js

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

dist/ref-parser.min.js.map

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

lib/bundle.js

+135-136
Original file line numberDiff line numberDiff line change
@@ -22,171 +22,170 @@ module.exports = bundle;
2222
* @param {$RefParserOptions} options
2323
*/
2424
function bundle(parser, options) {
25-
util.debug('Bundling $ref pointers in %s', parser._basePath);
25+
util.debug('Bundling $ref pointers in %s', parser.$refs._basePath);
2626

27-
optimize(parser.$refs);
28-
remap(parser.$refs, options);
29-
dereference(parser._basePath, parser.$refs, options);
30-
}
27+
// Build an inventory of all $ref pointers in the JSON Schema
28+
var inventory = [];
29+
crawl(parser.schema, parser.$refs._basePath + '#', '#', inventory, parser.$refs, options);
3130

32-
/**
33-
* Optimizes the {@link $Ref#referencedAt} list for each {@link $Ref} to contain as few entries
34-
* as possible (ideally, one).
35-
*
36-
* @example:
37-
* {
38-
* first: { $ref: somefile.json#/some/part },
39-
* second: { $ref: somefile.json#/another/part },
40-
* third: { $ref: somefile.json },
41-
* fourth: { $ref: somefile.json#/some/part/sub/part }
42-
* }
43-
*
44-
* In this example, there are four references to the same file, but since the third reference points
45-
* to the ENTIRE file, that's the only one we care about. The other three can just be remapped to point
46-
* inside the third one.
47-
*
48-
* On the other hand, if the third reference DIDN'T exist, then the first and second would both be
49-
* significant, since they point to different parts of the file. The fourth reference is not significant,
50-
* since it can still be remapped to point inside the first one.
51-
*
52-
* @param {$Refs} $refs
53-
*/
54-
function optimize($refs) {
55-
Object.keys($refs._$refs).forEach(function(key) {
56-
var $ref = $refs._$refs[key];
57-
58-
// Find the first reference to this $ref
59-
var first = $ref.referencedAt.filter(function(at) { return at.firstReference; })[0];
60-
61-
// Do any of the references point to the entire file?
62-
var entireFile = $ref.referencedAt.filter(function(at) { return at.hash === '#'; });
63-
if (entireFile.length === 1) {
64-
// We found a single reference to the entire file. Done!
65-
$ref.referencedAt = entireFile;
66-
}
67-
else if (entireFile.length > 1) {
68-
// We found more than one reference to the entire file. Pick the first one.
69-
if (entireFile.indexOf(first) >= 0) {
70-
$ref.referencedAt = [first];
71-
}
72-
else {
73-
$ref.referencedAt = entireFile.slice(0, 1);
74-
}
75-
}
76-
else {
77-
// There are noo references to the entire file, so optimize the list of reference points
78-
// by eliminating any duplicate/redundant ones (e.g. "fourth" in the example above)
79-
console.log('========================= %s BEFORE =======================', $ref.path, JSON.stringify($ref.referencedAt, null, 2));
80-
[first].concat($ref.referencedAt).forEach(function(at) {
81-
dedupe(at, $ref.referencedAt);
82-
});
83-
console.log('========================= %s AFTER =======================', $ref.path, JSON.stringify($ref.referencedAt, null, 2));
84-
}
85-
});
86-
}
87-
88-
/**
89-
* Removes redundant entries from the {@link $Ref#referencedAt} list.
90-
*
91-
* @param {object} original - The {@link $Ref#referencedAt} entry to keep
92-
* @param {object[]} dupes - The {@link $Ref#referencedAt} list to dedupe
93-
*/
94-
function dedupe(original, dupes) {
95-
for (var i = dupes.length - 1; i >= 0; i--) {
96-
var dupe = dupes[i];
97-
if (dupe !== original && dupe.hash.indexOf(original.hash) === 0) {
98-
dupes.splice(i, 1);
99-
}
100-
}
31+
// Remap all $ref pointers
32+
remap(inventory);
10133
}
10234

10335
/**
104-
* Re-maps all $ref pointers in the schema, so that they are relative to the root of the schema.
36+
* Recursively crawls the given value, and inventories all JSON references.
10537
*
38+
* @param {*} obj - The value to crawl. If it's not an object or array, it will be ignored.
39+
* @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash
40+
* @param {string} pathFromRoot - The path of `obj` from the schema root
41+
* @param {object[]} inventory - An array of already-inventoried $ref pointers
10642
* @param {$Refs} $refs
10743
* @param {$RefParserOptions} options
10844
*/
109-
function remap($refs, options) {
110-
var remapped = [];
111-
112-
// Crawl the schema and determine the re-mapped values for all $ref pointers.
113-
// NOTE: We don't actually APPLY the re-mappings yet, since that can affect other re-mappings
114-
Object.keys($refs._$refs).forEach(function(key) {
115-
var $ref = $refs._$refs[key];
116-
crawl($ref.value, $ref.path + '#', $refs, remapped, options);
117-
});
45+
function crawl(obj, path, pathFromRoot, inventory, $refs, options) {
46+
if (obj && typeof obj === 'object') {
47+
var keys = Object.keys(obj);
11848

119-
// Now APPLY all of the re-mappings
120-
for (var i = 0; i < remapped.length; i++) {
121-
var mapping = remapped[i];
122-
mapping.old$Ref.$ref = mapping.new$Ref.$ref;
123-
}
124-
}
49+
// Most people will expect references to be bundled into the the "definitions" property,
50+
// so we always crawl that property first, if it exists.
51+
var defs = keys.indexOf('definitions');
52+
if (defs > 0) {
53+
keys.splice(0, 0, keys.splice(defs, 1)[0]);
54+
}
12555

126-
/**
127-
* Recursively crawls the given value, and re-maps any JSON references.
128-
*
129-
* @param {*} obj - The value to crawl. If it's not an object or array, it will be ignored.
130-
* @param {string} path - The path to use for resolving relative JSON references
131-
* @param {$Refs} $refs - The resolved JSON references
132-
* @param {object[]} remapped - An array of the re-mapped JSON references
133-
* @param {$RefParserOptions} options
134-
*/
135-
function crawl(obj, path, $refs, remapped, options) {
136-
if (obj && typeof obj === 'object') {
137-
Object.keys(obj).forEach(function(key) {
56+
keys.forEach(function(key) {
13857
var keyPath = Pointer.join(path, key);
58+
var keyPathFromRoot = Pointer.join(pathFromRoot, key);
13959
var value = obj[key];
14060

14161
if ($Ref.is$Ref(value)) {
142-
// We found a $ref, so resolve it
143-
util.debug('Re-mapping $ref pointer "%s" at %s', value.$ref, keyPath);
144-
var $refPath = url.resolve(path, value.$ref);
145-
var pointer = $refs._resolve($refPath, options);
146-
147-
// Find the path from the root of the JSON schema
148-
var hash = util.path.getHash(value.$ref);
149-
var referencedAt = pointer.$ref.referencedAt.filter(function(at) {
150-
return hash.indexOf(at.hash) === 0;
151-
})[0];
152-
153-
console.log(
154-
'referencedAt.pathFromRoot =', referencedAt.pathFromRoot,
155-
'\nreferencedAt.hash =', referencedAt.hash,
156-
'\nhash =', hash,
157-
'\npointer.path.hash =', util.path.getHash(pointer.path)
158-
);
159-
160-
// Re-map the value
161-
var new$RefPath = referencedAt.pathFromRoot + util.path.getHash(pointer.path).substr(1);
162-
util.debug(' new value: %s', new$RefPath);
163-
remapped.push({
164-
old$Ref: value,
165-
new$Ref: {$ref: new$RefPath} // Note: DON'T name this property `new` (https://github.com/BigstickCarpet/json-schema-ref-parser/issues/3)
166-
});
62+
// Skip this $ref if we've already inventoried it
63+
if (!inventory.some(function(i) { return i.parent === obj && i.key === key; })) {
64+
inventory$Ref(obj, key, path, keyPathFromRoot, inventory, $refs, options);
65+
}
16766
}
16867
else {
169-
crawl(value, keyPath, $refs, remapped, options);
68+
crawl(value, keyPath, keyPathFromRoot, inventory, $refs, options);
17069
}
17170
});
17271
}
17372
}
17473

17574
/**
176-
* Dereferences each external $ref pointer exactly ONCE.
75+
* Inventories the given JSON Reference (i.e. records detailed information about it so we can
76+
* optimize all $refs in the schema), and then crawls the resolved value.
17777
*
178-
* @param {string} basePath
78+
* @param {object} $refParent - The object that contains a JSON Reference as one of its keys
79+
* @param {string} $refKey - The key in `$refParent` that is a JSON Reference
80+
* @param {string} path - The full path of the JSON Reference at `$refKey`, possibly with a JSON Pointer in the hash
81+
* @param {string} pathFromRoot - The path of the JSON Reference at `$refKey`, from the schema root
82+
* @param {object[]} inventory - An array of already-inventoried $ref pointers
17983
* @param {$Refs} $refs
18084
* @param {$RefParserOptions} options
18185
*/
182-
function dereference(basePath, $refs, options) {
183-
basePath = util.path.stripHash(basePath);
86+
function inventory$Ref($refParent, $refKey, path, pathFromRoot, inventory, $refs, options) {
87+
var $ref = $refParent[$refKey];
88+
var $refPath = url.resolve(path, $ref.$ref);
89+
var pointer = $refs._resolve($refPath, options);
90+
var depth = Pointer.parse(pathFromRoot).length;
91+
var file = util.path.stripHash(pointer.path);
92+
var hash = util.path.getHash(pointer.path);
93+
var external = file !== $refs._basePath;
94+
var extended = Object.keys($ref).length > 1;
95+
96+
inventory.push({
97+
$ref: $ref, // The JSON Reference (e.g. {$ref: string})
98+
parent: $refParent, // The object that contains this $ref pointer
99+
key: $refKey, // The key in `parent` that is the $ref pointer
100+
pathFromRoot: pathFromRoot, // The path to the $ref pointer, from the JSON Schema root
101+
depth: depth, // How far from the JSON Schema root is this $ref pointer?
102+
file: file, // The file that the $ref pointer resolves to
103+
hash: hash, // The hash within `file` that the $ref pointer resolves to
104+
value: pointer.value, // The resolved value of the $ref pointer
105+
circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
106+
extended: extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
107+
external: external // Does this $ref pointer point to a file other than the main JSON Schema file?
108+
});
184109

185-
Object.keys($refs._$refs).forEach(function(key) {
186-
var $ref = $refs._$refs[key];
110+
// Recursively crawl the resolved value
111+
crawl(pointer.value, pointer.path, pathFromRoot, inventory, $refs, options);
112+
}
187113

188-
if ($ref.referencedAt.length > 0) {
189-
$refs.set(basePath + $ref.referencedAt[0].pathFromRoot, $ref.value, options);
114+
/**
115+
* Re-maps every $ref pointer, so that they're all relative to the root of the JSON Schema.
116+
* Each referenced value is dereferenced EXACTLY ONCE. All subsequent references to the same
117+
* value are re-mapped to point to the first reference.
118+
*
119+
* @example:
120+
* {
121+
* first: { $ref: somefile.json#/some/part },
122+
* second: { $ref: somefile.json#/another/part },
123+
* third: { $ref: somefile.json },
124+
* fourth: { $ref: somefile.json#/some/part/sub/part }
125+
* }
126+
*
127+
* In this example, there are four references to the same file, but since the third reference points
128+
* to the ENTIRE file, that's the only one we need to dereference. The other three can just be
129+
* remapped to point inside the third one.
130+
*
131+
* On the other hand, if the third reference DIDN'T exist, then the first and second would both need
132+
* to be dereferenced, since they point to different parts of the file. The fourth reference does NOT
133+
* need to be dereferenced, because it can be remapped to point inside the first one.
134+
*
135+
* @param {object[]} inventory
136+
*/
137+
function remap(inventory) {
138+
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
139+
inventory.sort(function(a, b) {
140+
if (a.file !== b.file) {
141+
return a.file < b.file ? -1 : +1; // Group all the $refs that point to the same file
142+
}
143+
else if (a.hash !== b.hash) {
144+
return a.hash < b.hash ? -1 : +1; // Group all the $refs that point to the same part of the file
145+
}
146+
else if (a.circular !== b.circular) {
147+
return a.circular ? -1 : +1; // If the $ref points to itself, then sort it higher than other $refs that point to this $ref
148+
}
149+
else if (a.extended !== b.extended) {
150+
return a.extended ? +1 : -1; // If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value
151+
}
152+
else if (a.depth !== b.depth) {
153+
return a.depth - b.depth; // Sort $refs by how close they are to the JSON Schema root
154+
}
155+
else {
156+
// If all else is equal, use the $ref that's in the "definitions" property
157+
return b.pathFromRoot.lastIndexOf('/definitions') - a.pathFromRoot.lastIndexOf('/definitions');
190158
}
191159
});
160+
161+
var file, hash, pathFromRoot;
162+
inventory.forEach(function(i) {
163+
util.debug('Re-mapping $ref pointer "%s" at %s', i.$ref.$ref, i.pathFromRoot);
164+
165+
if (!i.external) {
166+
// This $ref already resolves to the main JSON Schema file
167+
i.$ref.$ref = i.hash;
168+
}
169+
else if (i.file !== file || i.hash.indexOf(hash) !== 0) {
170+
// We've moved to a new file or new hash
171+
file = i.file;
172+
hash = i.hash;
173+
pathFromRoot = i.pathFromRoot;
174+
175+
// This is the first $ref to point to this value, so dereference the value.
176+
// Any other $refs that point to the same value will point to this $ref instead
177+
i.$ref = i.parent[i.key] = util.dereference(i.$ref, i.value);
178+
179+
if (i.circular) {
180+
// This $ref points to itself
181+
i.$ref.$ref = i.pathFromRoot;
182+
}
183+
}
184+
else {
185+
// This $ref points to the same value as the prevous $ref
186+
i.$ref.$ref = Pointer.join(pathFromRoot, Pointer.parse(i.hash));
187+
}
188+
189+
util.debug(' new value: %s', (i.$ref && i.$ref.$ref) ? i.$ref.$ref : '[object Object]');
190+
});
192191
}

tests/index.html

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
<script src="specs/external/external.bundled.js"></script>
4141
<script src="specs/external/external.spec.js"></script>
4242

43+
<script src="specs/external-partial/external-partial.parsed.js"></script>
44+
<script src="specs/external-partial/external-partial.dereferenced.js"></script>
45+
<script src="specs/external-partial/external-partial.bundled.js"></script>
46+
<script src="specs/external-partial/external-partial.spec.js"></script>
47+
4348
<script src="specs/circular/circular.parsed.js"></script>
4449
<script src="specs/circular/circular.dereferenced.js"></script>
4550
<script src="specs/circular/circular.spec.js"></script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"required string": {
3+
"$ref": "required-string.yaml"
4+
},
5+
"string": {
6+
"$ref": "#/required%20string/type"
7+
},
8+
"name": {
9+
"$ref": "../definitions/name.yaml"
10+
},
11+
"age": {
12+
"type": "integer",
13+
"minimum": 0
14+
},
15+
"gender": {
16+
"type": "string",
17+
"enum": ["male", "female"]
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
title: name
2+
type: object
3+
required:
4+
- first
5+
- last
6+
properties:
7+
first:
8+
$ref: ../definitions/definitions.json#/required string
9+
last:
10+
$ref: ./required-string.yaml
11+
middle:
12+
type:
13+
$ref: "definitions.json#/name/properties/first/type"
14+
minLength:
15+
$ref: "definitions.json#/name/properties/first/minLength"
16+
prefix:
17+
$ref: "../definitions/definitions.json#/name/properties/last"
18+
minLength: 3
19+
suffix:
20+
type: string
21+
$ref: "definitions.json#/name/properties/prefix"
22+
maxLength: 3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
title: required string
2+
type: string
3+
minLength: 1

0 commit comments

Comments
 (0)