lib/bolgia.js
/*
* Bolgia, an helper module for the config hell.
* It recursively clones, mixes, updates and improves configuration
* objects/hashes with nested properties.
* '..non ragioniam di lor, ma guarda e passa..' ",
*
* Copyright(c) 2013-present Guglielmo Ferri <44gatti@gmail.com>
* MIT Licensed
*/
exports.Bolgia = ( function () {
var isArray = Array.isArray
, slice = Array.prototype.slice
, otype = Object.prototype.toString
, abs = Math.abs
// toString table
, o = {
arg : '[object Arguments]'
, arr : '[object Array]'
, boo : '[object Boolean]'
, dat : '[object Date]'
, err : '[object Error]'
, fun : '[object Function]'
, nul : '[object Null]'
, num : '[object Number]'
, obj : '[object Object]'
, reg : '[object RegExp]'
, str : '[object String]'
, und : '[object Undefined]'
// ECMASCRIPT 2017, toString.call() returns Uint8Array for Buffers
, ui8 : '[object Uint8Array]'
// custom values
, buf : '[object Buffer]'
, nan : '[object NaN]'
, nst : '[object NullString]'
}
, isBuffer = Buffer.isBuffer
// toString that nullifies NaN and ''
, toString = function ( el, custom ) {
var t = otype.call( el )
, c = custom
;
switch ( t ) {
case o.num:
t = isNaN( el ) ? ( c ? o.nan : o.nul ) : t;
break;
case o.str:
t = ( el === '' ) ? ( c ? o.nst : o.nul ) : t;
break;
case o.ui8:
case o.obj:
t = isBuffer( el ) ? o.buf : t;
break;
}
return t;
}
// recursively convert an obj/hash to an array.
, toArray = function ( obj, recur, arr ) {
var list = arr || []
, p = null
, v = null
, vtype = null
, nest = null
;
for ( p in obj ) {
v = obj[ p ];
if ( recur ) {
vtype = otype.call( v );
if ( ( vtype === o.obj ) ||
( vtype === o.arr ) ) {
nest = [];
list.push( p, nest );
toArray( v, recur, nest );
continue;
}
}
list.push( p, v );
}
return list;
}
// recursively convert an obj/hash to an array, listing only enumerable properties
, toArrayEnum = function ( obj, recur, arr ) {
var list = arr || []
, p = null
, v = null
, vtype = null
, nest = null
;
for ( p in obj ) {
if ( ! obj.propertyIsEnumerable( p ) ) continue;
v = obj[ p ];
if ( recur ) {
vtype = otype.call( v );
if ( ( vtype === o.obj ) ||
( vtype === o.arr ) ) {
nest = [];
list.push( p, nest );
toArray( v, recur, nest );
continue;
}
}
list.push( p, v );
}
return list;
}
/*
* Recursively convert an indexed array to an hash/obj;
* optionally it converts Buffers to Strings and Strings
* representing numbers to Numbers.
*
* NOTE: pay attention to possible keys collisions/overwritings.
*/
, toHash = function ( arr, recur, obj, convert ) {
var h = obj || {}
, c = convert
, a = isArray( arr ) ? arr : []
, alen = a.length
, i = 0
, k = null
, v = null
;
for ( ; i < alen; ) {
k = String( a[ i++ ] );
v = a[ i++ ];
if ( recur && ( otype.call( v ) === o.arr ) ) {
h[ k ] = {};
toHash( v, recur, h[ k ], c );
continue;
}
if ( ! c ) {
h[ k ] = v;
continue;
}
// conversion is enabled
switch ( toString( v ) ) {
case o.buf:
//convert node Buffers to Strings
if ( v.length === 0 ) {
h[ k ] = '';
break;
}
case o.str:
// convert Strings representing numbers to Numbers.
h[ k ] = isNaN( + v ) ? String( v ) : + v;
break;
default:
h[ k ] = v;
break;
}
}
return h;
}
/*
* recursively convert nested Array of Buffer items to Strings and Numbers,
* note that max integer precision is 2^53 or 9007199254740992, if number
* is greater than this value a string will be returned.
*/
, reveal = function ( arr ) {
// var a = isArray( arr ) ? arr : ( arr === undefined || arr === null ) ? [] : [ arr ]
var a = isArray( arr ) ? arr : [ arr ]
, alen = a.length
, el = a[ 0 ]
, i = 0
;
for ( ; i < alen; el = a[ ++i ] ) isArray( el ) ?
reveal( el ) :
a[ i ] = el === null || el === undefined ?
el :
( isNaN( + el ) || ( abs( + el ) === abs( + el ) + 1 ) ) ? String( el ) : + el
;
return a;
}
/*
* Recursively count obj properties,
* or only non-object properties (leafs).
*/
, count = function ( obj, leaf ) {
var p = null
, l = leaf
, cnt = 0
;
for ( p in obj ) {
switch ( otype.call( obj[ p ] ) ) {
case o.arr:
case o.obj:
cnt += ( l ? 0 : 1 ) + count( obj[ p ] );
break;
default:
++cnt;
break;
}
}
return cnt;
}
/*
* simple object/hash cloning.
*/
, clone = function ( src ) {
// update an empty hash ;)
return update( {}, src );
}
/*
* mix/update dest object with all properties
* from src, with no recursion.
* It brutally overwrites dest properties/values.
*/
, mix = function ( dest, src ) {
// src is not an object
if ( ! src ) return dest;
var keys = Object.keys( src )
, i = keys.length
, k = null
;
while ( i-- ) {
k = keys[ i ];
dest[ k ] = src[ k ];
}
return dest;
}
/*
* Improve a dest object with all properties/values
* from a src object, with recursion.
* It doesn't overwrite properties/values that already
* exist in the dest object.
*/
, improve = function ( dest, src ) {
var d = dest
, s = src
, ctor = s && s.constructor || {}
, stype = otype.call( s )
, dtype = otype.call( d )
, l = 0
, p = null
;
switch ( stype ) {
case o.nul:
case o.und:
break;
case o.arg:
// build array from arguments
s = slice.call( s );
stype = o.arr;
case o.arr:
l = s.length;
if ( dtype === o.nul ) break;
if ( dtype === o.und ) d = [];
while ( l ) d[ --l ] = improve( d[ l ], s[ l ] );
break;
case o.obj:
if ( ctor === Object ) {
if ( dtype === o.nul ) break;
if ( dtype === o.und ) d = {};
for ( p in s ) d[ p ] = improve( d[ p ], s[ p ] );
}
break;
case o.dat:
if ( dtype === o.und ) d = new Date( s.getTime() );
break;
}
return ( dtype === o.und ) ? s : d;
}
/*
* Update a dest object with all properties/values
* from a src object, with recursion.
* It overwrites properties/values that already
* exist in the dest object.
*/
, update = function ( dest, src ) {
var d = dest
, s = src
, sector = s && s.constructor || {}
, doctor = d && d.constructor
, stype = otype.call( s )
, dtype = otype.call( d )
, l = 0
, p = null
;
switch ( stype ) {
case o.und:
break;
case o.arg:
// build array from arguments
s = slice.call( s );
stype = o.arr;
case o.arr:
l = s.length;
// dest array doesn't exists, init
if ( ! isArray( d ) ) d = [];
while ( l ) d[ --l ] = update( d[ l ], s[ l ] );
break;
case o.obj:
if ( sector === Object ) {
// dest hash doesn't exists, init
if ( ( doctor !== Object ) || ( dtype !== o.obj ) ) d = {};
for ( p in s ) d[ p ] = update( d[ p ], s[ p ] );
}
break;
case o.dat:
d = new Date( s.getTime() );
break;
default :
/*
* o.nul
* o.num
* o.str
* o.boo
* o.fun
* o.reg
*/
return s;
}
return d;
}
/* querystrings and queryobjects methods */
// generic error message for field
, fmsg = 'a root field name should be specified!'
// encode field and value to a querystring
, build = function ( f , k ) {
return ( f + '[' + encodeURIComponent( k ) + ']' );
}
// filter field types
, filter = function ( f, ftype, o ) {
switch ( ftype ) {
case o.nul:
case o.und:
case o.fun:
break;
default:
return true;
}
}
/*
* method to output a queryobject representation of the object/hash
* It requires 2 arguments, a root field name and a value object to parse.
*/
, flatten = function ( field, value, hash ) {
var f = field
, h = hash || {}
, v = value
, vtype = otype.call( v )
, ftype = otype.call( f )
, k = 0
, len = 0
;
switch ( ftype ) {
case o.und:
case o.nul:
throw new TypeError( 'Bolgia#flatten: ' + fmsg );
}
switch ( vtype ) {
case o.arg:
// build array from arguments
v = slice.call( v );
vtype = o.arr;
case o.arr:
for ( len = v.length; k < len; ++k ) flatten( build( f, k ), v[ k ], h );
break;
case o.obj:
if ( v.constructor === Object ) for ( k in v ) flatten( build( f, k ), v[ k ], h );
break;
default:
// leaf
h[ f ] = v;
break;
}
return h;
}
/*
* Method to output a (query)string representation of an object/hash.
* It requires 2 arguments, a root field name and a value object to parse;
* optionally a configuration object.
*/
, qs = function ( field, value, opt, hash ) {
var cfg = opt
, f = field
, h = hash || ''
, v = value
, vtype = otype.call( v )
, ftype = otype.call( f )
, k = 0
;
switch ( ftype ) {
case o.und:
case o.nul:
throw new TypeError( 'Bolgia#qs: ' + fmsg );
}
switch ( vtype ) {
case o.arg:
// build array from arguments
v = slice.call( v );
vtype = o.arr;
case o.arr:
for ( k in v ) h = qs( build( f, k ), v[ k ], cfg, h );
break;
case o.obj:
if ( v.constructor === Object ) for ( k in v ) h = qs( build( f, k ), v[ k ], cfg, h );
break;
default:
// leaf
if ( cfg.filter( v, vtype, o ) ) h += f + cfg.eq + encodeURIComponent( v ) + cfg.dl;
else h += f + cfg.eq + cfg.dl;
break;
}
return h;
}
;
return {
circles : o
, clone : clone
, improve : improve
, mix : mix
, update : update
// less ambiguous with Object.toString
, doString : toString
, toString : toString
, toArray : toArray
, toHash : toHash
, reveal : reveal
, count : count
, flatten : function ( hash ) {
var h = hash
, k = null
, f = {}
, htype = otype.call( h )
;
if ( ( htype === o.obj ) && ( h.constructor === Object ) )
for ( k in h ) update( f, flatten( k, h[ k ] ) );
return f;
}
, qs : function ( hash, opt ) {
var fopt = {
dl : '&'
, eq : '='
, filter : filter
}
, cfg = update( fopt, opt || {} )
, h = hash
, k = null
, s = ''
, htype = otype.call( h )
;
// faster than array push & join
if ( ( htype === o.obj ) && ( h.constructor === Object ) )
for ( k in h ) s += qs( encodeURIComponent( k ), h[ k ], cfg );
// remove trailing '&'
return s.slice( 0, -1 );
}
};
} )();