1 | ;
|
2 |
|
3 | var $Ref = require('./ref'),
|
4 | Pointer = require('./pointer'),
|
5 | url = require('./util/url');
|
6 |
|
7 | module.exports = bundle;
|
8 |
|
9 | /**
|
10 | * Bundles all external JSON references into the main JSON schema, thus resulting in a schema that
|
11 | * only has *internal* references, not any *external* references.
|
12 | * This method mutates the JSON schema object, adding new references and re-mapping existing ones.
|
13 | *
|
14 | * @param {$RefParser} parser
|
15 | * @param {$RefParserOptions} options
|
16 | */
|
17 | function bundle (parser, options) {
|
18 | // console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
|
19 |
|
20 | // Build an inventory of all $ref pointers in the JSON Schema
|
21 | var inventory = [];
|
22 | crawl(parser, 'schema', parser.$refs._root$Ref.path + '#', '#', 0, inventory, parser.$refs, options);
|
23 |
|
24 | // Remap all $ref pointers
|
25 | remap(inventory);
|
26 | }
|
27 |
|
28 | /**
|
29 | * Recursively crawls the given value, and inventories all JSON references.
|
30 | *
|
31 | * @param {object} parent - The object containing the value to crawl. If the value is not an object or array, it will be ignored.
|
32 | * @param {string} key - The property key of `parent` to be crawled
|
33 | * @param {string} path - The full path of the property being crawled, possibly with a JSON Pointer in the hash
|
34 | * @param {string} pathFromRoot - The path of the property being crawled, from the schema root
|
35 | * @param {object[]} inventory - An array of already-inventoried $ref pointers
|
36 | * @param {$Refs} $refs
|
37 | * @param {$RefParserOptions} options
|
38 | */
|
39 | function crawl (parent, key, path, pathFromRoot, indirections, inventory, $refs, options) {
|
40 | var obj = key === null ? parent : parent[key];
|
41 |
|
42 | if (obj && typeof obj === 'object') {
|
43 | if ($Ref.isAllowed$Ref(obj)) {
|
44 | inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
|
45 | }
|
46 | else {
|
47 | // Crawl the object in a specific order that's optimized for bundling.
|
48 | // This is important because it determines how `pathFromRoot` gets built,
|
49 | // which later determines which keys get dereferenced and which ones get remapped
|
50 | var keys = Object.keys(obj)
|
51 | .sort(function (a, b) {
|
52 | // Most people will expect references to be bundled into the the "definitions" property,
|
53 | // so we always crawl that property first, if it exists.
|
54 | if (a === 'definitions') {
|
55 | return -1;
|
56 | }
|
57 | else if (b === 'definitions') {
|
58 | return 1;
|
59 | }
|
60 | else {
|
61 | // Otherwise, crawl the keys based on their length.
|
62 | // This produces the shortest possible bundled references
|
63 | return a.length - b.length;
|
64 | }
|
65 | });
|
66 |
|
67 | keys.forEach(function (key) {
|
68 | var keyPath = Pointer.join(path, key);
|
69 | var keyPathFromRoot = Pointer.join(pathFromRoot, key);
|
70 | var value = obj[key];
|
71 |
|
72 | if ($Ref.isAllowed$Ref(value)) {
|
73 | inventory$Ref(obj, key, path, keyPathFromRoot, indirections, inventory, $refs, options);
|
74 | }
|
75 | else {
|
76 | crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
|
77 | }
|
78 | });
|
79 | }
|
80 | }
|
81 | }
|
82 |
|
83 | /**
|
84 | * Inventories the given JSON Reference (i.e. records detailed information about it so we can
|
85 | * optimize all $refs in the schema), and then crawls the resolved value.
|
86 | *
|
87 | * @param {object} $refParent - The object that contains a JSON Reference as one of its keys
|
88 | * @param {string} $refKey - The key in `$refParent` that is a JSON Reference
|
89 | * @param {string} path - The full path of the JSON Reference at `$refKey`, possibly with a JSON Pointer in the hash
|
90 | * @param {string} pathFromRoot - The path of the JSON Reference at `$refKey`, from the schema root
|
91 | * @param {object[]} inventory - An array of already-inventoried $ref pointers
|
92 | * @param {$Refs} $refs
|
93 | * @param {$RefParserOptions} options
|
94 | */
|
95 | function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, inventory, $refs, options) {
|
96 | var $ref = $refKey === null ? $refParent : $refParent[$refKey];
|
97 | var $refPath = url.resolve(path, $ref.$ref);
|
98 | var pointer = $refs._resolve($refPath, options);
|
99 | var depth = Pointer.parse(pathFromRoot).length;
|
100 | var file = url.stripHash(pointer.path);
|
101 | var hash = url.getHash(pointer.path);
|
102 | var external = file !== $refs._root$Ref.path;
|
103 | var extended = $Ref.isExtended$Ref($ref);
|
104 | indirections += pointer.indirections;
|
105 |
|
106 | var existingEntry = findInInventory(inventory, $refParent, $refKey);
|
107 | if (existingEntry) {
|
108 | // This $Ref has already been inventoried, so we don't need to process it again
|
109 | if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
|
110 | removeFromInventory(inventory, existingEntry);
|
111 | }
|
112 | else {
|
113 | return;
|
114 | }
|
115 | }
|
116 |
|
117 | inventory.push({
|
118 | $ref: $ref, // The JSON Reference (e.g. {$ref: string})
|
119 | parent: $refParent, // The object that contains this $ref pointer
|
120 | key: $refKey, // The key in `parent` that is the $ref pointer
|
121 | pathFromRoot: pathFromRoot, // The path to the $ref pointer, from the JSON Schema root
|
122 | depth: depth, // How far from the JSON Schema root is this $ref pointer?
|
123 | file: file, // The file that the $ref pointer resolves to
|
124 | hash: hash, // The hash within `file` that the $ref pointer resolves to
|
125 | value: pointer.value, // The resolved value of the $ref pointer
|
126 | circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
|
127 | extended: extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
|
128 | external: external, // Does this $ref pointer point to a file other than the main JSON Schema file?
|
129 | indirections: indirections, // The number of indirect references that were traversed to resolve the value
|
130 | });
|
131 |
|
132 | // Recursively crawl the resolved value
|
133 | crawl(pointer.value, null, pointer.path, pathFromRoot, indirections + 1, inventory, $refs, options);
|
134 | }
|
135 |
|
136 | /**
|
137 | * Re-maps every $ref pointer, so that they're all relative to the root of the JSON Schema.
|
138 | * Each referenced value is dereferenced EXACTLY ONCE. All subsequent references to the same
|
139 | * value are re-mapped to point to the first reference.
|
140 | *
|
141 | * @example:
|
142 | * {
|
143 | * first: { $ref: somefile.json#/some/part },
|
144 | * second: { $ref: somefile.json#/another/part },
|
145 | * third: { $ref: somefile.json },
|
146 | * fourth: { $ref: somefile.json#/some/part/sub/part }
|
147 | * }
|
148 | *
|
149 | * In this example, there are four references to the same file, but since the third reference points
|
150 | * to the ENTIRE file, that's the only one we need to dereference. The other three can just be
|
151 | * remapped to point inside the third one.
|
152 | *
|
153 | * On the other hand, if the third reference DIDN'T exist, then the first and second would both need
|
154 | * to be dereferenced, since they point to different parts of the file. The fourth reference does NOT
|
155 | * need to be dereferenced, because it can be remapped to point inside the first one.
|
156 | *
|
157 | * @param {object[]} inventory
|
158 | */
|
159 | function remap (inventory) {
|
160 | // Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
|
161 | inventory.sort(function (a, b) {
|
162 | if (a.file !== b.file) {
|
163 | // Group all the $refs that point to the same file
|
164 | return a.file < b.file ? -1 : +1;
|
165 | }
|
166 | else if (a.hash !== b.hash) {
|
167 | // Group all the $refs that point to the same part of the file
|
168 | return a.hash < b.hash ? -1 : +1;
|
169 | }
|
170 | else if (a.circular !== b.circular) {
|
171 | // If the $ref points to itself, then sort it higher than other $refs that point to this $ref
|
172 | return a.circular ? -1 : +1;
|
173 | }
|
174 | else if (a.extended !== b.extended) {
|
175 | // If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value
|
176 | return a.extended ? +1 : -1;
|
177 | }
|
178 | else if (a.indirections !== b.indirections) {
|
179 | // Sort direct references higher than indirect references
|
180 | return a.indirections - b.indirections;
|
181 | }
|
182 | else if (a.depth !== b.depth) {
|
183 | // Sort $refs by how close they are to the JSON Schema root
|
184 | return a.depth - b.depth;
|
185 | }
|
186 | else {
|
187 | // Determine how far each $ref is from the "definitions" property.
|
188 | // Most people will expect references to be bundled into the the "definitions" property if possible.
|
189 | var aDefinitionsIndex = a.pathFromRoot.lastIndexOf('/definitions');
|
190 | var bDefinitionsIndex = b.pathFromRoot.lastIndexOf('/definitions');
|
191 |
|
192 | if (aDefinitionsIndex !== bDefinitionsIndex) {
|
193 | // Give higher priority to the $ref that's closer to the "definitions" property
|
194 | return bDefinitionsIndex - aDefinitionsIndex;
|
195 | }
|
196 | else {
|
197 | // All else is equal, so use the shorter path, which will produce the shortest possible reference
|
198 | return a.pathFromRoot.length - b.pathFromRoot.length;
|
199 | }
|
200 | }
|
201 | });
|
202 |
|
203 | var file, hash, pathFromRoot;
|
204 | inventory.forEach(function (entry) {
|
205 | // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
|
206 |
|
207 | if (!entry.external) {
|
208 | // This $ref already resolves to the main JSON Schema file
|
209 | entry.$ref.$ref = entry.hash;
|
210 | }
|
211 | else if (entry.file === file && entry.hash === hash) {
|
212 | // This $ref points to the same value as the prevous $ref, so remap it to the same path
|
213 | entry.$ref.$ref = pathFromRoot;
|
214 | }
|
215 | else if (entry.file === file && entry.hash.indexOf(hash + '/') === 0) {
|
216 | // This $ref points to the a sub-value as the prevous $ref, so remap it beneath that path
|
217 | entry.$ref.$ref = Pointer.join(pathFromRoot, Pointer.parse(entry.hash));
|
218 | }
|
219 | else {
|
220 | // We've moved to a new file or new hash
|
221 | file = entry.file;
|
222 | hash = entry.hash;
|
223 | pathFromRoot = entry.pathFromRoot;
|
224 |
|
225 | // This is the first $ref to point to this value, so dereference the value.
|
226 | // Any other $refs that point to the same value will point to this $ref instead
|
227 | entry.$ref = entry.parent[entry.key] = $Ref.dereference(entry.$ref, entry.value);
|
228 |
|
229 | if (entry.circular) {
|
230 | // This $ref points to itself
|
231 | entry.$ref.$ref = entry.pathFromRoot;
|
232 | }
|
233 | }
|
234 |
|
235 | // console.log(' new value: %s', (entry.$ref && entry.$ref.$ref) ? entry.$ref.$ref : '[object Object]');
|
236 | });
|
237 | }
|
238 |
|
239 | /**
|
240 | * TODO
|
241 | */
|
242 | function findInInventory (inventory, $refParent, $refKey) {
|
243 | for (var i = 0; i < inventory.length; i++) {
|
244 | var existingEntry = inventory[i];
|
245 | if (existingEntry.parent === $refParent && existingEntry.key === $refKey) {
|
246 | return existingEntry;
|
247 | }
|
248 | }
|
249 | }
|
250 |
|
251 | function removeFromInventory (inventory, entry) {
|
252 | var index = inventory.indexOf(entry);
|
253 | inventory.splice(index, 1);
|
254 | }
|