multivocal
Version:
A node.js library to assist with building best practice, configuration driven, Actions for the Google Assistant.
315 lines (280 loc) • 9.06 kB
JavaScript
const Template = require('./template');
var objPath = function( obj, path ){
var splitRegexp = new RegExp('[\\/\\[]');
var ret = undefined;
if( (typeof obj == 'object') && (typeof path == 'string' || Array.isArray(path)) ){
var pathElements = path;
if( typeof path == 'string' ){
pathElements = path.split( splitRegexp );
}
ret = obj;
for( var co=0; co<pathElements.length && ret; co++ ){
var name = pathElements[co];
var closebracket = name.indexOf(']');
if( closebracket > 0 ){
name = name.substring( 0, closebracket );
name = Number.parseInt( name, 10 );
// TODO - what if this isn't a number?
}
ret = ret[name];
}
}
return ret;
};
exports.objPath = objPath;
var IsFalsey = {
"Standard": val => !val,
"Undefined": val => typeof val === 'undefined',
"Empty": val => val === '',
"Zero": val => val === 0,
"False": val => val === false,
"Null": val => val === null,
"NaN": val => Number.isNaN( val ),
"Never": val => false
};
exports.IsFalsey = IsFalsey;
var objPathsDefault = function( obj, paths, def, nullTests ){
// Turn the nullTests into an array
if( typeof nullTests === 'undefined' ){
nullTests = [IsFalsey.Undefined];
} else if( typeof nullTests === 'string' ){
nullTests = nullTests.split(/[, |&]+/);
} else if( !Array.isArray(nullTests) ){
nullTests = [nullTests];
}
// Make sure the nullTests array is an array of functions
var tests = nullTests.map( v => typeof v === 'function' ? v : (IsFalsey[v] || IsFalsey.Never) );
var isNull = v => tests.map( f => f(v) ).reduce( (acc, v) => acc || v );
if( typeof obj !== 'object' || typeof paths === 'undefined' ){
return def;
}
if( !Array.isArray(paths) ){
paths = [paths];
}
var ret = undefined;
for( var co=0; co<paths.length && isNull(ret); co++ ){
var path = paths[co];
ret = objPath( obj, path );
}
if( isNull(ret) ){
ret = def;
}
return ret;
};
exports.objPathsDefault = objPathsDefault;
var setObjPath = function( obj, path, val ){
if( !Array.isArray(path) ){
path = path.split('/')
}
var root = path[0];
var childPath = path.slice(1);
var arrayIndexes = [];
var bracketIndex = root.indexOf('[');
if( bracketIndex > 0 ){
var bracketValue = root.substring( bracketIndex );
arrayIndexes = bracketValue.split(/[\[\]]+/);
if( arrayIndexes.length > 2 ){
arrayIndexes = arrayIndexes.splice( 1, arrayIndexes.length - 2 );
} else {
// There are no indices to reference
arrayIndexes = [];
}
root = root.substring( 0, bracketIndex );
}
var arraySize = undefined;
if( arrayIndexes.length ){
if( !Array.isArray(obj[root]) ){
obj[root] = [];
}
}
var childObj = obj[root];
for( var co=0; co<arrayIndexes.length; co++ ){
// Determine which index in the childObj we're referencing
var arrayIndex = arrayIndexes[co];
if( arrayIndex === '=' ){
arrayIndex = Math.max( childObj.length - 1, 0 );
} else if( arrayIndex === '+' ){
arrayIndex = childObj.length;
} else {
arrayIndex = Number.parseInt( arrayIndex, 10 );
}
// Make sure it exists
if( typeof childObj[arrayIndex] === 'undefined' ){
// If we have more indexes to reference, then make sure this is an array,
// Otherwise, it will either be an object for further referencing,
// or the value itself.
if( co === arrayIndexes.length - 1 ){
// This is the last index, so the next one should be an object
if( childPath.length > 0 ){
// We have more references to go
childObj[arrayIndex] = {};
} else {
// this is the end of the line, set the value
childObj[arrayIndex] = val;
}
} else {
childObj[arrayIndex] = [];
}
} else if( (co === arrayIndexes.length - 1) && (childPath.length == 0) ){
// This is the last item, but it already exists, so set the value
childObj[arrayIndex] = val;
}
// Make it the new childObj
childObj = childObj[arrayIndex];
}
if( childPath.length > 0 ){
if( typeof childObj === 'undefined' ){
obj[root] = {};
childObj = obj[root];
}
setObjPath( childObj, childPath, val );
} else if( arrayIndexes.length === 0 ){
obj[root] = val;
}
return obj;
};
exports.setObjPath = setObjPath;
var incObjPath = function( obj, path, inc ){
if( typeof inc === 'undefined' ){
inc = 1;
}
var val = objPathsDefault( obj, path, 0 );
val += inc;
return setObjPath( obj, path, val );
};
exports.incObjPath = incObjPath;
var setObjPathFrom = function( obj, path, valuePath ){
var val = objPath( obj, valuePath );
if( typeof val !== 'undefined' ){
setObjPath( obj, path, val );
}
return obj;
};
exports.setObjPathFrom = setObjPathFrom;
/**
* Get a value from the Config/Setting object in the environment or,
* if not set, the DefCon/Setting object (the default configuration).
* @param env
* @param settingPaths
* @param resultEval
* @returns {undefined}
*/
var setting = function( env, settingPaths, resultEval ){
if( typeof settingPaths === 'undefined' ){
return undefined;
} else if( !Array.isArray( settingPaths ) ){
settingPaths = [settingPaths];
}
if( !resultEval ){
resultEval = Template.Methods.Identity;
}
// Prefix the setting paths with the config and default config setting prefixes
var configPaths = settingPaths.map( p => `Config/Setting/${p}`);
var defconPaths = settingPaths.map( p => `DefCon/Setting/${p}`);
var paths = configPaths.concat( defconPaths );
// get the value set at these paths
var ret;
var result = objPathsDefault( env, paths );
if( typeof result !== 'undefined' ){
ret = Template.eval( result, env, resultEval );
}
return ret;
};
exports.setting = setting;
/**
* Get the Path value from pathOfSetting and get the value from evaluating those paths.
* If it does not exist, get the Default value from pathOfSetting and return it.
* @param env
* @param pathOfSetting
*/
var pathSetting = function( env, pathOfSetting ){
var settingPath = `${pathOfSetting}/Path`;
var settingDefault = `${pathOfSetting}/Default`;
var settingIsFalseyPath = `${pathOfSetting}/IsFalsey`;
var settingIsFalsey = setting( env, settingIsFalseyPath );
var paths = setting( env, settingPath, Template.Methods.Array );
var ret = objPathsDefault( env, paths, undefined, settingIsFalsey );
if( typeof ret === 'undefined' ){
ret = setting( env, settingDefault, Template.Methods.Typed );
}
return ret;
};
exports.pathSetting = pathSetting;
/**
* Evaluate a list of objects, returning the value of the first criteria that matches.
* @param env The environment
* @param criteriaMatchList A list of objects with attributes {Criteria, Value}
* @param def The default value if there is no match
* @return The first Value whose Criteria evaluates to true or the default value if nothing matches.
*/
var criteriaMatch = function( env, criteriaMatchList, def ){
var criteriaObj = {
CriteriaMatch: criteriaMatchList,
Default: def
};
return Template.evalObjCriteria( criteriaObj, env );
}
exports.criteriaMatch = criteriaMatch;
var criteriaMatchSetting = function( env, pathOfCriteriaMatch ){
var criteriaObj = setting( env, pathOfCriteriaMatch );
return Template.evalObjCriteria( criteriaObj, env );
}
exports.criteriaMatchSetting = criteriaMatchSetting;
var random = function( size ){
var ret = Math.floor( Math.random() * size );
return ret;
};
exports.random = random;
/**
* Return a shallow copy of the array with the contents shuffled
* @param arr
*/
var shuffled = function( arr ){
if( !Array.isArray(arr) ){
return [arr];
}
var ret = arr.slice();
var size = ret.length;
for( var co=0; co<size; co++ ){
var target = random(size);
var tmp = ret[co];
ret[co] = ret[target];
ret[target] = tmp;
}
return ret;
};
exports.shuffled = shuffled;
/**
* Return if this attribute name in a Voice object should be treated as an SSML tag.
* In general, most attributes that start with an upper case character should be
* treated as SSML tags, but some (such as Name) are reserved for other uses.
* @param s Attribute name to test for
* @return {boolean}
*/
var isVoiceTag = function( s ){
// Some strings won't be tags
if( s === 'Name' ){
return false;
}
// Other tags should start with upper case
var c = s.charAt(0);
return c === c.toUpperCase();
}
exports.isVoiceTag = isVoiceTag;
/**
* Given a Voice object, loop over its attributes. For those that are considered
* attributes that represent an SSML tag,
* @param voice
* @param func {function( string, object )}
*/
var eachVoiceTag = function( voice, func ){
if( voice ){
var keys = Object.keys( voice );
keys.forEach( key => {
if( isVoiceTag( key ) ){
func( key.toLowerCase(), voice[key] );
}
})
}
}
exports.eachVoiceTag = eachVoiceTag;