Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 118 additions & 43 deletions lib/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ function bundle (parser, options) {
let inventory = [];
crawl(parser, "schema", parser.$refs._root$Ref.path + "#", "#", 0, inventory, parser.$refs, options);

// Group/sort pointers into specificity order
sort(inventory);

// add additional references so that external references are referenced from the same relative path as they had in their original file
if (options.bundle && options.bundle.shallowReferences) {
inventory = addShallowReferences(parser, inventory)
}

// Remap all $ref pointers
remap(inventory);
}
Expand Down Expand Up @@ -162,49 +170,6 @@ function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, i
* @param {object[]} inventory
*/
function remap (inventory) {
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
inventory.sort((a, b) => {
if (a.file !== b.file) {
// Group all the $refs that point to the same file
return a.file < b.file ? -1 : +1;
}
else if (a.hash !== b.hash) {
// Group all the $refs that point to the same part of the file
return a.hash < b.hash ? -1 : +1;
}
else if (a.circular !== b.circular) {
// If the $ref points to itself, then sort it higher than other $refs that point to this $ref
return a.circular ? -1 : +1;
}
else if (a.extended !== b.extended) {
// If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value
return a.extended ? +1 : -1;
}
else if (a.indirections !== b.indirections) {
// Sort direct references higher than indirect references
return a.indirections - b.indirections;
}
else if (a.depth !== b.depth) {
// Sort $refs by how close they are to the JSON Schema root
return a.depth - b.depth;
}
else {
// Determine how far each $ref is from the "definitions" property.
// Most people will expect references to be bundled into the the "definitions" property if possible.
let aDefinitionsIndex = a.pathFromRoot.lastIndexOf("/definitions");
let bDefinitionsIndex = b.pathFromRoot.lastIndexOf("/definitions");

if (aDefinitionsIndex !== bDefinitionsIndex) {
// Give higher priority to the $ref that's closer to the "definitions" property
return bDefinitionsIndex - aDefinitionsIndex;
}
else {
// All else is equal, so use the shorter path, which will produce the shortest possible reference
return a.pathFromRoot.length - b.pathFromRoot.length;
}
}
});

let file, hash, pathFromRoot;
for (let entry of inventory) {
// console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
Expand Down Expand Up @@ -257,3 +222,113 @@ function removeFromInventory (inventory, entry) {
let index = inventory.indexOf(entry);
inventory.splice(index, 1);
}

// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
function sort(inventory) {
return inventory.sort((a, b) => {
if (a.file !== b.file) {
// Group all the $refs that point to the same file
return a.file < b.file ? -1 : +1;
}
else if (a.hash !== b.hash) {
// Group all the $refs that point to the same part of the file
return a.hash < b.hash ? -1 : +1;
}
else if (a.circular !== b.circular) {
// If the $ref points to itself, then sort it higher than other $refs that point to this $ref
return a.circular ? -1 : +1;
}
else if (a.extended !== b.extended) {
// If the $ref extends the resolved value, then sort it lower than other $refs that don't extend the value
return a.extended ? +1 : -1;
}
else if (a.indirections !== b.indirections) {
// Sort direct references higher than indirect references
return a.indirections - b.indirections;
}
else if (a.depth !== b.depth) {
// Sort $refs by how close they are to the JSON Schema root
return a.depth - b.depth;
}
else {
// Determine how far each $ref is from the "definitions" property.
// Most people will expect references to be bundled into the the "definitions" property if possible.
let aDefinitionsIndex = a.pathFromRoot.lastIndexOf("/definitions");
let bDefinitionsIndex = b.pathFromRoot.lastIndexOf("/definitions");

if (aDefinitionsIndex !== bDefinitionsIndex) {
// Give higher priority to the $ref that's closer to the "definitions" property
return bDefinitionsIndex - aDefinitionsIndex;
}
else {
// All else is equal, so use the shorter path, which will produce the shortest possible reference
return a.pathFromRoot.length - b.pathFromRoot.length;
}
}
});
}

// when an external reference is made, a new reference is added to the schema, at the same path as the external reference had relative to the external root.
function addShallowReferences(parser, inventory) {
inventory = inventory.reduce((inv, entry) => {
if (entry.external) {
const prev = inv[inv.length - 1] || { file: null, hash: null }

// if this is not already a shallow reference (depth 2 or less),
// when we reference a new file, or a new hash (that is not a subhash of the previous) add a new reference
if (entry.depth > 2 && (entry.file !== prev.file || (entry.file === prev.file && entry.hash !== prev.hash && entry.hash.indexOf(prev.hash + '/') !== 0))) {
const path = entry.hash.substring(2)
const pathParts = path.split('/')
const refClone = { $ref: entry.$ref.$ref }
const parent = set(parser.schema, pathParts, refClone)

if (!parent) {
throw new Error(`Adding a shallow reference for ${entry.$ref.$ref} would overwrite an existing part of the schema.`)
}

const key = pathParts[pathParts.length - 1]

inv.push({
$ref: refClone, // The JSON Reference (e.g. {$ref: string})
parent, // The object that contains this $ref pointer
key, // The key in `parent` that is the $ref pointer
pathFromRoot: entry.hash, // The path to the $ref pointer, from the JSON Schema root
depth: pathParts.length, // How far from the JSON Schema root is this $ref pointer?
file: entry.file, // The file that the $ref pointer resolves to
hash: entry.hash, // The hash within `file` that the $ref pointer resolves to
value: entry.value, // The resolved value of the $ref pointer
circular: entry.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
extended: false, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
external: true, // Does this $ref pointer point to a file other than the main JSON Schema file?
indirections: 0, // The number of indirect references that were traversed to resolve the value
})
}
}

inv.push(entry)
return inv
}, [])

return inventory
}

// set a value at a path in the object, return the parent of set value
function set(obj, pathParts, value) {
if (pathParts.length === 1) {
if (obj[pathParts[0]] !== undefined) {
return null;
} else {
obj[pathParts[0]] = value
return obj
}
} else {
let next = obj[pathParts[0]]

if (next === undefined) {
next = {}
obj[pathParts[0]] = next
}

return set(next, pathParts.slice(1), value)
}
}