diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..12bf434 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": "google", + + "rules": { + "no-var": "off", + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double", { + "avoidEscape": true + } + ], + "semi": [ + "error", + "always" + ], + "max-len": [ + "warn", { + "ignoreComments": true + } + ], + "prefer-spread": ["off"], + "prefer-rest-params": ["off"], + "camelcase" : ["off"] + } +} diff --git a/logic.js b/logic.js index aeff558..245e447 100644 --- a/logic.js +++ b/logic.js @@ -1,267 +1,316 @@ +/* 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) { +;(function(root, factory) { + if (typeof define === "function" && define.amd) { define(factory); - } else if (typeof exports === 'object') { + } 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){ - Array.prototype.unique = function() { - var a = []; - for (var i=0, l=this.length; i" : 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, - sub_props = String(a).split("."), - 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 = [], - keys = Array.isArray(arguments[0]) ? arguments[0] : arguments ; - - for(var i = 0 ; i < keys.length ; i++){ - var key = keys[i], - 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); +}(this, function() { + "use strict"; + /* globals console:false */ + + if ( ! Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === "[object Array]"; + }; } -}; - -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. + if( ! Array.unique) { + /* eslint-disable no-extend-native */ + Array.prototype.unique = function() { + var a = []; + for (var i=0, l=this.length; i": 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); + } + } -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; - } + 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); - data = data || {}; + 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; + } - var op = Object.keys(logic)[0], - values = logic[op], - i, current; + data = data || {}; - //easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} - if( ! Array.isArray(values)){ values = [values]; } + var op = Object.keys(logic)[0]; + var values = logic[op]; + var i; + var current; - // '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 }; + // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} + if( ! Array.isArray(values)) { + values = [values]; + } - 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){ + // '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)){ + 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){ + 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) ){ + if( jsonLogic.truthy(current) ) { return current; } } - return current; //Last - } - + return current; // Last + } - // Everyone else gets immediate depth-first recursion - values = values.map(function(val){ return jsonLogic.apply(val, data); }); + // 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('.'), - 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('.') + ")"); + 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); -}; + return operation.apply(data, values); + }else{ + throw new Error("Unrecognized operation " + op ); + } -jsonLogic.uses_data = function(logic){ - var collection = []; + // 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); + }; - if( jsonLogic.is_logic(logic) ){ - var op = Object.keys(logic)[0], - values = logic[op]; + jsonLogic.uses_data = function(logic) { + var collection = []; - if( ! Array.isArray(values)){ values = [values]; } + if( jsonLogic.is_logic(logic) ) { + var op = Object.keys(logic)[0]; + var values = logic[op]; - 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) ); - }); - } - } + if( ! Array.isArray(values)) { + values = [values]; + } - return collection.unique(); -}; + 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) ); + }); + } + } -jsonLogic.add_operation = function (name, code){ - operations[name] = code; -}; + return collection.unique(); + }; -return jsonLogic; + jsonLogic.add_operation = function(name, code) { + operations[name] = code; + }; + return jsonLogic; })); diff --git a/package.json b/package.json index c080a63..d0ab179 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ }, "dependencies": {}, "devDependencies": { + "eslint": "^3.9.1", + "eslint-config-google": "^0.7.0", "gulp": "^3.9.0", "qunit": "^0.7.7", "request": "^2.65.0" diff --git a/tests/tests.js b/tests/tests.js index 8eda6f6..1ad9fb9 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -1,83 +1,86 @@ -var jsonLogic = require('../logic.js'), - http = require('http'), - fs = require('fs'); +var jsonLogic = require("../logic.js"); +var http = require("http"); +var fs = require("fs"); var download = function(url, dest, cb) { var file = fs.createWriteStream(dest); - var request = http.get(url, function(response) { + http.get(url, function(response) { response.pipe(file); - file.on('finish', function() { + file.on("finish", function() { file.close(cb); // close() is async, call cb after close completes. }); - }).on('error', function(err) { // Handle errors + }).on("error", function(err) { // Handle errors fs.unlink(dest); // Delete the file async. (But we don't check the result) if (cb) cb(err.message); }); }; -var process_test_file = function (filename){ - fs.readFile(filename, 'utf8', function (error, body) { - try{ - tests = JSON.parse(body); - }catch(e){ - throw new Error("Trouble parsing shared test: " + e.message); - } +var process_test_file = function(filename) { + fs.readFile(filename, "utf8", function(error, body) { + try{ + tests = JSON.parse(body); + }catch(e) { + throw new Error("Trouble parsing shared test: " + e.message); + } - console.log("Including "+tests.length+" shared tests from JsonLogic.com"); + console.log("Including "+tests.length+" shared tests from JsonLogic.com"); - for(var i = 0 ; i < tests.length ; i+=1){ - var test = tests[i]; - if(typeof test === "string") continue; //Comment + for(var i = 0; i < tests.length; i+=1) { + var test = tests[i]; + if(typeof test === "string") continue; // Comment - var rule = test[0], - data = test[1], - expected = test[2]; + var rule = test[0]; + var data = test[1]; + var expected = test[2]; - assert.deepEqual( + assert.deepEqual( jsonLogic.apply(rule, data), expected, - "jsonLogic("+ JSON.stringify(rule) +","+ JSON.stringify( data ) +") = " + JSON.stringify(expected) + "jsonLogic("+ JSON.stringify(rule) +"," + + JSON.stringify( data ) +") = " + + JSON.stringify(expected) ); - } + } - start(); - }); + start(); + }); }; QUnit.test( "Bad operator", function( assert ) { - assert.throws( - function(){ jsonLogic.apply({"fubar": []}); }, + assert.throws( + function() { + jsonLogic.apply({"fubar": []}); +}, /Unrecognized operation/ ); }); +QUnit.test( "Shared JsonLogic.com tests ", function( assert ) { + // Only waiting on the request() is async + stop(); -QUnit.test( "Shared JsonLogic.com tests ", function( assert ){ - //Only waiting on the request() is async - stop(); - - var local_file = 'tests.json'; - - fs.stat(local_file, function(err, stats){ - if(err){ - console.log("Downloading shared tests from JsonLogic.com"); - download('http://jsonlogic.com/tests.json', local_file, function(){ - process_test_file(local_file); - }); - }else{ - console.log("Using cached tests"); - process_test_file(local_file); - } - }); - + var local_file = "tests.json"; + fs.stat(local_file, function(err, stats) { + if(err) { + console.log("Downloading shared tests from JsonLogic.com"); + download("http://jsonlogic.com/tests.json", local_file, function() { + process_test_file(local_file); + }); + }else{ + console.log("Using cached tests"); + process_test_file(local_file); + } + }); }); -var real_console = console.log, last_console; -console.log = function(logged){ last_console = logged; real_console.apply(this, arguments); }; QUnit.test( "logging", function( assert ) { - assert.equal( jsonLogic.apply({"log" : [1]}), 1 ); + var last_console; + console.log = function(logged) { + last_console = logged; + }; + assert.equal( jsonLogic.apply({"log": [1]}), 1 ); assert.equal( last_console, 1 ); }); @@ -86,162 +89,184 @@ QUnit.test( "edge cases", function( assert ) { }); QUnit.test( "Expanding functionality with add_operator", function( assert) { - //Operator is not yet defined - assert.throws( - function(){ jsonLogic.apply({"add_to_a": []}); }, + // Operator is not yet defined + assert.throws( + function() { + jsonLogic.apply({"add_to_a": []}); +}, /Unrecognized operation/ ); - //Set up some outside data, and build a basic function operator - var a = 0, - add_to_a = function(b){ if(b === undefined){b=1;} return a += b; }; - jsonLogic.add_operation("add_to_a", add_to_a); - //New operation executes, returns desired result - //No args - assert.equal( jsonLogic.apply({"add_to_a" : []}), 1 ); - //Unary syntactic sugar - assert.equal( jsonLogic.apply({"add_to_a" : 41}), 42 ); - //New operation had side effects. - assert.equal(a, 42); - - var fives = { - add : function(i){ return i + 5; }, - subtract : function(i){ return i - 5; } - }; - - jsonLogic.add_operation("fives", fives); - assert.equal( jsonLogic.apply({"fives.add" : 37}), 42 ); - assert.equal( jsonLogic.apply({"fives.subtract" : [47] }), 42 ); - - //Calling a method with multiple var as arguments. - jsonLogic.add_operation("times", function(a,b){ return a*b; }); - assert.equal( + // Set up some outside data, and build a basic function operator + var a = 0; + var add_to_a = function(b) { + if(b === undefined) { + b=1; + } return a += b; + }; + jsonLogic.add_operation("add_to_a", add_to_a); + // New operation executes, returns desired result + // No args + assert.equal( jsonLogic.apply({"add_to_a": []}), 1 ); + // Unary syntactic sugar + assert.equal( jsonLogic.apply({"add_to_a": 41}), 42 ); + // New operation had side effects. + assert.equal(a, 42); + + var fives = { + add: function(i) { + return i + 5; + }, + subtract: function(i) { + return i - 5; + }, + }; + + jsonLogic.add_operation("fives", fives); + assert.equal( jsonLogic.apply({"fives.add": 37}), 42 ); + assert.equal( jsonLogic.apply({"fives.subtract": [47]}), 42 ); + + // Calling a method with multiple var as arguments. + jsonLogic.add_operation("times", function(a, b) { + return a*b; + }); + assert.equal( jsonLogic.apply( - {"times" : [{"var":"a"}, {"var":"b"}]}, - {a:6,b:7} + {"times": [{"var": "a"}, {"var": "b"}]}, + {a: 6, b: 7} ), 42 ); - //Calling a method that takes an array, but the inside of the array has rules, too - jsonLogic.add_operation("array_times", function(a){ return a[0]*a[1]; }); - assert.equal( + // Calling a method that takes an array, but the inside of the array has rules, too + jsonLogic.add_operation("array_times", function(a) { + return a[0]*a[1]; + }); + assert.equal( jsonLogic.apply( - {"array_times" : [[{"var":"a"}, {"var":"b"}]]}, - {a:6,b:7} + {"array_times": [[{"var": "a"}, {"var": "b"}]]}, + {a: 6, b: 7} ), 42 ); - }); QUnit.test( "Expanding functionality with method", function( assert) { - - //Data contains a real object with methods and local state - var a = { - count : 0, - increment : function(){ return this.count += 1; }, - add : function(b){ return this.count += b; }, - }; - - //Look up "a" in data, and run the increment method on it with no args. - assert.equal( + // Data contains a real object with methods and local state + var a = { + count: 0, + increment: function() { + return this.count += 1; + }, + add: function(b) { + return this.count += b; + }, + }; + + // Look up "a" in data, and run the increment method on it with no args. + assert.equal( jsonLogic.apply( - {"method" : [ {"var" : "a"}, "increment" ]}, - {"a" : a} + {"method": [{"var": "a"}, "increment"]}, + {"a": a} ), - 1 //Happy return value + 1 // Happy return value ); - assert.equal(a.count, 1); //Happy state change + assert.equal(a.count, 1); // Happy state change - //Run the add method with an argument - assert.equal( + // Run the add method with an argument + assert.equal( jsonLogic.apply( - {"method" : [ {"var" : "a"}, "add", [41] ]}, - {"a" : a} + {"method": [{"var": "a"}, "add", [41]]}, + {"a": a} ), - 42 //Happy return value + 42 // Happy return value ); - assert.equal(a.count, 42); //Happy state change - + assert.equal(a.count, 42); // Happy state change }); -QUnit.test("Control structures don't use depth-first computation", function(assert){ - //Depth-first recursion was wasteful but not harmful until we added custom operations that could have side-effects. - - //If operations run the condition, if truthy, it runs and returns that consequent. - //Consequents of falsy conditions should not run. - //After one truthy condition, no other condition should run - var conditions = []; - var consequents = []; - jsonLogic.add_operation("push.if", function(v){ conditions.push(v); return v; }); - jsonLogic.add_operation("push.then", function(v){ consequents.push(v); return v; }); - jsonLogic.add_operation("push.else", function(v){ consequents.push(v); return v; }); - - jsonLogic.apply({"if":[ - {"push.if" : [true] }, - {"push.then":["first"]}, - {"push.if" : [false]}, - {"push.then":["second"]}, - {"push.else":["third"]} - ]}); - assert.deepEqual(conditions, [true]); - assert.deepEqual(consequents, ["first"]); - - conditions = []; - consequents = []; - jsonLogic.apply({"if":[ - {"push.if" : [false] }, - {"push.then":["first"]}, - {"push.if" : [true]}, - {"push.then":["second"]}, - {"push.else":["third"]} - ]}); - assert.deepEqual(conditions, [false,true]); - assert.deepEqual(consequents, ["second"]); - - conditions = []; - consequents = []; - jsonLogic.apply({"if":[ - {"push.if" : [false] }, - {"push.then":["first"]}, - {"push.if" : [false]}, - {"push.then":["second"]}, - {"push.else":["third"]} - ]}); - assert.deepEqual(conditions, [false,false]); - assert.deepEqual(consequents, ["third"]); - - - jsonLogic.add_operation("push", function(arg){ i.push(arg); return arg; }); - var i = []; - - i = []; - jsonLogic.apply({"and":[ {"push":[false]}, {"push":[false]} ]}); - assert.deepEqual(i, [false]); - i = []; - jsonLogic.apply({"and":[ {"push":[false]}, {"push":[true]} ]}); - assert.deepEqual(i, [false]); - i = []; - jsonLogic.apply({"and":[ {"push":[true]}, {"push":[false]} ]}); - assert.deepEqual(i, [true, false]); - i = []; - jsonLogic.apply({"and":[ {"push":[true]}, {"push":[true]} ]}); - assert.deepEqual(i, [true, true]); - - - i = []; - jsonLogic.apply({"or":[ {"push":[false]}, {"push":[false]} ]}); - assert.deepEqual(i, [false,false]); - i = []; - jsonLogic.apply({"or":[ {"push":[false]}, {"push":[true]} ]}); - assert.deepEqual(i, [false,true]); - i = []; - jsonLogic.apply({"or":[ {"push":[true]}, {"push":[false]} ]}); - assert.deepEqual(i, [true]); - i = []; - jsonLogic.apply({"or":[ {"push":[true]}, {"push":[true]} ]}); - assert.deepEqual(i, [true]); +QUnit.test("Control structures don't eval depth-first", function(assert) { + // Depth-first recursion was wasteful but not harmful until we added custom operations that could have side-effects. + + // If operations run the condition, if truthy, it runs and returns that consequent. + // Consequents of falsy conditions should not run. + // After one truthy condition, no other condition should run + var conditions = []; + var consequents = []; + jsonLogic.add_operation("push.if", function(v) { + conditions.push(v); return v; + }); + jsonLogic.add_operation("push.then", function(v) { + consequents.push(v); return v; + }); + jsonLogic.add_operation("push.else", function(v) { + consequents.push(v); return v; + }); + jsonLogic.apply({"if": [ + {"push.if": [true]}, + {"push.then": ["first"]}, + {"push.if": [false]}, + {"push.then": ["second"]}, + {"push.else": ["third"]}, + ]}); + assert.deepEqual(conditions, [true]); + assert.deepEqual(consequents, ["first"]); + + conditions = []; + consequents = []; + jsonLogic.apply({"if": [ + {"push.if": [false]}, + {"push.then": ["first"]}, + {"push.if": [true]}, + {"push.then": ["second"]}, + {"push.else": ["third"]}, + ]}); + assert.deepEqual(conditions, [false, true]); + assert.deepEqual(consequents, ["second"]); + + conditions = []; + consequents = []; + jsonLogic.apply({"if": [ + {"push.if": [false]}, + {"push.then": ["first"]}, + {"push.if": [false]}, + {"push.then": ["second"]}, + {"push.else": ["third"]}, + ]}); + assert.deepEqual(conditions, [false, false]); + assert.deepEqual(consequents, ["third"]); + + + jsonLogic.add_operation("push", function(arg) { + i.push(arg); return arg; + }); + var i = []; + + i = []; + jsonLogic.apply({"and": [{"push": [false]}, {"push": [false]}]}); + assert.deepEqual(i, [false]); + i = []; + jsonLogic.apply({"and": [{"push": [false]}, {"push": [true]}]}); + assert.deepEqual(i, [false]); + i = []; + jsonLogic.apply({"and": [{"push": [true]}, {"push": [false]}]}); + assert.deepEqual(i, [true, false]); + i = []; + jsonLogic.apply({"and": [{"push": [true]}, {"push": [true]}]}); + assert.deepEqual(i, [true, true]); + + + i = []; + jsonLogic.apply({"or": [{"push": [false]}, {"push": [false]}]}); + assert.deepEqual(i, [false, false]); + i = []; + jsonLogic.apply({"or": [{"push": [false]}, {"push": [true]}]}); + assert.deepEqual(i, [false, true]); + i = []; + jsonLogic.apply({"or": [{"push": [true]}, {"push": [false]}]}); + assert.deepEqual(i, [true]); + i = []; + jsonLogic.apply({"or": [{"push": [true]}, {"push": [true]}]}); + assert.deepEqual(i, [true]); });