Source: misc.js

/**
 * Roboliq: Automation for liquid-handling robots
 * @copyright 2017, ETH Zurich, Ellis Whitehead
 * @license GPL-3.0
 */

/**
 * A set of miscellaneous helper functions.
 *
 * Some of these are HACKs that should be moved to another module or removed entirely.
 *
 * @module
 */

var _ = require('lodash');
var assert = require('assert');
var Handlebars = require('handlebars');

Handlebars.registerHelper('toJSON', function(obj) {
	return JSON.stringify(obj);
});

/**
 * queryResults: value returned from llpl.query()
 * predicateName: name of the predicate that was used for the query
 * @returns {object} `{parameterName1: parameterValues1, ...}`
 * @static
 */
function extractValuesFromQueryResults(queryResults, predicateName) {
	var acc = _.reduce(queryResults, function(acc, x1) {
		var x2 = x1[predicateName];
		_.forEach(x2, function(value, name) {
			if (_.isEmpty(acc[name]))
				acc[name] = [value];
			else
				acc[name].push(value);
		});
		return acc;
	}, {});
	return acc;
}

function findObjectsValue(key, objects, effects, defaultValue, prefix) {
	if (effects) {
		var id = (prefix) ? prefix+"."+key : key;
		if (effects.hasOwnProperty(id))
			return effects[id];
	}
	return _.get(objects, key, defaultValue);
}

// NOTE: This is basically a copy of expect.objectsValue
function getObjectsValue(key, objects, effects, prefix) {
	assert(_.isString(key), "getObjectsValue expected a string key, received: "+JSON.stringify(key));
	if (effects) {
		var id = (prefix) ? prefix+"."+key : key;
		if (effects.hasOwnProperty(id))
			return effects[id];
	}
	var l = key.split('.');
	for (var i = 0; !_.isEmpty(objects) && i < l.length; i++) {
		if (!objects.hasOwnProperty(l[i])) {
			var objectName = _.take(l, i + 1).join('.');
			if (prefix) objectName = prefix + '.' + objectName;
			var message = "value `"+objectName+"`: undefined";
			// console.log({key, objects})
			//console.log("objects:", objects)
			//console.log(message);
			throw new Error(message);//{name: "ProcessingError", errors: [message]};
		}
		objects = objects[l[i]];
	}
	return objects;
}

function getVariableValue(spec, objects, effects, prefix) {
	if (_.isString(spec)) {
		if (_.startsWith(spec, '"'))
			return spec;
		var found = findObjectsValue(spec, objects, effects);
		if (!_.isUndefined(found)) {
			if (found.type === "Variable") {
				return found.value;
			}
			else {
				return found;
			}
		}
	}
	return spec;
}

function getObjectsOfType(objects, types, prefix) {
	if (_.isString(types)) types = [types];
	if (!prefix) prefix = [];

	var l = {};
	_.forEach(objects, function(o, name) {
		var prefix1 = prefix.concat([name]);
		if (_.has(o, "type") && _.isString(o.type) && types.indexOf(o.type) >= 0) {
			var id = prefix1.join('.');
			l[id] = o;
		}
		_.forEach(o, function(o2, name2) {
			if (_.isPlainObject(o2)) {
				var prefix2 = prefix1.concat([name2]);
				_.merge(l, getObjectsOfType(o2, types, prefix2));
			}
		});
	});
	return l;
}

/**
 * If spec is a directive, process it and return the result.
 *
 * @param  {Any} spec Any value.  If this is a directive, it will be an object with a single key that starts with '#'.
 * @param  {Object} data An object with properties: directiveHandlers, objects, events.
 * @return {Any} Return the object, or if it was a directive, the results of the directive handler.
 */
function handleDirective(spec, data) {
	// console.log(`handleDirective: `+JSON.stringify(spec))
	// console.log("data1: "+JSON.stringify(data));
	const directiveHandlers = data.directiveHandlers || (data.protocol || {}).directiveHandlers;
	if (_.isPlainObject(spec)) {
		const keys = _.keys(spec);
		if (keys.length === 1) {
			const key0 = keys[0];
			const key
				= (_.startsWith(key0, "#")) ? key0.substr(1)
				: (_.endsWith(key0, "()")) ? key0.substr(0, key0.length - 2)
				: undefined;
			// console.log({key0, key})
			if (key) {
				if (directiveHandlers.hasOwnProperty(key)) {
					var spec2 = spec[key0];
					// console.log({spec2, handler: directiveHandlers[key]})
					var spec3 = (_.isPlainObject(spec2))
						? _.omit(spec2, 'override')
						: spec2;
					const result = {
						x: directiveHandlers[key](spec3, data)
					};
					if (spec2.hasOwnProperty('override')) {
						//console.log({result0: result.x})
						_.merge(result, {x: spec2.override});
						//console.log({result1: result.x})
					}
					return result.x;
				}
				else {
					throw new Error("unknown directive object: "+key);
				}
			}
		}
	}
	else if (_.isString(spec)) {
	 	// Inline directives
		if (_.startsWith(spec, "#")) {
		 	const hash2 = spec.indexOf('#', 1);
			const key = (hash2 > 0) ? spec.substr(1, hash2 - 1) : spec.substr(1);
			// console.log({hash2, key})
			if (directiveHandlers.hasOwnProperty(key)) {
				const spec2 = (hash2 > 0) ? spec.substr(hash2 + 1) : undefined;
				const spec3 = handleDirective(spec2, data);
				const result = directiveHandlers[key](spec3, data);
				if (spec.hasOwnProperty('override')) {
					_.merge(result, spec.override);
				}
				return result;
			}
			else {
				throw new Error("unknown directive string: "+spec);
			}
		}
		// Protocol parameters
		else if (_.startsWith(spec, "$#")) {
			const key = spec.substr(2);
			if (_.has(data.objects.PARAMS, key)) {
				const result = data.objects.PARAMS[key];
				if (!_.isUndefined(result)) {
					return result;
				}
				else {
					throw new Error("undefined parameter value: "+spec);
				}
			}
			else {
				throw new Error("undefined parameter: "+spec);
			}
		}
	}
	return spec;
}

/**
 * Recurses into object properties and replaces them with the result of handleDirective.
 * It will, however, skip properties named 'steps'.
 *
 * @param  {Any} spec Any value.  If this is a directive, it will be an object with a single key that starts with '#'.
 * @param  {Object} data An object with properties: directiveHandlers, objects, events.
 * @return {Any} Return the object, or if it was a directive, the results of the directive handler.
 */
function handleDirectiveDeep(x, data) {
	//return mapDeep(spec, function(spec) { return handleDirective(spec, data); });
	if (_.isPlainObject(x)) {
		if (!x.hasOwnProperty('data')) {
			x = _.mapValues(x, function(value, key) {
				return (key === 'steps')
					? value
					: handleDirectiveDeep(value, data);
			});
		}
	}
	else if (_.isArray(x)) {
		x = _.map(x, function(value, i) {
			return handleDirectiveDeep(value, data);
		});
	}
	x = handleDirective(x, data);
	return x;
}

/**
 * Recurses into object properties and maps them to the result of fn.
 *
 * @static
 * @param  {Any} x Any value.
 * @param  {Function} fn A function (x, key, path) that returns a mapped value.
 * @return {Any} Return the deeply mapped object.
 */
function mapDeep(x, fn, key, path = []) {
	if (_.isPlainObject(x)) {
		x = _.mapValues(x, function(value, key) {
			return mapDeep(value, fn, key, path.concat(key));
		});
	}
	else if (_.isArray(x)) {
		x = _.map(x, function(value, i) {
			return mapDeep(value, fn, i, path.concat(i));
		});
	}
	x = fn(x, key, path);
	return x;
}

/**
 * Recurses into object properties and replaces them with the result of fn.
 * 'x' will be mutated.
 *
 * @static
 * @param  {Any} x Any value.
 * @param  {Function} fn A function that returns a transformed value.
 * @return nothing
 */
function mutateDeep(x, fn) {
	//console.log("x:", x)
	if (_.isPlainObject(x)) {
		for (var key in x) {
			//console.log("key:", key)
			x[key] = mutateDeep(x[key], fn);
		}
	}
	else if (_.isArray(x)) {
		for (let i = 0; i < x.length; i++) {
			x[i] = mutateDeep(x[i], fn);
		}
	}
	return fn(x);
}

function renderTemplate(template, scope, data) {
	//console.log("renderTemplate:", template)
	if (_.isString(template)) {
		return renderTemplateString(template, scope, data);
	}
	else if (_.isArray(template)) {
		return _.map(template, function(x) { return renderTemplate(x, scope, data); });
	}
	else if (_.isPlainObject(template)) {
		return _.mapValues(template, function(x) { return renderTemplate(x, scope, data); });
	}
	else {
		return template;
	}
}

function renderTemplateString(s, scope, data) {
	//console.log("renderTemplateString:", s)

	assert(_.isString(s));
	if (_.startsWith(s, "${") && _.endsWith(s, "}")) {
		var name = s.substr(2, s.length - 3);
		return scope[name];
	}
	else if (_.startsWith(s, "{{") && _.endsWith(s, "}}")) {
		const s2 = Handlebars.compile(s)(scope);
		try {
			return JSON.parse(s2);
		}
		catch (e) {

		}
		return s2;
	}
	else {
		return Handlebars.compile(s)(scope);
	}
}

module.exports = {
	extractValuesFromQueryResults,
	getObjectsOfType,
	getObjectsValue,
	getVariableValue,
	handleDirective,
	handleDirectiveDeep,
	findObjectsValue,
	mutateDeep,
	mapDeep,
	renderTemplate
}