106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const recurse = require('./recurse.js').recurse;
|
||
|
const clone = require('./clone.js').shallowClone;
|
||
|
const jptr = require('./jptr.js').jptr;
|
||
|
const isRef = require('./isref.js').isRef;
|
||
|
|
||
|
var getLogger = function (options) {
|
||
|
if (options && options.verbose) {
|
||
|
return {
|
||
|
warn: function() {
|
||
|
var args = Array.prototype.slice.call(arguments);
|
||
|
console.warn.apply(console, args);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
return {
|
||
|
warn: function() {
|
||
|
//nop
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* dereferences the given object
|
||
|
* @param o the object to dereference
|
||
|
* @definitions a source of definitions to reference
|
||
|
* @options optional settings (used recursively)
|
||
|
* @return the dereferenced object
|
||
|
*/
|
||
|
function dereference(o,definitions,options) {
|
||
|
if (!options) options = {};
|
||
|
if (!options.cache) options.cache = {};
|
||
|
if (!options.state) options.state = {};
|
||
|
options.state.identityDetection = true;
|
||
|
// options.depth allows us to limit cloning to the first invocation
|
||
|
options.depth = (options.depth ? options.depth+1 : 1);
|
||
|
let obj = (options.depth > 1 ? o : clone(o));
|
||
|
let container = { data: obj };
|
||
|
let defs = (options.depth > 1 ? definitions : clone(definitions));
|
||
|
// options.master is the top level object, regardless of depth
|
||
|
if (!options.master) options.master = obj;
|
||
|
|
||
|
let logger = getLogger(options);
|
||
|
|
||
|
let changes = 1;
|
||
|
while (changes > 0) {
|
||
|
changes = 0;
|
||
|
recurse(container,options.state,function(obj,key,state){
|
||
|
if (isRef(obj,key)) {
|
||
|
let $ref = obj[key]; // immutable
|
||
|
changes++;
|
||
|
if (!options.cache[$ref]) {
|
||
|
let entry = {};
|
||
|
entry.path = state.path.split('/$ref')[0];
|
||
|
entry.key = $ref;
|
||
|
logger.warn('Dereffing %s at %s',$ref,entry.path);
|
||
|
entry.source = defs;
|
||
|
entry.data = jptr(entry.source,entry.key);
|
||
|
if (entry.data === false) {
|
||
|
entry.data = jptr(options.master,entry.key);
|
||
|
entry.source = options.master;
|
||
|
}
|
||
|
if (entry.data === false) {
|
||
|
logger.warn('Missing $ref target',entry.key);
|
||
|
}
|
||
|
options.cache[$ref] = entry;
|
||
|
entry.data = state.parent[state.pkey] = dereference(jptr(entry.source,entry.key),entry.source,options);
|
||
|
if (options.$ref && (typeof state.parent[state.pkey] === 'object') && (state.parent[state.pkey] !== null)) state.parent[state.pkey][options.$ref] = $ref;
|
||
|
entry.resolved = true;
|
||
|
}
|
||
|
else {
|
||
|
let entry = options.cache[$ref];
|
||
|
if (entry.resolved) {
|
||
|
// we have already seen and resolved this reference
|
||
|
logger.warn('Patching %s for %s',$ref,entry.path);
|
||
|
state.parent[state.pkey] = entry.data;
|
||
|
if (options.$ref && (typeof state.parent[state.pkey] === 'object') && (state.parent[state.pkey] !== null)) state.parent[state.pkey][options.$ref] = $ref;
|
||
|
}
|
||
|
else if ($ref === entry.path) {
|
||
|
// reference to itself, throw
|
||
|
throw new Error(`Tight circle at ${entry.path}`);
|
||
|
}
|
||
|
else {
|
||
|
// we're dealing with a circular reference here
|
||
|
logger.warn('Unresolved ref');
|
||
|
state.parent[state.pkey] = jptr(entry.source,entry.path);
|
||
|
if (state.parent[state.pkey] === false) {
|
||
|
state.parent[state.pkey] = jptr(entry.source,entry.key);
|
||
|
}
|
||
|
if (options.$ref && (typeof state.parent[state.pkey] === 'object') && (state.parent[state.pkey] !== null)) state.parent[options.$ref] = $ref;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
return container.data;
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
dereference : dereference
|
||
|
};
|
||
|
|