mirror of
https://github.com/jwadhams/json-logic-js.git
synced 2026-02-01 17:27:48 +00:00
317 lines
9.1 KiB
JavaScript
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;
|
|
}));
|