import {fastApply} from '@avocado/core'; import compile from './compile'; const render = (ops) => ops.reduce((rendered, op) => { switch (op.type) { case 'key': return `${rendered ? `${rendered}.` : ''}${op.key}`; case 'invoke': return `${rendered}(${op.args.length > 0 ? '...' : ''})`; default: return rendered; } }, ''); function compileOp(op) { let args; if ('invoke' === op.type) { args = op.args.map(compile); } return (context, previous, current) => { switch (op.type) { case 'key': return current[op.key]; case 'invoke': // Pass the context itself as the last arg. const evaluated = args.map((fn) => fn(context)).concat(context); // Promises are resolved transparently. const apply = (args) => fastApply(previous, current, args); return evaluated.some((arg) => arg instanceof Promise) ? Promise.all(evaluated).then(apply) : apply(evaluated); } }; } function disambiguateResult(value, fn) { const apply = (value) => fn(value); return value instanceof Promise ? value.then(apply) : apply(value); } function compileExpression(expression) { if (0 === expression.ops.length) { return () => undefined; } const assign = 'undefined' !== typeof expression.assign ? compile(expression.assign) : undefined; const ops = expression.ops.map(compileOp); const {ops: rawOps} = expression; return (context) => { let previous = null; let shorted = false; const [first, ...rest] = ops; return rest.reduce((current, op, index) => disambiguateResult(current, (current) => { if (shorted) { return undefined; } let next = undefined; const isLastOp = index === ops.length - 2; if (!isLastOp || !assign) { if ('undefined' === typeof current) { const rendered = render(rawOps.slice(0, index + 2)); // next = Promise.reject(new Error(`'${rendered}' is undefined`)); next = undefined; shorted = true; } else { next = op(context, previous, current); } } else { const rawOp = rawOps[index + 1]; switch (rawOp.type) { case 'key': if ('object' === typeof current) { current[rawOp.key] = assign(context); next = undefined; } break; case 'invoke': const rendered = render(rawOps.slice(0, index + 2)); // next = Promise.reject(new Error(`invalid assignment to function '${rendered}'`)); next = undefined; break; } } previous = current; current = next; return current; }), context.getValue(rawOps[0].key)); }; } function compileCondition(condition) { const {operator} = condition; const operands = condition.operands.map(compile); return (context) => { switch (operator) { case 'is': return operands[0](context) === operands[1](context); case 'isnt': return operands[0](context) !== operands[1](context); case '>': return operands[0](context) > operands[1](context); case '>=': return operands[0](context) >= operands[1](context); case '<': return operands[0](context) < operands[1](context); case '<=': return operands[0](context) <= operands[1](context); case 'or': if (0 === operands.length) { return true; } for (let i = 0; i < operands.length; i++) { if (!!operands[i](context)) { return true; } } return false; case 'and': if (0 === operands.length) { return true; } for (let i = 0; i < operands.length; i++) { if (!operands[i](context)) { return false; } } return true; case 'contains': const haystack = operands[0](context); const needle = operands[1](context); return -1 !== haystack.indexOf(needle); } }; } export function behaviorCompilers() { return { condition: (condition) => compileCondition(condition), literal: ({value}) => (context) => value, expression: (expression) => compileExpression(expression), expressions: ({expressions}) => () => expressions.map(compileExpression), }; }