filter-expressions
Version:
Basic array structure that can conditionally validate objects
286 lines (265 loc) • 8.45 kB
JavaScript
const _isEqual = require("lodash.isequal");
const _get = require("lodash.get");
const _isEmpty = require("lodash.isempty");
const turfBooleanWithin = require("@turf/boolean-within").default;
const turfBooleanContains = require("@turf/boolean-contains").default;
const turfBooleanDisjoint = require("@turf/boolean-disjoint").default;
const turfBooleanCrosses = require("@turf/boolean-crosses").default;
const turfBooleanOverlap = require("@turf/boolean-overlap").default;
const modifier = /^(.+)\(([^\)]+)\)$/;
// Evaluate an expression
const evaluate = (expression, target = null, options = {}) => {
// If the supplied expression is resolved
if (typeof expression === "boolean") {
return expression;
}
// Expressions are arrays
if (!Array.isArray(expression)) {
return expression;
}
// Expressions should have more than 2 values
if (expression.length < 2) {
return expression;
}
// Expressions start with a string in teh array
if (typeof expression[0] !== "string") {
return expression;
}
// Resolve the expressions
// ... each part of an expression is possible to be evaluated..
const resolvedExpressions = expression.map(expr => evaluate(expr, target, options));
// Identify the comparative target
// ... targets can allow you to extract from context..
let comparative = resolvedExpressions[1];
comparative = _get(target || {}, resolvedExpressions[1], resolvedExpressions[1]);
// Support function modifiers
// ... you can modify values.. e.g. not(a)
if (modifier.test(resolvedExpressions[1]) && typeof resolvedExpressions[1] === "string" && options && options.modifiers) {
const modifierParts = expression[1].match(modifier);
const modifierFunc = modifierParts[1];
const modifierVal = modifierParts[2];
if (options.modifiers[modifierFunc]) {
let value = target ? _get(target, modifierVal) : modifierVal;
comparative = options.modifiers[modifierFunc](value, target);
}
}
// Evaluate the expression
switch (resolvedExpressions[0].toLowerCase()) {
// Existence
case "has":
case "have":
case "exists":
case "exist":
{
const t = target ? target[resolvedExpressions[1]] : resolvedExpressions[1];
return typeof t !== "undefined" && t !== null;
}
case "empty":
{
return _isEmpty(comparative);
}
case "!empty":
{
return !_isEmpty(comparative);
}
case "!has":
case "!have":
case "!exist":
case "!exists":
{
const t = target ? target[resolvedExpressions[1]] : resolvedExpressions[1];
return typeof t === "undefined" || t === null;
}
// Comparison expressions
case "==":
return _isEqual(comparative, resolvedExpressions[2]);
case "!=":
return !_isEqual(comparative, resolvedExpressions[2]);
case ">":
case ">=":
case "<":
case "<=":
{
let a = comparative;
let b = resolvedExpressions[2];
// Transformations based on type comparisons
if (Object.prototype.toString.call(a) === "[object Date]") {
a = a.getTime();
}
if (Object.prototype.toString.call(b) === "[object Date]") {
b = b.getTime();
}
if (typeof a !== "number" || typeof b !== "number") {
return false;
}
switch (expression[0]) {
case ">":
return a > b;
case ">=":
return a >= b;
case "<":
return a < b;
case "<=":
return a <= b;
default:
return false;
}
}
// Membership
case "in":
{
if (Array.isArray(comparative)) {
return comparative.reduce((carry, comp) => carry || expression.slice(2).indexOf(comp) > -1, false);
}
return expression.slice(2).indexOf(comparative) > -1;
}
case "!in":
{
if (Array.isArray(comparative)) {
return comparative.reduce((carry, comp) => carry || expression.slice(2).indexOf(comp) === -1, false);
}
return expression.slice(2).indexOf(comparative) === -1;
}
// String
case "match":
{
const [regex, flags = undefined] = resolvedExpressions.slice(2);
return new RegExp(regex, flags).test(comparative);
}
// Combining
case "all":
{
return resolvedExpressions.slice(1).reduce((carry, expr) => carry && expr === true, true);
}
case "any":
{
return resolvedExpressions.slice(1).reduce((carry, expr) => carry || expr === true, false);
}
case "none":
{
return !resolvedExpressions.slice(1).reduce((carry, expr) => carry || expr === true, false);
}
// Geo evaluation
case "geo-within":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return turfBooleanWithin(comparative, resolvedExpressions[2]);
}
case "!geo-within":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return !turfBooleanWithin(comparative, resolvedExpressions[2]);
}
case "geo-contains":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return turfBooleanContains(comparative, resolvedExpressions[2]);
}
case "!geo-contains":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return !turfBooleanContains(comparative, resolvedExpressions[2]);
}
case "geo-disjoint":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return turfBooleanDisjoint(comparative, resolvedExpressions[2]);
}
case "!geo-disjoint":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return !turfBooleanDisjoint(comparative, resolvedExpressions[2]);
}
case "geo-crosses":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return turfBooleanCrosses(comparative, resolvedExpressions[2]);
}
case "!geo-crosses":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return !turfBooleanCrosses(comparative, resolvedExpressions[2]);
}
case "geo-overlap":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return turfBooleanOverlap(comparative, resolvedExpressions[2]);
}
case "!geo-overlap":
{
if (!comparative || !resolvedExpressions[2]) {
return false;
}
return !turfBooleanOverlap(comparative, resolvedExpressions[2]);
}
// Accessors
case "get":
{
const t = target ? _get(target, resolvedExpressions[1], resolvedExpressions[2]) : resolvedExpressions[1];
return t;
}
// Type creation
case "date":
{
const t = resolvedExpressions[1];
if (t) {
return new Date(t);
}
}
// Otherwise ...
default:
// Support pluggable comparisons
if (options) {
const keyLower = resolvedExpressions[0].toLowerCase();
// Look for the comparison
let func = options.comparisons && options.comparisons[keyLower];
let inverse = false;
let type = "comparison";
// Look for an inverse comparison expression
if (!func && keyLower[0] === "!") {
inverse = true;
func = options.comparisons[keyLower.substr(1)];
}
// Look for an operator
if (!func && options.operators) {
func = options.operators[keyLower];
type = "operator";
}
// Check the result
if (typeof func === "function") {
if (type === "comparison") {
const value = func(comparative, ...resolvedExpressions.slice(2));
if (value) {
return inverse ? false : true;
} else {
return inverse ? true : false;
}
} else if (type === "operator") {
const value = func(target, ...resolvedExpressions.slice(1));
// Return the actual result
return value;
}
}
}
return resolvedExpressions;
}
};
module.exports = { evaluate };