146 lines
4.2 KiB
JavaScript
146 lines
4.2 KiB
JavaScript
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`));
|
|
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}'`));
|
|
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),
|
|
};
|
|
}
|