|
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 () { |
2 | 16 | 'use strict'; |
3 | 17 | /*globals console:false */ |
4 | 18 |
|
5 | | -if (!Array.isArray) { |
| 19 | +if ( ! Array.isArray) { |
6 | 20 | Array.isArray = function(arg) { |
7 | 21 | return Object.prototype.toString.call(arg) === '[object Array]'; |
8 | 22 | }; |
9 | 23 | } |
10 | 24 |
|
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){ |
12 | 101 | //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; |
15 | 104 | } |
16 | 105 |
|
17 | 106 | data = data || {}; |
18 | 107 |
|
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(); |
76 | 133 |
|
| 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 | + |
77 | 159 | if(undefined === operations[op]){ |
78 | 160 | throw new Error("Unrecognized operation " + op ); |
79 | 161 | } |
80 | 162 |
|
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); |
83 | 164 |
|
84 | | - //Recursion! |
85 | | - values = values.map(function(val){ return jsonLogic(val, data); }); |
| 165 | +}; |
86 | 166 |
|
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(); |
88 | 188 | }; |
89 | 189 |
|
90 | | -}(this)); |
| 190 | +return jsonLogic; |
91 | 191 |
|
| 192 | +})); |
0 commit comments