/**
* 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
}