forked from jwadhams/json-logic-js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding new helpers, so the default import is an object, not a single …
…function. Working on complex conditionals.
- Loading branch information
Showing
5 changed files
with
239 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,91 +1,192 @@ | ||
(function(global) { | ||
/* | ||
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) { | ||
if ( ! Array.isArray) { | ||
Array.isArray = function(arg) { | ||
return Object.prototype.toString.call(arg) === '[object Array]'; | ||
}; | ||
} | ||
|
||
global.jsonLogic = function(tests, data){ | ||
if( ! Array.unique){ | ||
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 = {}, | ||
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 !a; }, | ||
"%" : function(a,b){ return a % b; }, | ||
"and" : function(){ | ||
return Array.prototype.reduce.call(arguments, function(a,b){ return a && b; }); | ||
}, | ||
"or" : function(){ | ||
return Array.prototype.reduce.call(arguments, 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); } | ||
}; | ||
|
||
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){ | ||
//You've recursed to a primitive, stop! | ||
if(tests === null || typeof tests !== "object" || Array.isArray(tests) ){ | ||
return tests; | ||
if( ! jsonLogic.is_logic(logic) ){ | ||
return logic; | ||
} | ||
|
||
data = data || {}; | ||
|
||
var op = Object.keys(tests)[0], | ||
values = tests[op], | ||
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 !a; }, | ||
"%" : function(a,b){ return a % b; }, | ||
"and" : function(){ | ||
return Array.prototype.reduce.call(arguments, function(a,b){ return a && b; }); | ||
}, | ||
"or" : function(){ | ||
return Array.prototype.reduce.call(arguments, function(a,b){ return a || b; }); | ||
}, | ||
"?:" : function(a,b,c){ return a ? b : c; }, | ||
"log" : function(a){ console.log(a); return a; }, | ||
"in" : function(a, b){ | ||
if(typeof b.indexOf === 'undefined') return false; | ||
return (b.indexOf(a) !== -1); | ||
}, | ||
"var" : function(a, not_found){ | ||
if(not_found === undefined) not_found = null; | ||
var sub_props = String(a).split("."); | ||
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; | ||
}, | ||
"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); | ||
}); | ||
}, | ||
"*" : 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); } | ||
}; | ||
var op = Object.keys(logic)[0], | ||
values = logic[op]; | ||
|
||
//easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} | ||
if( ! Array.isArray(values)){ values = [values]; } | ||
|
||
// 'if' violates the normal rule of depth-first calculating consequents, let it manage recursion | ||
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: | ||
given two+ parameters, | ||
shift off the first two values. | ||
If the first evaluates truthy, evaluate and return the second | ||
If the first evaluates falsy, start again with the remaining parameters. | ||
given one parameter, evaluate and return it. | ||
given 0 parameters, return NULL | ||
*/ | ||
while(values.length >= 2){ | ||
var conditional = jsonLogic.apply(values.shift(), data), | ||
consequent = values.shift(); | ||
|
||
if( jsonLogic.truthy(conditional) ){ | ||
return jsonLogic.apply(consequent, data); | ||
} | ||
} | ||
|
||
if(values.length === 1) return jsonLogic.apply(values[0], data); | ||
return null; | ||
} | ||
|
||
|
||
// Everyone else gets immediate depth-first recursion | ||
values = values.map(function(val){ return jsonLogic.apply(val, data); }); | ||
|
||
// 'var' needs access to data, only available in this scope | ||
if(op === "var"){ | ||
var not_found = values[1] || null, | ||
sub_props = String(values[0]).split("."); | ||
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; | ||
} | ||
|
||
if(undefined === operations[op]){ | ||
throw new Error("Unrecognized operation " + op ); | ||
} | ||
|
||
//easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} | ||
if(!Array.isArray(values)){ values = [values]; } | ||
return operations[op].apply({}, values); | ||
|
||
//Recursion! | ||
values = values.map(function(val){ return jsonLogic(val, data); }); | ||
}; | ||
|
||
return operations[op].apply({}, values); | ||
jsonLogic.uses_data = function(logic){ | ||
var collection = []; | ||
|
||
if( jsonLogic.is_logic(logic) ){ | ||
var op = Object.keys(logic)[0], | ||
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(); | ||
}; | ||
|
||
}(this)); | ||
return jsonLogic; | ||
|
||
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
[ | ||
"Too few args", | ||
[{"if":[]}, null, null], | ||
[{"if":[true]}, null, true], | ||
[{"if":[false]}, null, false], | ||
[{"if":["apple"]}, null, "apple"], | ||
|
||
"Simple if/then/else cases", | ||
[{"if":[true, "apple"]}, null, "apple"], | ||
[{"if":[false, "apple"]}, null, null], | ||
[{"if":[true, "apple", "banana"]}, null, "apple"], | ||
[{"if":[false, "apple", "banana"]}, null, "banana"], | ||
|
||
"Empty arrays are falsey", | ||
[{"if":[ [], "apple", "banana"]}, null, "banana"], | ||
[{"if":[ [1], "apple", "banana"]}, null, "apple"], | ||
[{"if":[ [1,2,3,4], "apple", "banana"]}, null, "apple"], | ||
|
||
"Empty strings are falsey, all other strings are truthy", | ||
[{"if":[ "", "apple", "banana"]}, null, "banana"], | ||
[{"if":[ "zucchini", "apple", "banana"]}, null, "apple"], | ||
[{"if":[ "0", "apple", "banana"]}, null, "apple"], | ||
|
||
"You can cast a string to numeric with a unary + ", | ||
[{"===":[0,"0"]}, null, false], | ||
[{"===":[0,{"+":"0"}]}, null, true], | ||
[{"if":[ {"+":"0"}, "apple", "banana"]}, null, "banana"], | ||
[{"if":[ {"+":"1"}, "apple", "banana"]}, null, "apple"], | ||
|
||
"Zero is falsy, all other numbers are truthy", | ||
[{"if":[ 0, "apple", "banana"]}, null, "banana"], | ||
[{"if":[ 1, "apple", "banana"]}, null, "apple"], | ||
[{"if":[ 3.1416, "apple", "banana"]}, null, "apple"], | ||
[{"if":[ -1, "apple", "banana"]}, null, "apple"], | ||
|
||
"If the conditional is logic, it gets evaluated", | ||
[{"if":[ {">":[2,1]}, "apple", "banana"]}, null, "apple"], | ||
[{"if":[ {">":[1,2]}, "apple", "banana"]}, null, "banana"], | ||
|
||
"If the consequents are logic, they get evaluated", | ||
[{"if":[ true, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "apple"], | ||
[{"if":[ false, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "banana"], | ||
|
||
"If/then/elseif/then cases", | ||
[{"if":[true, "apple", true, "banana"]}, null, "apple"], | ||
[{"if":[true, "apple", false, "banana"]}, null, "apple"], | ||
[{"if":[false, "apple", true, "banana"]}, null, "banana"], | ||
[{"if":[false, "apple", false, "banana"]}, null, null], | ||
|
||
[{"if":[true, "apple", true, "banana", "carrot"]}, null, "apple"], | ||
[{"if":[true, "apple", false, "banana", "carrot"]}, null, "apple"], | ||
[{"if":[false, "apple", true, "banana", "carrot"]}, null, "banana"], | ||
[{"if":[false, "apple", false, "banana", "carrot"]}, null, "carrot"], | ||
|
||
[{"if":[false, "apple", false, "banana", false, "carrot"]}, null, null], | ||
[{"if":[false, "apple", false, "banana", false, "carrot", "date"]}, null, "date"], | ||
[{"if":[false, "apple", false, "banana", true, "carrot", "date"]}, null, "carrot"], | ||
[{"if":[false, "apple", true, "banana", false, "carrot", "date"]}, null, "banana"], | ||
[{"if":[false, "apple", true, "banana", true, "carrot", "date"]}, null, "banana"], | ||
[{"if":[true, "apple", false, "banana", false, "carrot", "date"]}, null, "apple"], | ||
[{"if":[true, "apple", false, "banana", true, "carrot", "date"]}, null, "apple"], | ||
[{"if":[true, "apple", true, "banana", false, "carrot", "date"]}, null, "apple"], | ||
[{"if":[true, "apple", true, "banana", true, "carrot", "date"]}, null, "apple"] | ||
|
||
] |