json-logic-js/logic.js

317 lines
9.1 KiB
JavaScript

/* globals define,module */
/*
Using a Universal Module Loader that should be browser, require, and AMD friendly
http://ricostacruz.com/cheatsheets/umdjs.html
*/
;(function(root, factory) {
if (typeof define === "function" && define.amd) {
define(factory);
} else if (typeof exports === "object") {
module.exports = factory();
} else {
root.jsonLogic = factory();
}
}(this, function() {
"use strict";
/* globals console:false */
if ( ! Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === "[object Array]";
};
}
if( ! Array.unique) {
/* eslint-disable no-extend-native */
Array.prototype.unique = function() {
var a = [];
for (var i=0, l=this.length; i<l; i++) {
if (a.indexOf(this[i]) === -1) {
a.push(this[i]);
}
}
return a;
};
}
var jsonLogic = {};
var operations = {
"==": function(a, b) {
return a == b;
},
"===": function(a, b) {
return a === b;
},
"!=": function(a, b) {
return a != b;
},
"!==": function(a, b) {
return a !== b;
},
">": function(a, b) {
return a > b;
},
">=": function(a, b) {
return a >= b;
},
"<": function(a, b, c) {
return (c === undefined) ? a < b : (a < b) && (b < c);
},
"<=": function(a, b, c) {
return (c === undefined) ? a <= b : (a <= b) && (b <= c);
},
"!!": function(a) {
return jsonLogic.truthy(a);
},
"!": function(a) {
return !jsonLogic.truthy(a);
},
"%": function(a, b) {
return a % b;
},
"log": function(a) {
console.log(a); return a;
},
"in": function(a, b) {
if(typeof b.indexOf === "undefined") return false;
return (b.indexOf(a) !== -1);
},
"cat": function() {
return Array.prototype.join.call(arguments, "");
},
"+": function() {
return Array.prototype.reduce.call(arguments, function(a, b) {
return parseFloat(a, 10) + parseFloat(b, 10);
}, 0);
},
"*": function() {
return Array.prototype.reduce.call(arguments, function(a, b) {
return parseFloat(a, 10) * parseFloat(b, 10);
});
},
"-": function(a, b) {
if(b === undefined) {
return -a;
}else{
return a - b;
}
},
"/": function(a, b) {
if(b === undefined) {
return a;
}else{
return a / b;
}
},
"min": function() {
return Math.min.apply(this, arguments);
},
"max": function() {
return Math.max.apply(this, arguments);
},
"merge": function() {
return Array.prototype.reduce.call(arguments, function(a, b) {
return a.concat(b);
}, []);
},
"var": function(a, b) {
var not_found = (b === undefined) ? null : b;
var sub_props = String(a).split(".");
var data = this;
for(var i = 0; i < sub_props.length; i++) {
// Descending into data
data = data[sub_props[i]];
if(data === undefined) {
return not_found;
}
}
return data;
},
"missing": function() {
/*
Missing can receive many keys as many arguments, like {"missing:[1,2]}
Missing can also receive *one* argument that is an array of keys,
which typically happens if it's actually acting on the output of another command
(like 'if' or 'merge')
*/
var missing = [];
var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments;
for(var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = jsonLogic.apply({"var": key}, this);
if(value === null || value === "") {
missing.push(key);
}
}
return missing;
},
"missing_some": function(need_count, options) {
// missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence.
var are_missing = jsonLogic.apply({"missing": options}, this);
if(options.length - are_missing.length >= need_count) {
return [];
}else{
return are_missing;
}
},
"method": function(obj, method, args) {
return obj[method].apply(obj, args);
},
};
jsonLogic.is_logic = function(logic) {
return (
logic !== null
&& typeof logic === "object"
&& ! Array.isArray(logic)
);
};
/*
This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.
Literal | JS | PHP | JsonLogic
--------+-------+-------+---------------
[] | true | false | false
"0" | true | false | true
*/
jsonLogic.truthy = function(value) {
if(Array.isArray(value) && value.length === 0) {
return false;
}
return !! value;
};
jsonLogic.apply = function(logic, data) {
// Does this array contain logic? Only one way to find out.
if(Array.isArray(logic)) {
return logic.map(function(l) {
return jsonLogic.apply(l, data);
});
}
// You've recursed to a primitive, stop!
if( ! jsonLogic.is_logic(logic) ) {
return logic;
}
data = data || {};
var op = Object.keys(logic)[0];
var values = logic[op];
var i;
var current;
// easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
if( ! Array.isArray(values)) {
values = [values];
}
// 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
if(op === "if" || op == "?:") {
/* 'if' should be called with a odd number of parameters, 3 or greater
This works on the pattern:
if( 0 ){ 1 }else{ 2 };
if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
The implementation is:
For pairs of values (0,1 then 2,3 then 4,5 etc)
If the first evaluates truthy, evaluate and return the second
If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
given 0 parameters, return NULL (not great practice, but there was no Else)
*/
for(i = 0; i < values.length - 1; i += 2) {
if( jsonLogic.truthy( jsonLogic.apply(values[i], data) ) ) {
return jsonLogic.apply(values[i+1], data);
}
}
if(values.length === i+1) return jsonLogic.apply(values[i], data);
return null;
}else if(op === "and") { // Return first falsy, or last
for(i=0; i < values.length; i+=1) {
current = jsonLogic.apply(values[i], data);
if( ! jsonLogic.truthy(current)) {
return current;
}
}
return current; // Last
}else if(op === "or") {// Return first truthy, or last
for(i=0; i < values.length; i+=1) {
current = jsonLogic.apply(values[i], data);
if( jsonLogic.truthy(current) ) {
return current;
}
}
return current; // Last
}
// Everyone else gets immediate depth-first recursion
values = values.map(function(val) {
return jsonLogic.apply(val, data);
});
if(typeof operations[op] === "function") {
return operations[op].apply(data, values);
}else if(op.indexOf(".") > 0) { // Contains a dot, and not in the 0th position
var sub_ops = String(op).split(".");
var operation = operations;
for(i = 0; i < sub_ops.length; i++) {
// Descending into operations
operation = operation[sub_ops[i]];
if(operation === undefined) {
throw new Error("Unrecognized operation " + op +
" (failed at " + sub_ops.slice(0, i+1).join(".") + ")");
}
}
return operation.apply(data, values);
}else{
throw new Error("Unrecognized operation " + op );
}
// The operation is called with "data" bound to its "this" and "values" passed as arguments.
// Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
return operations[op].apply(data, values);
};
jsonLogic.uses_data = function(logic) {
var collection = [];
if( jsonLogic.is_logic(logic) ) {
var op = Object.keys(logic)[0];
var values = logic[op];
if( ! Array.isArray(values)) {
values = [values];
}
if(op === "var") {
// This doesn't cover the case where the arg to var is itself a rule.
collection.push(values[0]);
}else{
// Recursion!
values.map(function(val) {
collection.push.apply(collection, jsonLogic.uses_data(val) );
});
}
}
return collection.unique();
};
jsonLogic.add_operation = function(name, code) {
operations[name] = code;
};
return jsonLogic;
}));