Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
'use strict';
/**
* a collection of cloning functions
*/
/**
* a no-op placeholder which returns the given object unchanged
* useful for when a clone function needs to be passed but cloning is not
* required
* @param obj the input object
* @return the input object, unchanged
*/
function nop(obj) {
return obj;
}
/**
* clones the given object using JSON.parse and JSON.stringify
* @param obj the object to clone
* @return the cloned object
*/
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
/**
* clones the given object's properties shallowly, ignores properties from prototype
* @param obj the object to clone
* @return the cloned object
*/
function shallowClone(obj) {
let result = {};
for (let p in obj) {
if (obj.hasOwnProperty(p)) {
result[p] = obj[p];
}
}
return result;
}
/**
* clones the given object's properties deeply, ignores properties from prototype
* @param obj the object to clone
* @return the cloned object
*/
function deepClone(obj) {
let result = Array.isArray(obj) ? [] : {};
for (let p in obj) {
if (obj.hasOwnProperty(p) || Array.isArray(obj)) {
result[p] = (typeof obj[p] === 'object') ? deepClone(obj[p]) : obj[p];
}
}
return result;
}
/**
* clones the given object's properties shallowly, using Object.assign
* @param obj the object to clone
* @return the cloned object
*/
function fastClone(obj) {
return Object.assign({},obj);
}
/**
* Source: stackoverflow http://bit.ly/2A1Kha6
*/
function circularClone(obj, hash) {
if (!hash) hash = new WeakMap();
// Do not try to clone primitives or functions
if (Object(obj) !== obj || obj instanceof Function) return obj;
if (hash.has(obj)) return hash.get(obj); // Cyclic reference
try { // Try to run constructor (without arguments, as we don't know them)
var result = new obj.constructor();
} catch(e) { // Constructor failed, create object without running the constructor
result = Object.create(Object.getPrototypeOf(obj));
}
// Optional: support for some standard constructors (extend as desired)
/*if (obj instanceof Map)
Array.from(obj, ([key, val]) => result.set(circularClone(key, hash),
circularClone(val, hash)) );
else if (obj instanceof Set)
Array.from(obj, (key) => result.add(circularClone(key, hash)) );
*/
// Register in hash
hash.set(obj, result);
// Clone and assign enumerable own properties recursively
return Object.assign(result, ...Object.keys(obj).map (
key => ({ [key]: circularClone(obj[key], hash) }) ));
}
module.exports = {
nop : nop,
clone : clone,
shallowClone : shallowClone,
deepClone : deepClone,
fastClone : fastClone,
circularClone : circularClone
};
+105
View File
@@ -0,0 +1,105 @@
'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
};
+21
View File
@@ -0,0 +1,21 @@
'use strict';
const recurse = require('./recurse.js').recurse;
function findObj(container,obj) {
if (container === obj) {
return { found: true, path: '#/', parent: null };
}
let result = { found: false, path: null, parent: null };
recurse(container,{},function(o,key,state){
if ((o[key] === obj) && (!result.found)) {
result = { found: true, path: state.path, parent: o };
}
});
return result;
}
module.exports = {
findObj: findObj
};
+41
View File
@@ -0,0 +1,41 @@
'use strict';
const recurse = require('./recurse.js').recurse;
/**
* flattens an object into an array of properties
* @param obj the object to flatten
* @param callback a function which can mutate or filter the entries (by returning null)
* @return the flattened object as an array of properties
*/
function flatten(obj,callback) {
let arr = [];
let iDepth, oDepth = 0;
let state = {identityDetection:true};
recurse(obj,state,function(obj,key,state){
let entry = {};
entry.name = key;
entry.value = obj[key];
entry.path = state.path;
entry.parent = obj;
entry.key = key;
if (callback) entry = callback(entry);
if (entry) {
if (state.depth > iDepth) {
oDepth++;
}
else if (state.depth < iDepth) {
oDepth--;
}
entry.depth = oDepth;
iDepth = state.depth;
arr.push(entry);
}
});
return arr;
}
module.exports = {
flatten : flatten
};
+10
View File
@@ -0,0 +1,10 @@
'use strict';
function isRef(obj,key) {
return ((key === '$ref') && (!!obj && typeof obj[key] === 'string'));
}
module.exports = {
isRef: isRef
};
+97
View File
@@ -0,0 +1,97 @@
'use strict';
/**
* escapes JSON Pointer using ~0 for ~ and ~1 for /
* @param s the string to escape
* @return the escaped string
*/
function jpescape(s) {
return s.replace(/\~/g, '~0').replace(/\//g, '~1');
}
/**
* unescapes JSON Pointer using ~0 for ~ and ~1 for /
* @param s the string to unescape
* @return the unescaped string
*/
function jpunescape(s) {
return s.replace(/\~1/g, '/').replace(/~0/g, '~');
}
// JSON Pointer specification: http://tools.ietf.org/html/rfc6901
/**
* from obj, return the property with a JSON Pointer prop, optionally setting it
* to newValue
* @param obj the object to point into
* @param prop the JSON Pointer or JSON Reference
* @param newValue optional value to set the property to
* @return the found property, or false
*/
function jptr(obj, prop, newValue) {
if (typeof obj === 'undefined') return false;
if (!prop || typeof prop !== 'string' || (prop === '#')) return (typeof newValue !== 'undefined' ? newValue : obj);
if (prop.indexOf('#')>=0) {
let parts = prop.split('#');
let uri = parts[0];
if (uri) return false; // we do internal resolution only
prop = parts[1];
prop = decodeURIComponent(prop.slice(1).split('+').join(' '));
}
if (prop.startsWith('/')) prop = prop.slice(1);
let components = prop.split('/');
for (let i=0;i<components.length;i++) {
components[i] = jpunescape(components[i]);
let setAndLast = (typeof newValue !== 'undefined') && (i == components.length-1);
let index = parseInt(components[i],10);
if (!Array.isArray(obj) || isNaN(index) || (index.toString() !== components[i])) {
index = (Array.isArray(obj) && components[i] === '-') ? -2 : -1;
}
else {
components[i] = (i > 0) ? components[i-1] : ''; // backtrack to indexed property name
}
if ((index != -1) || (obj && obj.hasOwnProperty(components[i]))) {
if (index >= 0) {
if (setAndLast) {
obj[index] = newValue;
}
obj = obj[index];
}
else if (index === -2) {
if (setAndLast) {
if (Array.isArray(obj)) {
obj.push(newValue);
}
return newValue;
}
else return undefined;
}
else {
if (setAndLast) {
obj[components[i]] = newValue;
}
obj = obj[components[i]];
}
}
else {
if ((typeof newValue !== 'undefined') && (typeof obj === 'object') &&
(!Array.isArray(obj))) {
obj[components[i]] = (setAndLast ? newValue : ((components[i+1] === '0' || components[i+1] === '-') ? [] : {}));
obj = obj[components[i]];
}
else return false;
}
}
return obj;
}
module.exports = {
jptr : jptr,
jpescape : jpescape,
jpunescape : jpunescape
};
+62
View File
@@ -0,0 +1,62 @@
'use strict';
const jpescape = require('./jptr.js').jpescape;
function defaultState() {
return {
path: '#',
depth: 0,
pkey: '',
parent: {},
payload: {},
seen: new WeakMap(),
identity: false,
identityDetection: false
};
}
/**
* recurses through the properties of an object, given an optional starting state
* anything you pass in state.payload is passed to the callback each time
* @param object the object to recurse through
* @param state optional starting state, can be set to null or {}
* @param callback the function which receives object,key,state on each property
*/
function recurse(object, state, callback) {
if (!state) state = {depth:0};
if (!state.depth) {
state = Object.assign({},defaultState(),state);
}
if (typeof object !== 'object') return;
let oPath = state.path;
for (let key in object) {
state.key = key;
state.path = state.path + '/' + encodeURIComponent(jpescape(key));
state.identityPath = state.seen.get(object[key]);
state.identity = (typeof state.identityPath !== 'undefined');
if (object.hasOwnProperty(key)) {
callback(object, key, state);
}
if ((typeof object[key] === 'object') && (!state.identity)) {
if (state.identityDetection && !Array.isArray(object[key]) && object[key] !== null) {
state.seen.set(object[key],state.path);
}
let newState = {};
newState.parent = object;
newState.path = state.path;
newState.depth = state.depth ? state.depth+1 : 1;
newState.pkey = key;
newState.payload = state.payload;
newState.seen = state.seen;
newState.identity = false;
newState.identityDetection = state.identityDetection;
recurse(object[key], newState, callback);
}
state.path = oPath;
}
}
module.exports = {
recurse : recurse
};
+34
View File
@@ -0,0 +1,34 @@
'use strict';
const recurse = require('./recurse.js').recurse;
const jptr = require('./jptr.js').jptr;
/**
* Simply modifies an object to have no self-references by replacing them
* with $ref pointers
* @param obj the object to re-reference
* @param options may contain a prefix property for the generated refs
* @return the re-referenced object (mutated)
*/
function reref(obj,options) {
let master = obj;
recurse(obj,{identityDetection:true},function(obj,key,state){
if (state.identity) {
let replacement = jptr(master,state.identityPath);
// ensure it's still there and we've not reffed it away
let newRef = state.identityPath;
if (state.identityPath !== state.path) {
if (options && options.prefix) newRef = newRef.replace('#/',options.prefix);
if (replacement !== false) obj[key] = { $ref: newRef }
else if (options && options.verbose) console.warn(state.identityPath,'gone away at',state.path);
}
}
});
return obj;
}
module.exports = {
reref : reref
};
+148
View File
@@ -0,0 +1,148 @@
'use strict';
/**
* TopoSort function is LICENSE: MIT, everything else is BSD-3-Clause
*/
/*
* Source: https://simplapi.wordpress.com/2015/08/19/detect-graph-cycle-in-javascript/
* removed dependency on underscore, MER
*/
const recurse = require('./recurse.js').recurse;
const isRef = require('./isref.js').isRef;
/*
Nodes should look like:
var nodes = [
{ _id: "3", links: ["8", "10"] },
{ _id: "5", links: ["11"] },
{ _id: "7", links: ["11", "8"] },
{ _id: "8", links: ["9"] },
{ _id: "11", links: ["2", "9", "10"] },
{ _id: "10", links: [] },
{ _id: "9", links: [] },
{ _id: "2", links: [] }
];
*/
/**
* Try to get a topological sorting out of directed graph.
*
* @typedef {Object} Result
* @property {array} sort the sort, empty if not found
* @property {array} nodesWithEdges, will be empty unless a cycle is found
* @param nodes {Object} A list of nodes, including edges (see below).
* @return {Result}
*/
function toposort (nodes) {
// Test if a node has got any incoming edges
function hasIncomingEdge(list, node) {
for (var i = 0, l = list.length; i < l; ++i) {
if (list[i].links.find(function(e,i,a){
return node._id == e;
})) return true;
}
return false;
};
// Kahn's Algorithm
var L = [],
S = nodes.filter(function(node) {
return !hasIncomingEdge(nodes, node);
}),
n = null;
while(S.length) {
// Remove a node n from S
n = S.pop();
// Add n to tail of L
L.push(n);
var i = n.links.length;
while (i--) {
// Getting the node associated to the current stored id in links
var m = nodes.find(function(e){
return n.links[i] === e._id;
});
if (!m) throw new Error('Could not find node from link: '+n.links[i]);
// Remove edge e from the graph
n.links.pop();
if (!hasIncomingEdge(nodes, m)) {
S.push(m);
}
}
}
// If any of them still have links, there is cycle somewhere
var nodesWithEdges = nodes.filter(function(node) {
return node.links.length !== 0;
});
// modified to return both the cyclic nodes and the sort
return {
sort : nodesWithEdges.length == 0 ? L : null,
nodesWithEdges : nodesWithEdges
};
}
/**
* Takes an object and creates a graph of JSON Pointer / References
* @param obj the object to convert
* @param containerName the property containing definitions. Default: definitions
* @return the graph suitable for passing to toposort()
*/
function objToGraph(obj, containerName) {
if (!containerName) containerName = 'definitions';
let graph = [];
for (let p in obj[containerName]) {
let entry = {};
entry._id = '#/'+containerName+'/'+p;
entry.links = [];
graph.push(entry);
}
recurse(obj,{identityDetection:true},function(obj,key,state){
if (isRef(obj,key)) {
let ptr = obj[key].replace('/$ref','');
let spath = state.path.replace('/$ref','');
let target = graph.find(function(e){
return e._id === ptr;
});
if (target) {
target.links.push(ptr);
}
else {
target = {};
target._id = ptr;
target.links = [];
target.links.push(ptr);
graph.push(target);
}
let source = graph.find(function(e){
return e._id === spath;
});
if (source) {
//source.links.push(spath);
}
else {
source = {};
source._id = spath
source.links = [];
graph.push(source);
}
}
});
return graph;
}
module.exports = {
toposort : toposort,
objToGraph : objToGraph
};
+65
View File
@@ -0,0 +1,65 @@
'use strict';
const recurse = require('./recurse.js').recurse;
const jptr = require('./jptr.js').jptr;
/**
* Given an expanded object and an optional object to compare to (e.g. its $ref'd form), will call
* the following functions:
* * callbacks.before - lets you modify the initial starting state, must return it
* * callbacks.where - lets you select a subset of properties, return a truthy value
* * callbacks.filter - called for all selected properties, can mutate/remove (by setting to undefined)
* * callbacks.compare - allowing the objects to be compared by path (i.e. for $ref reinstating)
* * callbacks.identity - called on any object identity (previously seen) properties
* * callbacks.selected - called for all selected/unfiltered properties, does not mutate directly
* * callbacks.count - called at the end with the number of selected properties
* * callbacks.finally - called at the end of the traversal
* @param obj the object to visit
* @param comparison optional object to compare to
* @param callbacks object containing functions as above
* @return the possibly mutated object
*/
function visit(obj,comparison,callbacks) {
let state = {identityDetection:true};
let count = 0;
if (callbacks.before) state = callbacks.before(obj,'',{});
recurse(obj,state,function(obj,key,state){
let selected = true;
if (callbacks.where) {
selected = callbacks.where(obj,key,state);
}
if (selected) {
if (callbacks.filter) {
obj[key] = callbacks.filter(obj,key,state);
if (typeof obj[key] === 'undefined') {
delete obj[key]; // to be doubly sure
}
}
if (typeof obj[key] !== 'undefined') {
if (callbacks.compare && comparison) {
let equiv = jptr(comparison,state.path);
if (equiv) {
obj[key] = callbacks.compare(obj,key,state,equiv);
}
}
if (typeof obj[key] !== 'undefined' && state.identity && callbacks.identity) {
obj[key] = callbacks.identity(obj,key,state,state.identityPath);
}
if (typeof obj[key] !== 'undefined') {
if (callbacks.selected) {
callbacks.selected(obj,key,state);
}
count++;
}
}
}
});
if (callbacks.count) callbacks.count(obj,'',state,count);
if (callbacks.finally) callbacks.finally(obj,'',state);
return obj;
}
module.exports = {
visit : visit
};