Source: expectCore.js

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

const _ = require('lodash');
const assert = require('assert');

// NOTE: with Babel 6 set to transpile to ES6, classes which extend Error are
// not properly recognized as subclasses -- so "instanceof RoboliqError" won't
// work.  We need a number of work-arounds because of that.
class RoboliqError extends Error {
	constructor(context = {}, errors, fnIgnore, stack) {
		super(getErrors(context, errors).join("; "));
		this.isRoboliqError = true;
		this.name = "RoboliqError";
		this.context = _.cloneDeep(context);
		this.errors = _.isArray(errors) ? errors : [errors];
		if (_.isUndefined(stack)) {
			Error.captureStackTrace(this, fnIgnore || this.constructor.name);
			//Error.captureStackTrace(this);
		}
		else {
			this.stack = stack;
		}

		// console.log("new RoboliqError: "+this.__proto__)
		// console.log(this);
		// console.log(this.stack)
	}

	getPrefix() { return getPrefix(this.context); }

	toString() { return getErrors(context, errors).join("; "); }

	static getErrors(e) { return getErrors(e.context, e.errors); }
}

function getPrefix(context) {
	const prefixes = [];
	if (_.isPlainObject(context)) {
		if (_.isString(context.stepName)) prefixes.push(`steps.${context.stepName}`);
		if (_.isString(context.objectName)) prefixes.push(`objects.${context.objectName}`);
		if (_.isString(context.paramName)) prefixes.push(`parameter "${context.paramName}"`);
		else if (_.isArray(context.paramName)) prefixes.push(`parameters "${context.paramName.join('`, `')}"`);
	}
	return (prefixes.length > 0) ? prefixes.join(", ")+": " : "";
}

function addContext(e, context) {
	_.mergeWith(e.context, context || {}, (a, b) => {
		if (_.isArray(a) && _.isArray(b)) {
			return a.concat(b);
		}
	});
}

function getErrors(context, errors) {
	errors = _.isArray(errors) ? errors : [errors];
	const prefix = getPrefix(context);
	return _.map(errors || [], s => prefix+s);
}

function _context(context, fn) {
	try {
		fn();
	}
	catch (e) {
		// console.log("_context: "+e.__proto__)
		// console.log(JSON.stringify(e, null, '\t'))
		rethrow(e, context);
	}
}

function rethrow(e, context, fnIgnore = rethrow) {
	if (e instanceof Error) {
		if (e.isRoboliqError) {
			// console.log("rethrow1")
			addContext(e, context);
			throw e;
		}
		else {
			// console.log("rethrow2")
			const error = new RoboliqError(context, [e.message], undefined, e.stack);
			throw error;
		}
	}
	else if (typeof e === "string") {
		throw new RoboliqError({errors: [e]}, fnIgnore);
	}
	else {
		// console.log("rethrow4")
		throw new RoboliqError(context, [e.toString()], fnIgnore);
	}
}

/*function getContextPrefix(context) {
	const prefixes = [];
	if (!_.isEmpty(context)) {
		if (_.isString(context.paramName)) prefixes.push(`parameter "${context.paramName}"`);
		else if (_.isArray(context.paramName)) prefixes.push(`parameters "${context.paramName.join('`, `')}"`);
		if (_.isString(context.objectName)) prefixes.push(`object "${context.objectName}"`);
	}
	return (prefixes.length > 0) ? prefixes.join(", ")+": " : "";
}*/

/*function handleError(context, e) {
	var prefix = getContextPrefix(context);

	if (!e.trace) {
		if (e.stack)
			console.log(e.stack);
		e.trace = e.stack;
	}

	if (e.errors) {
		e.errors = _.map(e.errors, message => prefix+message);
	}
	else {
		e.name = "ProcessingError";
		e.errors = _.compact([prefix+e.message]);
	}

	//console.log({epath: e.path, cpath: context.path})
	if (!e.path && context.path)
		e.path = context.path;

	throw e;
}*/

function truthy(context, result, message) {
	assert(message, "you must provide a `message` value");
	if (!result) {
		if (_.isFunction(message))
			message = message();
		throw new RoboliqError(context, [message], truthy);
	}
}

function _try(context, fn) {
	try {
		return fn();
	} catch (e) {
		rethrow(e, context, _try);
	}
}

function _throw(context, errors) {
	throw new RoboliqError(context, errors, _throw);
}

// TODO: get rid of this function after refactoring parameter processing in roboliqDirectives
function paramsRequired(params, names) {
	assert(_.isPlainObject(params));
	assert(_.isArray(names));
	_.forEach(names, function(name) {
		truthy({paramName: name}, params.hasOwnProperty(name), "missing required value [CODE 135]");
	});
}

module.exports = {
	RoboliqError,
	context: _context,
	getPrefix,
	paramsRequired: paramsRequired,
	rethrow,
	throw: _throw,
	truthy: truthy,
	try: _try
}