Skip to content

Commit 8c0037c

Browse files
committed
Adding new helpers, so the default import is an object, not a single function. Working on complex conditionals.
1 parent 7f3db14 commit 8c0037c

File tree

5 files changed

+239
-77
lines changed

5 files changed

+239
-77
lines changed

gulpfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ gulp.task('test', function(cb) {
1414
});
1515

1616
gulp.task('watch', function() {
17-
gulp.watch('**/*.js', ['test']);
17+
gulp.watch(['**/*.js', 'tests/tests.json'], ['test']);
1818
});

logic.js

Lines changed: 169 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,192 @@
1-
(function(global) {
1+
/*
2+
Using a Universal Module Loader that should be browser, require, and AMD friendly
3+
http://ricostacruz.com/cheatsheets/umdjs.html
4+
*/
5+
;(function (root, factory) {
6+
7+
if (typeof define === 'function' && define.amd) {
8+
define(factory);
9+
} else if (typeof exports === 'object') {
10+
module.exports = factory();
11+
} else {
12+
root.jsonLogic = factory();
13+
}
14+
15+
}(this, function () {
216
'use strict';
317
/*globals console:false */
418

5-
if (!Array.isArray) {
19+
if ( ! Array.isArray) {
620
Array.isArray = function(arg) {
721
return Object.prototype.toString.call(arg) === '[object Array]';
822
};
923
}
1024

11-
global.jsonLogic = function(tests, data){
25+
if( ! Array.unique){
26+
Array.prototype.unique = function() {
27+
var a = [];
28+
for (var i=0, l=this.length; i<l; i++){
29+
if (a.indexOf(this[i]) === -1){
30+
a.push(this[i]);
31+
}
32+
}
33+
return a;
34+
};
35+
}
36+
37+
var jsonLogic = {},
38+
operations = {
39+
"==" : function(a,b){ return a == b; },
40+
"===" : function(a,b){ return a === b; },
41+
"!=" : function(a,b){ return a != b; },
42+
"!==" : function(a,b){ return a !== b; },
43+
">" : function(a,b){ return a > b; },
44+
">=" : function(a,b){ return a >= b; },
45+
"<" : function(a,b,c){
46+
return (c === undefined) ? a < b : (a < b) && (b < c);
47+
},
48+
"<=" : function(a,b,c){
49+
return (c === undefined) ? a <= b : (a <= b) && (b <= c);
50+
},
51+
"!" : function(a){ return !a; },
52+
"%" : function(a,b){ return a % b; },
53+
"and" : function(){
54+
return Array.prototype.reduce.call(arguments, function(a,b){ return a && b; });
55+
},
56+
"or" : function(){
57+
return Array.prototype.reduce.call(arguments, function(a,b){ return a || b; });
58+
},
59+
"log" : function(a){ console.log(a); return a; },
60+
"in" : function(a, b){
61+
if(typeof b.indexOf === 'undefined') return false;
62+
return (b.indexOf(a) !== -1);
63+
},
64+
"cat" : function(){
65+
return Array.prototype.join.call(arguments, "");
66+
},
67+
"+" : function(){
68+
return Array.prototype.reduce.call(arguments, function(a,b){
69+
return parseFloat(a,10) + parseFloat(b, 10);
70+
}, 0);
71+
},
72+
"*" : function(){
73+
return Array.prototype.reduce.call(arguments, function(a,b){
74+
return parseFloat(a,10) * parseFloat(b, 10);
75+
});
76+
},
77+
"-" : function(a,b){ if(b === undefined){return -a;}else{return a - b;} },
78+
"/" : function(a,b){ if(b === undefined){return a;}else{return a / b;} },
79+
"min" : function(){ return Math.min.apply(this,arguments); },
80+
"max" : function(){ return Math.max.apply(this,arguments); }
81+
};
82+
83+
jsonLogic.is_logic = function(logic){
84+
return (logic !== null && typeof logic === "object" && ! Array.isArray(logic) );
85+
};
86+
87+
/*
88+
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.
89+
90+
Literal | JS | PHP | JsonLogic
91+
--------+-------+-------+---------------
92+
[] | true | false | false
93+
"0" | true | false | true
94+
*/
95+
jsonLogic.truthy = function(value){
96+
if(Array.isArray(value) && value.length === 0){ return false; }
97+
return !! value;
98+
};
99+
100+
jsonLogic.apply = function(logic, data){
12101
//You've recursed to a primitive, stop!
13-
if(tests === null || typeof tests !== "object" || Array.isArray(tests) ){
14-
return tests;
102+
if( ! jsonLogic.is_logic(logic) ){
103+
return logic;
15104
}
16105

17106
data = data || {};
18107

19-
var op = Object.keys(tests)[0],
20-
values = tests[op],
21-
operations = {
22-
"==" : function(a,b){ return a == b; },
23-
"===" : function(a,b){ return a === b; },
24-
"!=" : function(a,b){ return a != b; },
25-
"!==" : function(a,b){ return a !== b; },
26-
">" : function(a,b){ return a > b; },
27-
">=" : function(a,b){ return a >= b; },
28-
"<" : function(a,b,c){
29-
return (c === undefined) ? a < b : (a < b) && (b < c);
30-
},
31-
"<=" : function(a,b,c){
32-
return (c === undefined) ? a <= b : (a <= b) && (b <= c);
33-
},
34-
"!" : function(a){ return !a; },
35-
"%" : function(a,b){ return a % b; },
36-
"and" : function(){
37-
return Array.prototype.reduce.call(arguments, function(a,b){ return a && b; });
38-
},
39-
"or" : function(){
40-
return Array.prototype.reduce.call(arguments, function(a,b){ return a || b; });
41-
},
42-
"?:" : function(a,b,c){ return a ? b : c; },
43-
"log" : function(a){ console.log(a); return a; },
44-
"in" : function(a, b){
45-
if(typeof b.indexOf === 'undefined') return false;
46-
return (b.indexOf(a) !== -1);
47-
},
48-
"var" : function(a, not_found){
49-
if(not_found === undefined) not_found = null;
50-
var sub_props = String(a).split(".");
51-
for(var i = 0 ; i < sub_props.length ; i++){
52-
//Descending into data
53-
data = data[ sub_props[i] ];
54-
if(data === undefined){ return not_found; }
55-
}
56-
return data;
57-
},
58-
"cat" : function(){
59-
return Array.prototype.join.call(arguments, "");
60-
},
61-
"+" : function(){
62-
return Array.prototype.reduce.call(arguments, function(a,b){
63-
return parseFloat(a,10) + parseFloat(b, 10);
64-
});
65-
},
66-
"*" : function(){
67-
return Array.prototype.reduce.call(arguments, function(a,b){
68-
return parseFloat(a,10) * parseFloat(b, 10);
69-
});
70-
},
71-
"-" : function(a,b){ if(b === undefined){return -a;}else{return a - b;} },
72-
"/" : function(a,b){ if(b === undefined){return a;}else{return a / b;} },
73-
"min" : function(){ return Math.min.apply(this,arguments); },
74-
"max" : function(){ return Math.max.apply(this,arguments); }
75-
};
108+
var op = Object.keys(logic)[0],
109+
values = logic[op];
110+
111+
//easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
112+
if( ! Array.isArray(values)){ values = [values]; }
113+
114+
// 'if' violates the normal rule of depth-first calculating consequents, let it manage recursion
115+
if(op === 'if' || op == '?:'){
116+
/* 'if' should be called with a odd number of parameters, 3 or greater
117+
This works on the pattern:
118+
if( 0 ){ 1 }else{ 2 };
119+
if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
120+
if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
121+
122+
The implementation is:
123+
given two+ parameters,
124+
shift off the first two values.
125+
If the first evaluates truthy, evaluate and return the second
126+
If the first evaluates falsy, start again with the remaining parameters.
127+
given one parameter, evaluate and return it.
128+
given 0 parameters, return NULL
129+
*/
130+
while(values.length >= 2){
131+
var conditional = jsonLogic.apply(values.shift(), data),
132+
consequent = values.shift();
76133

134+
if( jsonLogic.truthy(conditional) ){
135+
return jsonLogic.apply(consequent, data);
136+
}
137+
}
138+
139+
if(values.length === 1) return jsonLogic.apply(values[0], data);
140+
return null;
141+
}
142+
143+
144+
// Everyone else gets immediate depth-first recursion
145+
values = values.map(function(val){ return jsonLogic.apply(val, data); });
146+
147+
// 'var' needs access to data, only available in this scope
148+
if(op === "var"){
149+
var not_found = values[1] || null,
150+
sub_props = String(values[0]).split(".");
151+
for(var i = 0 ; i < sub_props.length ; i++){
152+
//Descending into data
153+
data = data[ sub_props[i] ];
154+
if(data === undefined){ return not_found; }
155+
}
156+
return data;
157+
}
158+
77159
if(undefined === operations[op]){
78160
throw new Error("Unrecognized operation " + op );
79161
}
80162

81-
//easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
82-
if(!Array.isArray(values)){ values = [values]; }
163+
return operations[op].apply({}, values);
83164

84-
//Recursion!
85-
values = values.map(function(val){ return jsonLogic(val, data); });
165+
};
86166

87-
return operations[op].apply({}, values);
167+
jsonLogic.uses_data = function(logic){
168+
var collection = [];
169+
170+
if( jsonLogic.is_logic(logic) ){
171+
var op = Object.keys(logic)[0],
172+
values = logic[op];
173+
174+
if( ! Array.isArray(values)){ values = [values]; }
175+
176+
if(op === "var"){
177+
//This doesn't cover the case where the arg to var is itself a rule.
178+
collection.push(values[0]);
179+
}else{
180+
//Recursion!
181+
values.map(function(val){
182+
collection.push.apply(collection, jsonLogic.uses_data(val) );
183+
});
184+
}
185+
}
186+
187+
return collection.unique();
88188
};
89189

90-
}(this));
190+
return jsonLogic;
91191

192+
}));

play.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ <h1>Test JsonLogic in your Browser</h1>
9696
data = $("#data").val() === "" ? null : JSON.parse($("#data").val()),
9797
output;
9898
try{
99-
output = jsonLogic(rule, data);
99+
output = jsonLogic.apply(rule, data);
100100
console.log(output);
101101
$("#message-compute").fadeOut();
102102
$("#output").text( JSON.stringify(output, null, 4) );

tests/tests.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,21 @@ QUnit.test( "Shared JsonLogic.com tests ", function( assert ){
1616
//Only waiting on the request() is async
1717
stop();
1818

19-
/*
2019
var fs = require('fs');
2120
fs.readFile('tests.json', 'utf8', function (error, body) {
22-
var response = { statusCode : 200 };
23-
*/
21+
/*
2422
var request = require('request');
2523
request('http://jsonlogic.com/tests.json', function (error, response, body) {
2624
if (error || response.statusCode != 200) {
2725
console.log("Failed to load tests from JsonLogic.com:", error, response.statusCode);
2826
start();
2927
return;
3028
}
31-
29+
*/
3230
try{
3331
tests = JSON.parse(body);
3432
}catch(e){
35-
console.log("Trouble parsing shared test: ", body);
36-
start();
37-
return;
33+
throw new Error("Trouble parsing shared test: " + e.message);
3834
}
3935

4036
console.log("Including "+tests.length+" shared tests from JsonLogic.com");

tests/tests.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
[
2+
"Too few args",
3+
[{"if":[]}, null, null],
4+
[{"if":[true]}, null, true],
5+
[{"if":[false]}, null, false],
6+
[{"if":["apple"]}, null, "apple"],
7+
8+
"Simple if/then/else cases",
9+
[{"if":[true, "apple"]}, null, "apple"],
10+
[{"if":[false, "apple"]}, null, null],
11+
[{"if":[true, "apple", "banana"]}, null, "apple"],
12+
[{"if":[false, "apple", "banana"]}, null, "banana"],
13+
14+
"Empty arrays are falsey",
15+
[{"if":[ [], "apple", "banana"]}, null, "banana"],
16+
[{"if":[ [1], "apple", "banana"]}, null, "apple"],
17+
[{"if":[ [1,2,3,4], "apple", "banana"]}, null, "apple"],
18+
19+
"Empty strings are falsey, all other strings are truthy",
20+
[{"if":[ "", "apple", "banana"]}, null, "banana"],
21+
[{"if":[ "zucchini", "apple", "banana"]}, null, "apple"],
22+
[{"if":[ "0", "apple", "banana"]}, null, "apple"],
23+
24+
"You can cast a string to numeric with a unary + ",
25+
[{"===":[0,"0"]}, null, false],
26+
[{"===":[0,{"+":"0"}]}, null, true],
27+
[{"if":[ {"+":"0"}, "apple", "banana"]}, null, "banana"],
28+
[{"if":[ {"+":"1"}, "apple", "banana"]}, null, "apple"],
29+
30+
"Zero is falsy, all other numbers are truthy",
31+
[{"if":[ 0, "apple", "banana"]}, null, "banana"],
32+
[{"if":[ 1, "apple", "banana"]}, null, "apple"],
33+
[{"if":[ 3.1416, "apple", "banana"]}, null, "apple"],
34+
[{"if":[ -1, "apple", "banana"]}, null, "apple"],
35+
36+
"If the conditional is logic, it gets evaluated",
37+
[{"if":[ {">":[2,1]}, "apple", "banana"]}, null, "apple"],
38+
[{"if":[ {">":[1,2]}, "apple", "banana"]}, null, "banana"],
39+
40+
"If the consequents are logic, they get evaluated",
41+
[{"if":[ true, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "apple"],
42+
[{"if":[ false, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "banana"],
43+
44+
"If/then/elseif/then cases",
45+
[{"if":[true, "apple", true, "banana"]}, null, "apple"],
46+
[{"if":[true, "apple", false, "banana"]}, null, "apple"],
47+
[{"if":[false, "apple", true, "banana"]}, null, "banana"],
48+
[{"if":[false, "apple", false, "banana"]}, null, null],
49+
50+
[{"if":[true, "apple", true, "banana", "carrot"]}, null, "apple"],
51+
[{"if":[true, "apple", false, "banana", "carrot"]}, null, "apple"],
52+
[{"if":[false, "apple", true, "banana", "carrot"]}, null, "banana"],
53+
[{"if":[false, "apple", false, "banana", "carrot"]}, null, "carrot"],
54+
55+
[{"if":[false, "apple", false, "banana", false, "carrot"]}, null, null],
56+
[{"if":[false, "apple", false, "banana", false, "carrot", "date"]}, null, "date"],
57+
[{"if":[false, "apple", false, "banana", true, "carrot", "date"]}, null, "carrot"],
58+
[{"if":[false, "apple", true, "banana", false, "carrot", "date"]}, null, "banana"],
59+
[{"if":[false, "apple", true, "banana", true, "carrot", "date"]}, null, "banana"],
60+
[{"if":[true, "apple", false, "banana", false, "carrot", "date"]}, null, "apple"],
61+
[{"if":[true, "apple", false, "banana", true, "carrot", "date"]}, null, "apple"],
62+
[{"if":[true, "apple", true, "banana", false, "carrot", "date"]}, null, "apple"],
63+
[{"if":[true, "apple", true, "banana", true, "carrot", "date"]}, null, "apple"]
64+
65+
]

0 commit comments

Comments
 (0)