jsonjs
Version:
Jsonj inspired library for procesing JSON
856 lines (742 loc) • 18.3 kB
JavaScript
"use strict";
/**
* TODO:
* - keep map of nested key paths to prevent looping all the time
* - add more features from original jsonj
*/
/**
* Clone an object
* @param {object} obj
* @returns {object}
*/
function clone(obj) {
var cloned = {};
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
cloned[i] = obj[i];
}
}
return cloned;
}
/**
* Deep clone an object
* @param {object} obj
* @returns {object}
*/
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
var cloned = obj.constructor();
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
/**
* Deep merge one object to another
* @param one
* @param another
* @returns {*}
*/
function deepMerge(one, another) {
if (another == null || typeof another !== 'object') {
return another;
}
if(one == null && typeof another === 'object') {
if(Array.isArray(another)) {
one = [];
} else {
one = {};
}
}
var cloned = deepClone(another);
for (var key in cloned) {
if (cloned.hasOwnProperty(key)) {
one[key] = deepMerge(one[key], another[key]);
}
}
return one;
}
function extend(){
var args = Array.prototype.slice.call(arguments);
var original = args.shift();
args.forEach(function(obj){
deepMerge(original, obj);
});
return original;
}
function keysFromArguments() {
var keys = [];
if(arguments[0] && isType(arguments[0], 'array')) {
keys = arguments[0];
} else {
keys = Array.prototype.slice.call(arguments);
}
return keys;
}
function isTypeStrict(primitive, type, name) {
var rval = isType(primitive, type);
if(!rval) {
throw new Error('jsonjs validation error: ' + (name ? name : 'primitive') + ' is not a(n)' + type);
}
}
function isType(primitive, type) {
var rval = false;
switch(type) {
case 'primitive':
return isType(primitive, 'string') || isType(primitive, 'number') || isType(primitive, 'boolean');
case 'array':
if(typeof primitive == "object" && Array.isArray(primitive)) {
rval = true;
}
break;
case 'object':
if(typeof primitive == "object" && !Array.isArray(primitive)) {
rval = true;
}
break;
case 'string':
if(typeof primitive == "string") {
rval = true;
}
break;
case 'number':
if(typeof primitive == "number") {
rval = true;
} else if(typeof primitive === "string"){
if (primitive === Number(primitive).toString()) {
rval = true;
} else if(!isNaN(parseInt(primitive)) && Number(primitive) === parseInt(primitive)) {
rval = true;
}
}
break;
case 'int':
if(isType(primitive, 'number') && Number(primitive) % 1 === 0) {
rval = true;
}
break;
case 'float':
if(isType(primitive, 'number')) {
if(isType(primitive, 'int')) {
rval = true;
} else if(Number(primitive) % 1 !== 0) {
rval = true;
}
}
break;
case 'boolean':
if(typeof primitive == "boolean") {
rval = true;
}
break;
}
return rval;
}
function isDecoratedObject(object) {
return !!(object instanceof JSONObject);
}
function isDecoratedArray(array) {
return !!(array instanceof JSONArray);
}
function isDecorated(object) {
return isDecoratedArray(object) || isDecoratedObject(object);
}
function _flatten(element, delimiter, pathPrefix) {
delimiter || (delimiter = '.');
pathPrefix || (pathPrefix = '');
var paths = [];
if(isType(element, 'object')) {
if(!isDecoratedObject(element)) {
element = new JSONObject(element);
}
var keys = element.keys();
var keysLength = keys.length;
for(var i = 0; i < keysLength; i++) {
var key = keys[i];
var val = element.get(key);
if(isType(val, 'primitive')) {
paths.push([pathPrefix + key, val]);
} else if(isType(val, 'array') || isType(val, 'object')) {
paths = paths.concat(_flatten(val, delimiter, pathPrefix + key + delimiter));
} else {
throw new Error('Unknown type for key:' + pathPrefix + key);
}
}
} else if(isType(element, 'array')) {
if(isDecoratedArray(element)) {
element = element.data;
}
var len = element.length;
for(var i = 0; i < len; i++) {
var val = element[i];
if(isType(val, 'primitive')) {
paths.push([pathPrefix + i, val]);
} else if(isType(val, 'array') || isType(val, 'object')) {
paths = paths.concat(_flatten(val, delimiter, pathPrefix + i + delimiter));
} else {
throw new Error('Unknown type for key:' + pathPrefix + i);
}
}
} else {
throw new Error('Unknown type. Expecting an array or an object');
}
return paths;
}
function flatten(element, delimiter) {
var flatMap = {};
var items = _flatten(element, delimiter);
var len = items.length;
for(var i = 0; i < len; i++) {
var k = items[i][0];
var v = items[i][1];
flatMap[k] = v;
}
return flatMap;
}
function unflatten(element, delimiter) {
delimiter || (delimiter = '.');
var keys = Object.keys(element);
var len = keys.length;
var obj = new JSONObject();
for(var i = 0; i < len; i++) {
var key = keys[i];
var keyArray = key.split(delimiter);
//if(isType(keyArray[keyArray.length - 1], 'number')) {
// var idx = keyArray.pop();
// var arr = obj.getOrCreateArray(keyArray)[idx] = element[key];
//} else {
obj.put(keyArray, element[key]);
//}
}
return obj;
}
/**
*
* @param {object} [data]
* @param {boolean} [clone=false] - The original object will be cloned if true.
* @returns {JSONObject}
* @constructor
*/
function JSONObject(data, clone){
if(data instanceof JSONObject) {
return data;
}
if(typeof data === "object" && clone) {
data = deepClone(data);
}
this.data = data || {};
return this;
}
/**
* Check if key has a value
* @param {...String|String[]} key
* @returns {boolean}
*/
JSONObject.prototype.has = function() {
var keys = keysFromArguments.apply(this, arguments), current = this.data;
if(keys.length < 1) {
return true;
}
var value = this.get(keys);
if(value !== undefined) {
return true;
}
return false;
};
/**
* Check if value with key is a given type
* @param {...String|String[]} key
* @param {String} type
* @returns {boolean}
*/
JSONObject.prototype.is = function() {
var keys = keysFromArguments.apply(this, arguments);
var type = keys.pop();
var value = this.get(keys);
return isType(value, type);
};
/**
* Get a value from decorated object
* @param {...*|*[]} key
* @returns {*|{}}
*/
JSONObject.prototype.get = function() {
var keys = keysFromArguments.apply(this, arguments), current = this.data;
if(keys.length < 1) {
return this.object();
}
var i;
for (i = 0; i < keys.length; i++){
current = current[keys[i]];
if(current === undefined) return;
}
return current;
};
/**
* @alias get
*/
JSONObject.prototype.dget = function(){
return JSONObject.prototype.get.apply(this, arguments);
};
/**
* Get a object from decorated object
* @param {...*|*[]} key
* @returns {Object||{}}
*/
JSONObject.prototype.getObject = function(){
var value = this.get.apply(this, arguments);
isTypeStrict(value, 'object', 'value');
return value;
};
/**
* Get a array from decorated object
* @param {...*|*[]} key
* @returns {Array}
*/
JSONObject.prototype.getArray = function(){
var value = this.get.apply(this, arguments);
isTypeStrict(value, 'array', 'value');
return value;
};
/**
* Get a string from decorated object
* @param {...*|*[]} key
* @returns {String}
*/
JSONObject.prototype.getString = function(){
var value = this.get.apply(this, arguments);
isTypeStrict(value, 'string', 'value');
return value;
};
/**
* Get a boolean from decorated object
* @param {...*|*[]} key
* @returns {Boolean}
*/
JSONObject.prototype.getBoolean = function(){
var value = this.get.apply(this, arguments);
isTypeStrict(value, 'boolean', 'value');
return value;
};
/**
* Get a integer from decorated object
* @param {...*|*[]} key
* @returns {Number}
*/
JSONObject.prototype.getInt = function(){
var value = this.get.apply(this, arguments);
isTypeStrict(value, 'number', 'value');
return parseInt(value);
};
/**
* Get a float from decorated object
* @param {...*|*[]} key
* @returns {Number}
*/
JSONObject.prototype.getFloat = function(){
var value = this.get.apply(this, arguments);
isTypeStrict(value, 'number', 'value');
return parseFloat(value);
};
/**
* Get a decorated JSONObject from decorated object
* @param {...*|*[]} key
* @returns {JSONObject}
*/
JSONObject.prototype.getDecoratedObject = function(){
var value = this.getObject.apply(this, arguments);
return new JSONObject(value);
};
/**
* Get a decorated JSONArray from decorated object
* @param {...*|*[]} key
* @returns {JSONArray}
*/
JSONObject.prototype.getDecoratedArray = function(){
var value = this.getArray.apply(this, arguments);
return new JSONArray(value);
};
/**
* Update a value inside the decorated object
* @param {...*|*[]} key
* @param value
* @returns {JSONObject}
*/
JSONObject.prototype.put = function(key, value) {
var keys, current = this.data;
if(arguments.length < 2) {
throw new Error("put needs at least 2 arguments");
}
var args = Array.prototype.slice.call(arguments);
value = args.pop();
keys = keysFromArguments.apply(this, args);
var i, k;
for (i = 0; i < keys.length; i++){
k = keys[i];
if(isType(keys[i+1], 'number')) {
if(current[k] === undefined) {
current[k] = [];
}
} else {
if(current[k] === undefined) {
current[k] = {};
}
}
if((keys.length - 1) == i) {
current[k] = value;
}
current = current[k];
}
return this;
};
/**
* @alias put
*/
JSONObject.prototype.dput = function(){
return JSONObject.prototype.put.apply(this, arguments);
};
JSONObject.prototype.delete = function(){
var keys, current = this.data;
if(arguments.length < 1) {
throw new Error("delete needs at least 1 arguments");
}
keys = keysFromArguments.apply(this, arguments);
var i, last;
for (i = 0; i < keys.length; i++){
last = current;
current = current[keys[i]];
if(current === undefined) return;
}
delete last[keys[i-1]];
return current;
};
JSONObject.prototype.del = function(){
return JSONObject.prototype.delete.apply(this, arguments);
};
/**
* Return a reference to internal object
* @returns {object}
*/
JSONObject.prototype.object = function(){
return this.data;
};
/**
* Return a shallow copy of internal object
* @returns {*}
*/
JSONObject.prototype.shallowClone = function(){
return clone(this.object());
};
/**
* Deep clone an internal object
* @returns {object}
*/
JSONObject.prototype.clone = function(){
return this.deepClone();
};
/**
* Deep clone an internal object
* @returns {object}
*/
JSONObject.prototype.deepClone = function(){
return deepClone(this.object());
};
/**
* Return a copy of JSONObject instance
* @returns {JSONObject}
*/
JSONObject.prototype.decoratedClone = function(){
var data = this.deepClone();
return new JSONObject(data);
};
/**
* @alias decoratedClone
*/
JSONObject.prototype.copy = function(){
return this.decoratedClone.call(this);
};
/**
* Get an object with given keys or initialize and return empty object
* @param {(...string|string[])} key
* @returns {object|{}}
* @throws {TypeError} if value is not an object
*/
JSONObject.prototype.getOrCreateObject = function(){
var keys = keysFromArguments.apply(this, arguments);
var value = this.get(keys);
if(!value) {
this.put(keys, {});
return this.get(keys);
} else {
if(Array.isArray(value)) {
throw new TypeError("value found with " + keys.join(', ') + " was an array!");
} else if(typeof value === "object") {
return value;
} else {
throw new TypeError("value found with " + keys.join(', ') + " was not an object!");
}
}
};
/**
* Get an decorated JSONObject with given keys or initialize and return empty JSONObject
* @param {(...string|string[])} key
* @returns {JSONObject|{}}
* @throws {TypeError} if value is not an object
*/
JSONObject.prototype.getOrCreateDecoratedObject = function(){
var obj = this.getOrCreateObject.apply(this, arguments);
return new JSONObject(obj);
};
/**
* Get an array with given keys or initialize and return empty array
* @returns {Array|[]}
* @throws {TypeError} if value is not an array
*/
JSONObject.prototype.getOrCreateArray = function(){
var keys = keysFromArguments.apply(this, arguments);
var value = this.get(keys);
if(!value) {
this.put(keys, []);
return this.get(keys);
} else {
if(Array.isArray(value)) {
return value;
} else {
throw new TypeError("value found with " + keys.join(', ') + " was not an array!");
}
}
};
/**
* Get an decorated JSONArray with given keys or create new
* @returns {JSONArray}
* @throws {TypeError} if value is not an array
*/
JSONObject.prototype.getOrCreateDecoratedArray = function(){
var arr = this.getOrCreateArray.apply(this, arguments);
return new JSONArray(arr);
};
/**
* Return list of keys available in decorated JSONObject
* @returns {Array}
*/
JSONObject.prototype.keys = function(){
return Object.keys(this.data);
};
/**
* Return decorate JSONArray
* @param arr
* @constructor
*/
function JSONArray(arr) {
this.arr = arr || [];
}
/**
* Return return original array
* @returns {Array|[]}
*/
JSONArray.prototype.array = function(){
return this.arr;
};
/**
* Return original array or item in given index
* @param [idx]
* @returns {*}
*/
JSONArray.prototype.get = function(idx){
var arr = this.arr;
if(idx !== undefined) {
return arr[idx];
}
return this.arr;
};
/**
* Return object from given index
* @param idx
* @returns {JSONObject}
*/
JSONArray.prototype.getObject = function(idx){
var raw = this.get(idx);
isTypeStrict(raw, 'object');
return raw;
};
/**
* Return decorated JSONObject from given index
* @param idx
* @returns {JSONObject}
*/
JSONArray.prototype.getDecoratedObject = function(idx) {
return new JSONObject(this.getObject(idx));
};
/**
* Return array containing decorated JSONObjects
* @returns {Array}
*/
JSONArray.prototype.objects = function(){
return this.arr.map(function(item){
return new JSONObject(item);
});
};
/**
* Push item to JSONArray. Returns index of the item.
* @param item
* @returns {int}
*/
JSONArray.prototype.push = function(item){
return this.arr.push(item);
};
JSONArray.prototype.getArray = function(idx){
var item = this.get(idx);
isTypeStrict(item, 'array');
return item;
};
JSONArray.prototype.getDecoratedArray = function(idx){
return new JSONArray(this.getArray(idx));
};
/**
* Return size of the array.
* @returns {int}
*/
JSONArray.prototype.size = function(){
return this.arr.length;
};
module.exports = {
/**
* Clone given object and decorate it
* @param {object} object
* @returns {JSONObject}
* @static
*/
decoratedCopy: function(object){
return new JSONObject(object, true);
},
/**
* Decorate object
* @param {object|JSONObject|JSONArray} [object]
* @returns {JSONObject|JSONArray}
* @static
*/
decorate: function(object){
if(isDecorated(object)) {
return object;
}
if(Array.isArray(object)) {
return new JSONArray(object);
}
return new JSONObject(object);
},
/**
* Return empty decorated object
* @returns {JSONObject}
* @static
*/
object: function(){
return new JSONObject();
},
/**
* Return empty decorated array
* @returns {JSONArray}
* @static
*/
array: function(){
return new JSONArray();
},
/**
* Check primitive type
* @param primitive
* @param {string} type -- array|object|string|number|int|float
* @returns {boolean}
* @static
*/
is: isType,
/**
* Check primitive type or throw
* @param primitive
* @param {string} type -- array|object|string|number|int|float
* @param {string} [name] primitive name
* @throws {Error}
* @static
*/
isStrictly: isTypeStrict,
JSONObject: JSONObject,
JSONArray: JSONArray,
isDecoratedObject: isDecoratedObject,
isDecoratedArray: isDecoratedArray,
isDecorated: isDecorated,
/**
*
*/
utils: {
/**
* Clone an object
* @param {object} obj
* @type {Function}
* @returns {object}
* @static
*/
clone: clone,
/**
* Deep clone an object
* @param {object} obj
* @type {Function}
* @returns {object}
* @static
*/
deepClone: deepClone,
/**
* Deep merge one object to another
* @param one
* @param another
* @type {Function}
* @returns {object}
* @static
*/
deepMerge: deepMerge,
/**
* Deep merge multiple objects. First being the base.
* @param {object} ...obj
* @type {Function}
* @returns {object}
* @static
*/
extend: extend,
/**
* Check primitive type
* @param primitive
* @param {string} type -- array|object|string|number|int|float
* @type {Function}
* @returns {boolean}
* @static
*/
isType: isType,
/**
* Check primitive type or throw
* @param primitive
* @param {string} type -- array|object|string|number|int|float
* @param {string} [name] primitive name
* @type {Function}
* @throws {Error}
* @static
*/
isTypeStrict: isTypeStrict,
/**
* Flatten object or array to key value pairs
* @param element
* @param {string} [delimiter] -- Delimiter for nested keys. Defaults to '.'
* @static
* @returns {object}
*/
flatten: flatten,
/**
* Create nested object from key paths and values
* @param {object} paths
* @param {string} [delimiter] -- Key delimiter on paths. Defaults to '.'
* @static
* @returns {JSONObject}
*/
unflatten: unflatten
}
};