/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/*global requirejs */
/**
* Defines BajaScript utility functions {@link baja.strictArg},
* {@link baja.strictAllArgs}, {@link baja.def}, {@link baja.objectify},
* and {@link baja.iterate}.
*
* @module baja/sys/bajaUtils
*/
define([
'bajaPromises',
'bajaScript/baja/sys/structures/Cursor' ], function (
bajaPromises,
Cursor) {
"use strict";
const unescapeXmlMap = { apos: "'", quot: '"', lt: '<', gt: '>', amp: '&' };
const getBaja = (function () {
let baja;
return function () {
if (!baja) { baja = require('baja!'); }
return baja;
};
}());
let validateArgs = true;
/**
* Strict Argument Check.
*
* Checks a given argument to ensure it's not undefined.
*
* @private
* @function baja.strictArg
*
* @param arg the argument being tested.
* @param {Function|String|Type} [ctor] optional Constructor function used to test against arg.
* Can also be a String TypeSpec or a Type Object.
* @param {String} [errMsg] optional message to specify if argument check fails.
* @returns arg
* @throws {Error} if the argument check fails.
*
* @example
* <caption>
* If a constructor function is specified then it matches the
* argument's constructor against it.
* </caption>
*
* baja.strictArg(arg1, String);
*
* @example
* <caption>
* The second argument can also be a String TypeSpec or a Type Object.
* This will ensure the argument has the correct TypeSpec.
* </caption>
*
* baja.strictArg(arg1, "control:NumericWritable");
*
* //...or...
*
* baja.strictArg(arg1, baja.lt("control:NumericWritable"));
*/
function strictArg(arg, ctor, errMsg) {
if (!validateArgs) { return arg; }
var argType,
argCtor,
ctorType = typeof ctor,
ctorName,
isType = true,
isValid = false,
err = "";
// Null arguments are acceptable (just like Java)
// Bail any type checking if we don't have a valid Constructor
if (arg !== undefined &&
(arg === null || ctor === undefined || ctor === null)) {
return arg;
}
if (ctorType === "function") {
if (arg !== undefined) {
argCtor = arg.constructor;
if (argCtor === ctor || arg instanceof ctor) {
isValid = true;
}
argType = argCtor.name;
if (!argType && argCtor.$name) {
argType = argCtor.$name;
}
}
ctorName = ctor.name;
if (!ctorName && ctor.$name) {
ctorName = ctor.$name;
}
}
// If ctor is a String TypeSpec or Type Object
if (ctorType === "string" || ctorType === "object") {
if (arg !== undefined) {
// TODO: remove this require() as part of completing modularization
// of bajaScript.
var baja = getBaja();
if (!baja.hasType(arg)) {
isType = false;
}
argType = arg.getType();
ctor = baja.lt(ctor);
if (argType.is(ctor)) {
isValid = true;
}
}
ctorName = String(ctor);
}
if (arg === undefined) {
err = "Invalid argument (undefined)";
} else {
if (!isType) {
err = "Invalid argument type (unrecognized)";
} else if (!isValid) {
err = "Invalid argument type (" + argType + ")";
}
}
if (err) {
if (ctorName) {
err += ", expected " + ctorName;
}
// TODO: Missing tests for use of errMsg
throw new Error(errMsg || err);
}
return arg;
}
/**
* Strict Arguments Check.
*
* Checks all of the given arguments to ensure they're not undefined.
*
* @private
* @function baja.strictAllArgs
*
* @see baja.strictArg
*
* @param {Array} args an array of arguments being tested.
* @param {Array} ctors an array of Constructors being used to test against the arguments.
*
* @example
* <caption>
* An array of constructors is passed in to ensure they match the
* arguments' constructors. The constructors array can also hold a Type
* used for comparison (in the form of either a String or Type).
* </caption>
*
* baja.strictAllArgs([arg1, arg2], [String, Number]);
* baja.strictAllArgs([arg3, arg4], ['baja:Double', baja.lt('baja:Component')]);
*/
function strictAllArgs(args, ctors) {
if (!validateArgs) { return; }
if (ctors.length !== args.length) {
throw new Error("Invalid number of arguments. Expected " + ctors.length +
", received " + args.length + " instead.");
}
var i;
for (i = 0; i < args.length; i++) {
strictArg(args[i], ctors[i]);
}
}
/**
* Ensures the arg is a `baja:Number`, then returns the Number value
* in either case.
*
* @param {Number|baja.Simple} arg (any `baja:Number` type)
* @param {String} [errMsg] optional Error message to return if any argument
* check fails.
*
* @returns {Number}
*/
function strictNumber(arg, errMsg) {
var baja = getBaja();
strictArg(arg, 'baja:Number', errMsg);
if (baja.hasType(arg, 'baja:Number')) {
arg = arg.valueOf();
}
return arg;
}
/**
* Define a default value for possibly undefined variables.
*
* @private
* @function baja.def
*
* @param val the value to be tested.
* @param defVal the default value to be returned if the value is undefined.
* @returns the default value if the value is undefined.
*/
function def(val, defVal) {
return val === undefined ? defVal : val;
}
/**
* This is a convenience method used for working with functions that take
* an object literal as an argument.
*
* This method always ensures an Object is returned so its properties can be
* further validated.
*
* In some cases, the function may take an object literal or a single
* argument. If the function can take a single argument that isn't an
* object literal, then the `propName` can be specified. If this is
* specified, an Object is created and the value is assigned to the
* Object with the specified property name.
*
* @function baja.objectify
* @private
*
* @param obj the Object literal or a value to be added onto an Object if propName is specified.
* @param {String} [propName] if the object isn't an Object, an Object is created and the value is assigned
* to the object with this property name.
*
* @returns {Object} an Object
*
* @example
* // This function can take an Object literal or a Number
* function foo(obj) {
* obj = baja.objectify(obj, "num");
*
* // 'obj' will always point to an Object. We can now test and work with 'num'...
* baja.strictArg(obj.num, Number);
* }
*
* // Both method invocations are valid...
* foo(23.4);
* foo({num: 23.4});
*/
function objectify(obj, propName) {
if (!(obj === undefined || obj === null)) {
if (obj.constructor === Object) {
return obj;
}
if (typeof propName === "string") {
var o = {};
o[propName] = obj;
return o;
}
}
return {};
}
function iterateArray(arr, start, end, func) {
var i, result;
for (i = start; i < end; i++) {
result = func(arr[i], i);
if (result !== undefined) {
return result;
}
}
}
function iterateCustomNext(obj, func, nextFunc) {
while (obj) {
var result = func(obj);
if (result !== undefined) {
return result;
}
obj = nextFunc(obj);
}
}
function iterateJsProperties(obj, func) {
var name, result;
for (name in obj) {
if (obj.hasOwnProperty(name)) {
result = func(obj[name], name);
if (result !== undefined) {
return result;
}
}
}
}
function iterateByIndex(start, end, func) {
var i, result;
for (i = start; i < end; i++) {
result = func(i);
if (result !== undefined) {
return result;
}
}
}
/**
* A iteration general utility method that performs the given function on every JavaScript
* property in the given cursor or Javascript array. This function can be called
* with a variety of parameter configurations.
*
* `iterate()` is compatible with arrays, but *not* with
* `arguments` objects - pass in
* `Array.prototype.slice.call(arguments)` instead.
*
* In the last case with the `doIterate` and
* `getNext` functions, `doIterate` performs the
* iterative action on the object, and `getNext` returns the
* next object to iterate (this will be passed directly back into
* `doIterate`). This is handy for walking up prototype chains,
* supertype chains, component hierarchies, etc.
*
* In all cases, if the function being executed ever returns a value
* other than `undefined`, iteration will be stopped at that
* point and `iterate()` will return that value.
*
* For invocations of `iterate()` that include start or end
* indexes, note that start indexes are inclusive and end indexes are
* exclusive (e.g. `iterate(2, 5, function (i) { baja.outln(i); })`
* would print `2,3,4`).
*
* @function baja.iterate
* @returns any non-undefined value that's returned from any function
* or Cursor.
*
* @example
* baja.iterate(array, function (arrayElement, arrayIndex))
*
* baja.iterate(array, startIndex, function (arrayElement, arrayIndex))
*
* baja.iterate(array, startIndex, endIndex, function (arrayElement, arrayIndex))
*
* baja.iterate(numberOfTimesToIterate, function (index))
*
* baja.iterate(iterationStartIndex, iterationEndIndex, function (index))
*
* baja.iterate(object, function (objectJsProperty, objectJsPropertyName))
*
* baja.iterate(object, function doIterate(object), function getNext(object))
*/
function iterate() {
var args = arguments,
arg0 = args[0],
arg1 = args[1],
arg2 = args[2],
arg3 = args[3],
typeString = Object.prototype.toString.call(arg0);
if (arg0 === undefined || arg0 === null) {
throw new Error("undefined passed to baja.iterate()");
}
if (typeString === '[object Array]') {
switch (arguments.length) {
case 2: //iterate(array, function (arrayElement, arrayIndex))
return iterateArray(arg0, 0, arg0.length, arg1);
case 3: //iterate(array, startIndex, function (arrayElement, arrayIndex))
return iterateArray(arg0, arg1, arg0.length, arg2);
case 4: //iterate(array, startIndex, endIndex, function (arrayElement, arrayIndex))
return iterateArray(arg0, arg1, arg2, arg3);
}
} else if (typeString === '[object Object]') {
if (arg0 instanceof Cursor) {
//bajaScriptCursor.each(function ())
return arg0.each(arg1);
}
if (typeof arg2 === 'function') {
//iterate(object, function doIterate(object), function getNext(object))
return iterateCustomNext(arg0, arg1, arg2);
}
//iterate(object, function (objectJsProperty, objectJsPropertyName))
return iterateJsProperties(arg0, arg1);
}
if (typeString === '[object Number]') {
if (typeof arg1 === 'number') {
//iterate(iterationStartIndex, iterationEndIndex, function (index))
return iterateByIndex(arg0, arg1, arg2);
}
//iterate(numberOfTimesToIterate, function (index))
return iterateByIndex(0, arg0, arg1);
}
if (typeString === '[object Arguments]') {
throw new Error("Arguments object not iterable (pass through " +
"Array.prototype.slice first)");
}
throw new Error(arg0 + " is not iterable");
}
/**
* Unescape Xml characters out of a string.
* Here are some unescape examples:
* * '&lt;' to <
* * '&gt;' to >
* * '&amp;' to &
* * '&apos;' to '
* * '&quot;' to "
* * '&#x20ac;' to €
*
* @function baja.unescapeXml
* @param {String} [str]
* @return {String}
*/
function unescapeXml(str) {
if (!str) {
return str;
}
return str.replace(/&(#x([0-9a-fA-F]+)|(apos|quot|lt|gt|amp));/g, function (match, _, code, replace) {
return code ? String.fromCharCode(parseInt(code, 16)) : unescapeXmlMap[replace];
});
}
function doRequire(deps) {
const df = bajaPromises.deferred();
requirejs(deps, function () {
df.resolve(Array.prototype.slice.call(arguments).map(function (arg) {
if (arg && arg.__esModule) {
return arg.default || arg;
}
return arg;
}));
}, df.reject);
return df.promise();
}
function resolveDependencies(tree) {
function resolveLayerAt(i) {
const layer = tree && tree[i];
return bajaPromises.resolve(layer && doRequire(layer).then(() => resolveLayerAt(i + 1)));
}
return resolveLayerAt(0);
}
return {
def: def,
doRequire,
iterate: iterate,
objectify: objectify,
resolveDependencies,
strictArg: strictArg,
strictAllArgs: strictAllArgs,
strictNumber: strictNumber,
unescapeXml: unescapeXml,
$setValidateArgs: (v) => { validateArgs = v; }
};
});