json_merger
Version:
Merge JSON with indicators such as @override, @match, @delete and @insert to tell the processor how to merge the files.
435 lines (364 loc) • 14.6 kB
JavaScript
"use strict";
var fs = require('fs');
var path = require('path');
var ANTI_SANITIZER = {};
var indicators = require('./indicators.js');
var control_chars = require('./control_chars.js');
var util = require('./util.js');
/*******************************************************************************
* parseFile
*******************************************************************************/
var parseFile = function(file, options) {
var data = fs.readFileSync(file, 'utf-8');
// Remove UTF-8 BOM codes:
data = data.trim();
if (options.javascript) {
return util.js.parse(data);
}
else {
return JSON.parse(data);
}
return JSON.parse(data);
};
/*******************************************************************************
* stringify
*******************************************************************************/
var stringify = function(json, options) {
var indent = options.asText == 'pretty' ? '\t' : null;
if (options.javascript) {
return util.js.stringify(json, indent);
}
else {
return JSON.stringify(json, null, indent);
}
};
/*******************************************************************************
* processors
*******************************************************************************/
var processors = {};
processors.primitive = function(super_value, class_value, path, root) {
return class_value;
};
processors.dirrentType = function(super_value, class_value, path, root) {
return class_value;
};
processors.object = function(super_value, class_value, path, root) {
// If override is set to true return the class_value
// If override is set to an array override those properties
var override_action = class_value[indicators.OVERRIDE];
if (override_action === true) {
return class_value;
}
util.each(class_value, function(child_value, child_key) {
// Prevent indicators from being mapped out to super_value
if (util.isIndicator(child_key)) {
return;
}
// If super doesnt have the value simply set it
if (!util.has(super_value, child_key)) {
super_value[child_key] = child_value;
}
// If override is an array and child_key is present set it on super_value
else if (override_action && override_action.indexOf(child_key) > -1) {
super_value[child_key] = child_value;
}
// call function again and set return on super_value
else {
super_value[child_key] = processors.unknown(super_value[child_key], child_value, path + '/' + child_key, root);
}
});
// If delete is an array and child_key is present set super_value to
// control_chars.DELETE.
var delete_action = class_value[indicators.DELETE];
if (util.isArray(delete_action)) {
util.each(super_value, function(child_value, child_key) {
if (delete_action.indexOf(child_key) > -1) {
super_value[child_key] = control_chars.DELETE;
}
});
}
return super_value;
};
processors.array = function(super_value, class_value, path, root) {
// Store childs that have the @append, @prepend, @insert indicator
var inserts = [];
// Use prepend_index to store the current index that we are prepending at, eg
// with two or more prepends we to make sure that the first @prepend will be
// first item of the prepented items and not the last.
var prepend_index = 0;
// Use super_child_key to store array index relative to super_value.
var super_child_key = 0;
util.each(class_value, function(child_value, child_key) {
// Fetch all @append and push them to the array after initial iteration.
if (util.has(child_value, indicators.APPEND)) {
inserts.push([-1, child_value]);
}
// Fetch all @prepend and unshift them to the array after initial iteration.
else if (util.has(child_value, indicators.PREPEND)) {
inserts.push([prepend_index, child_value]);
prepend_index++;
}
// Fetch all @insert and splice them to the array after initial iteration.
else if (util.has(child_value, indicators.INSERT)) {
inserts.push([child_value[indicators.INSERT], child_value]);
}
else if (util.has(child_value, indicators.MATCH)) {
// Send in parent path to avoid having to go up then down to match a
// specific item in an array
// So you can use:
// '@match: "[name=john]"' instead of '@match: "../[name=john]"'
var matchInfo = util.pathQuery(child_value[indicators.MATCH], path, root);
// If match is *not* found ignore and preserve super_value's child_value.
// If match is found determinate what to do, delete/override/etc
if (matchInfo != null) {
// Call recursive with match info's object and child_value
// Remove @match to prevent recursive iteration
delete child_value[indicators.MATCH];
var merge_json = processors.unknown(matchInfo.object, child_value,
matchInfo.path, root);
// control_chars.DELETE indicates that we have to delete the original
// match
if (merge_json === control_chars.DELETE) {
util.deleteProperty(root, matchInfo.path);
}
// If the original child_value have @move delete the property
// from the original position and push it to inserts
else if (util.has(child_value, indicators.MOVE)) {
util.deleteProperty(root, matchInfo.path);
inserts.push([child_value[indicators.MOVE], merge_json]);
}
else {
util.setProperty(root, matchInfo.path, merge_json);
}
}
}
// If the child have @delete === true delete the child from the array
else if (child_value[indicators.DELETE] === true) {
super_value.splice(super_child_key, 1);
}
else {
super_value[super_child_key] = processors.unknown(
super_value[super_child_key], child_value, path + '/' + child_key, root);
super_child_key++;
}
});
util.each(inserts, function(cfg) {
var index = cfg[0];
var child_value = cfg[1];
// splice treatment of nagative indexes is counter intuitive.
// eg index = -1 is the 2. to last position (not the last)
// eg index = -2 is the 3. to last position (not the 2. to last)
// Therefore we need to use push if index = -1
// and we need to index++ if index is less than -1
if (index == -1) {
super_value.push(child_value);
}
else {
if (index < -1) {
index++;
}
super_value.splice(index, 0, child_value);
}
});
return super_value;
};
processors.unknown = function(super_value, class_value, path, root) {
if (path == null) {
path = '';
}
// Remap the class_value if it have the @value indicator
if (util.isObject(class_value) && util.has(class_value, indicators.VALUE)) {
class_value = class_value[indicators.VALUE];
}
// Force delete the super_value if class_value have the @delete indicator set
// to true. This will allow deletion of primitives and arrays, with the
// following syntax:
// super = { arr: [ 1, 2, 3 ] }
// child = { arr: { "@delete": true } }
if (util.isObject(class_value) && util.has(class_value, indicators.DELETE)) {
// If delete is set to true return control_chars.DELETE, this will delete
// the property in the JSON.stringify
// If delete is set to an array delete those properties
if (class_value[indicators.DELETE] === true) {
return control_chars.DELETE;
}
}
var super_type = util.getType(super_value);
var class_type = util.getType(class_value);
/*****************************************************************************
* Primitives
*****************************************************************************/
if (util.isPrimitive(super_type) || util.isPrimitive(class_type)) {
return processors.primitive(super_value, class_value, path, root);
}
/*****************************************************************************
* Different types
*****************************************************************************/
if (super_type != class_type) {
return processors.dirrentType(super_value, class_value, path, root);
}
/*****************************************************************************
* Objects
*****************************************************************************/
if (util.isObject(class_value)) {
return processors.object(super_value, class_value, path, root);
}
/*****************************************************************************
* Arrays
*****************************************************************************/
if (util.isArray(class_value)) {
return processors.array(super_value, class_value, path, root);
}
throw new Error('Unsupported type.');
};
/*******************************************************************************
* merge
*******************************************************************************/
var merge = function(super_json, child_json) {
return processors.unknown(super_json, child_json, null, super_json);
};
/*******************************************************************************
* fromObject
*******************************************************************************/
var fromObject = function(class_json, opts) {
var options = util.extend({
javascript: false, // true, false
scope: '', // directory to look for initial file
variables: {
// contains a key->value object with variables to @extends
}
}, opts);
// Stores JSON objects of files that have to be merged together:
// ["a", "b"]
var file_list = util.isArray(class_json[indicators.EXTENDS]) ?
class_json[indicators.EXTENDS] :
class_json[indicators.EXTENDS] != null ?
[ class_json[indicators.EXTENDS] ] :
[];
// Delete @extends from base_json to avoid infinitive recursion
delete class_json[indicators.EXTENDS];
// Push class_json to the path list:
file_list.push(class_json);
/*****************************************************************************
* Merge file by file
*****************************************************************************/
// default config for each file required:
// set asText = false so we get an object returned
// set scope = file's directory so there can be relative references
var json_config = util.defaults({
asText: false,
scope: options._as ? path.dirname(file_path) : options.scope,
// _as is a hack so that we know its we are calling the function recursive
_as: ANTI_SANITIZER
}, options);
// super_json is our output, start by containing first file in array
var super_json = fromFile(file_list.shift(), json_config);
// TODO: Figure out when to sanitize super_json hopefully we don't have to
// santize all returns. I'm not sure about this, and I'm not sure if we
// can even sanitize super_json when we return it.
// Is super_json used by anyone recursively?
while (file_list.length) {
var json = fromFile(file_list.shift(), json_config);
merge(super_json, json);
}
// If this file is called from the outside eg json_merger.from...
// File('file_which_doesnt_extends_another_file.json')
// we need to sanitize the file and remove all indicators
if (options._as != ANTI_SANITIZER) {
super_json = util.sanitizeValue(super_json);
}
/*****************************************************************************
* Return super_json
*****************************************************************************/
return super_json;
};
/*******************************************************************************
* fromFile
*******************************************************************************/
var fromFile = function(file, opts) {
if (util.isObject(file) || util.isArray(file)) {
return file;
}
var options = util.extend({
asText: false, // true, false, 'pretty'
javascript: false, // true, false
scope: '', // directory to look for initial file
variables: {
// contains a key->value object with variables to @extends
}
}, opts);
file = util.template(file, options.variables);
/*****************************************************************************
* Initialize
*****************************************************************************/
var file_path;
// If the file path is absolute use it
// If not then prepend the current scope of the previous file.
if (path.resolve(file) === path.normalize(file)) {
file_path = path.normalize(file);
}
else {
file_path = path.normalize(
options.scope ? options.scope + '/' + file : file
);
}
var class_json = parseFile(file_path, options);
// Stores JSON objects of files that have to be merged together:
// ["a", "b"]
var file_list = util.isArray(class_json[indicators.EXTENDS]) ?
class_json[indicators.EXTENDS] :
class_json[indicators.EXTENDS] != null ?
[ class_json[indicators.EXTENDS] ] :
[];
// Delete @extends from base_json to avoid infinitive recursion
delete class_json[indicators.EXTENDS];
// Push class_json to the path list:
file_list.push(class_json);
/*****************************************************************************
* Merge file by file
*****************************************************************************/
// default config for each file required:
// set asText = false so we get an object returned
// set scope = file's directory so there can be relative references
var json_config = util.defaults({
asText: false,
scope: path.dirname(file_path),
// _as is a hack so that we know its we are calling the function recursive
_as: ANTI_SANITIZER
}, options);
// super_json is our output, start by containing first file in array
var super_json = fromFile(file_list.shift(), json_config);
// TODO: Figure out when to sanitize super_json hopefully we don't have to
// santize all returns. I'm not sure about this, and I'm not sure if we
// can even sanitize super_json when we return it.
// Is super_json used by anyone recursively?
while (file_list.length) {
var json = fromFile(file_list.shift(), json_config);
merge(super_json, json);
}
// If this file is called from the outside eg json_merger.from...
// File('file_which_doesnt_extends_another_file.json')
// we need to sanitize the file and remove all indicators
if (options._as != ANTI_SANITIZER) {
super_json = util.sanitizeValue(super_json);
}
/*****************************************************************************
* Return super_json
*****************************************************************************/
if (options.asText) {
return stringify(super_json, options);
}
else {
return super_json;
}
};
/*******************************************************************************
* exports
*******************************************************************************/
module.exports = {
parseFile: parseFile,
stringify: stringify,
fromFile: fromFile,
fromObject: fromObject,
merge: merge
};