diff --git a/logic.js b/logic.js index 6b827df..2ac983a 100644 --- a/logic.js +++ b/logic.js @@ -3,7 +3,8 @@ Using a Universal Module Loader that should be browser, require, and AMD friendly http://ricostacruz.com/cheatsheets/umdjs.html */ -;(function(root, factory) { +; +(function(root, factory) { if (typeof define === "function" && define.amd) { define(factory); } else if (typeof exports === "object") { @@ -15,7 +16,7 @@ http://ricostacruz.com/cheatsheets/umdjs.html "use strict"; /* globals console:false */ - if ( ! Array.isArray) { + if (!Array.isArray) { Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === "[object Array]"; }; @@ -28,7 +29,7 @@ http://ricostacruz.com/cheatsheets/umdjs.html */ function arrayUnique(array) { var a = []; - for (var i=0, l=array.length; i= need_count) { + if (options.length - are_missing.length >= need_count) { return []; - }else{ + } else { return are_missing; } }, @@ -176,12 +182,25 @@ http://ricostacruz.com/cheatsheets/umdjs.html }; + // For a better is_logic detection + var asymetricOperationsNames = [ + "if", "?:", "and", "or", + "filter", "map", "reduce", "all", "none", "some", + "@", + ]; + jsonLogic.is_logic = function(logic) { return ( typeof logic === "object" && // An object logic !== null && // but not null - ! Array.isArray(logic) && // and not an array - Object.keys(logic).length === 1 // with exactly one key + !Array.isArray(logic) && // and not an array + Object.keys(logic).length === 1 && // with exactly one key + + ( + typeof operations[Object.keys(logic)[0]] !== "undefined" || + typeof operations[Object.keys(logic)[0].split(".")[0]] !== "undefined" || + asymetricOperationsNames.indexOf(Object.keys(logic)[0]) !== -1 + ) ); }; @@ -191,10 +210,10 @@ http://ricostacruz.com/cheatsheets/umdjs.html Spec and rationale here: http://jsonlogic.com/truthy */ jsonLogic.truthy = function(value) { - if(Array.isArray(value) && value.length === 0) { + if (Array.isArray(value) && value.length === 0) { return false; } - return !! value; + return !!value; }; @@ -206,15 +225,16 @@ http://ricostacruz.com/cheatsheets/umdjs.html return logic[jsonLogic.get_operator(logic)]; }; - jsonLogic.apply = function(logic, data) { + jsonLogic.apply = function(logic, data, internal) { // Does this array contain logic? Only one way to find out. - if(Array.isArray(logic)) { + if (Array.isArray(logic)) { return logic.map(function(l) { - return jsonLogic.apply(l, data); + return jsonLogic.apply(l, data, true); }); } // You've recursed to a primitive, stop! - if( ! jsonLogic.is_logic(logic) ) { + // If your are called from outside + if (!jsonLogic.is_logic(logic)) { return logic; } @@ -224,15 +244,18 @@ http://ricostacruz.com/cheatsheets/umdjs.html var values = logic[op]; var i; var current; - var scopedLogic, scopedData, filtered, initial; + var scopedLogic; + var scopedData; + var filtered; + var initial; // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} - if( ! Array.isArray(values)) { + 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 (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 }; @@ -246,96 +269,95 @@ http://ricostacruz.com/cheatsheets/umdjs.html 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); + 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); + 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) { + } 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) { + } 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 - - - - - }else if(op === 'filter'){ + } else if (op === "filter") { scopedData = jsonLogic.apply(values[0], data); scopedLogic = values[1]; - if ( ! Array.isArray(scopedData)) { - return []; + if (!Array.isArray(scopedData)) { + return []; } // Return only the elements from the array in the first argument, // that return truthy when passed to the logic in the second argument. // For parity with JavaScript, reindex the returned array - return scopedData.filter(function(datum){ - return jsonLogic.truthy( jsonLogic.apply(scopedLogic, datum)); + return scopedData.filter(function(datum) { + return jsonLogic.truthy(jsonLogic.apply(scopedLogic, datum)); }); - }else if(op === 'map'){ + } else if (op === "map") { scopedData = jsonLogic.apply(values[0], data); scopedLogic = values[1]; - if ( ! Array.isArray(scopedData)) { - return []; + if (!Array.isArray(scopedData)) { + return []; } - return scopedData.map(function(datum){ - return jsonLogic.apply(scopedLogic, datum); + return scopedData.map(function(datum) { + return jsonLogic.apply(scopedLogic, datum); }); - - }else if(op === 'reduce'){ + } else if (op === "reduce") { scopedData = jsonLogic.apply(values[0], data); scopedLogic = values[1]; - initial = typeof values[2] !== 'undefined' ? values[2] : null; + initial = typeof values[2] !== "undefined" ? values[2] : null; - if ( ! Array.isArray(scopedData)) { - return initial; + if (!Array.isArray(scopedData)) { + return initial; } return scopedData.reduce( - function(accumulator, current){ - return jsonLogic.apply( - scopedLogic, - {'current':current, 'accumulator':accumulator} - ); - }, - initial + function(accumulator, current) { + return jsonLogic.apply( + scopedLogic, { + "current": current, + "accumulator": accumulator, + } + ); + }, + initial ); - - }else if(op === "all") { + } else if (op === "all") { scopedData = jsonLogic.apply(values[0], data); scopedLogic = values[1]; // All of an empty set is false. Note, some and none have correct fallback after the for loop - if( ! scopedData.length) { + if (!scopedData.length) { return false; } - for(i=0; i < scopedData.length; i+=1) { - if( ! jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) { + for (i = 0; i < scopedData.length; i += 1) { + if (!jsonLogic.truthy(jsonLogic.apply(scopedLogic, scopedData[i]))) { return false; // First falsy, short circuit } } return true; // All were truthy - }else if(op === "none") { - filtered = jsonLogic.apply({'filter' : values}, data); + } else if (op === "none") { + filtered = jsonLogic.apply({ + "filter": values, + }, data); return filtered.length === 0; - - }else if(op === "some") { - filtered = jsonLogic.apply({'filter' : values}, data); + } else if (op === "some") { + filtered = jsonLogic.apply({ + "filter": values, + }, data); return filtered.length > 0; } @@ -348,44 +370,44 @@ http://ricostacruz.com/cheatsheets/umdjs.html // 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 - if(typeof operations[op] === "function") { + 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 + } 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++) { + for (i = 0; i < sub_ops.length; i++) { // Descending into operations operation = operation[sub_ops[i]]; - if(operation === undefined) { + if (operation === undefined) { throw new Error("Unrecognized operation " + op + - " (failed at " + sub_ops.slice(0, i+1).join(".") + ")"); + " (failed at " + sub_ops.slice(0, i + 1).join(".") + ")"); } } return operation.apply(data, values); } - throw new Error("Unrecognized operation " + op ); + throw new Error("Unrecognized operation " + op); }; jsonLogic.uses_data = function(logic) { var collection = []; - if( jsonLogic.is_logic(logic) ) { + if (jsonLogic.is_logic(logic)) { var op = jsonLogic.get_operator(logic); var values = logic[op]; - if( ! Array.isArray(values)) { + if (!Array.isArray(values)) { values = [values]; } - if(op === "var") { + if (op === "var") { // This doesn't cover the case where the arg to var is itself a rule. collection.push(values[0]); - }else{ + } else { // Recursion! values.map(function(val) { - collection.push.apply(collection, jsonLogic.uses_data(val) ); + collection.push.apply(collection, jsonLogic.uses_data(val)); }); } } @@ -403,30 +425,30 @@ http://ricostacruz.com/cheatsheets/umdjs.html jsonLogic.rule_like = function(rule, pattern) { // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?"); - if(pattern === rule) { + if (pattern === rule) { return true; } // TODO : Deep object equivalency? - if(pattern === "@") { + if (pattern === "@") { return true; } // Wildcard! - if(pattern === "number") { + if (pattern === "number") { return (typeof rule === "number"); } - if(pattern === "string") { + if (pattern === "string") { return (typeof rule === "string"); } - if(pattern === "array") { + if (pattern === "array") { // !logic test might be superfluous in JavaScript - return Array.isArray(rule) && ! jsonLogic.is_logic(rule); + return Array.isArray(rule) && !jsonLogic.is_logic(rule); } - if(jsonLogic.is_logic(pattern)) { - if(jsonLogic.is_logic(rule)) { + if (jsonLogic.is_logic(pattern)) { + if (jsonLogic.is_logic(rule)) { var pattern_op = jsonLogic.get_operator(pattern); var rule_op = jsonLogic.get_operator(rule); - if(pattern_op === "@" || pattern_op === rule_op) { - // echo "\nOperators match, go deeper\n"; + if (pattern_op === "@" || pattern_op === rule_op) { + // echo "\nOperators match, go deeper\n"; return jsonLogic.rule_like( jsonLogic.get_values(rule, false), jsonLogic.get_values(pattern, false) @@ -436,22 +458,22 @@ http://ricostacruz.com/cheatsheets/umdjs.html return false; // pattern is logic, rule isn't, can't be eq } - if(Array.isArray(pattern)) { - if(Array.isArray(rule)) { - if(pattern.length !== rule.length) { + if (Array.isArray(pattern)) { + if (Array.isArray(rule)) { + if (pattern.length !== rule.length) { return false; } /* Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT) */ - for(var i = 0; i < pattern.length; i += 1) { + for (var i = 0; i < pattern.length; i += 1) { // If any fail, we fail - if( ! jsonLogic.rule_like(rule[i], pattern[i])) { + if (!jsonLogic.rule_like(rule[i], pattern[i])) { return false; } } return true; // If they *all* passed, we pass - }else{ + } else { return false; // Pattern is array, rule isn't } } diff --git a/tests/tests.js b/tests/tests.js index b201bf6..e25c468 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -15,7 +15,7 @@ var download = function(url, dest, cb) { }); }; -var remote_or_cache = function (remote_url, local_file, description, runner){ +var remote_or_cache = function(remote_url, local_file, description, runner){ var parse_and_iterate = function(local_file, description, runner){ fs.readFile(local_file, "utf8", function(error, body) { var tests; @@ -36,7 +36,6 @@ var remote_or_cache = function (remote_url, local_file, description, runner){ start(); }); - }; // Only waiting on the request() is async @@ -53,14 +52,13 @@ var remote_or_cache = function (remote_url, local_file, description, runner){ parse_and_iterate(local_file, description, runner); } }); - }; remote_or_cache( "http://jsonlogic.com/tests.json", "tests.json", "applies() tests", - function(test){ + function(test) { var rule = test[0]; var data = test[1]; var expected = test[2]; @@ -79,7 +77,7 @@ remote_or_cache( "http://jsonlogic.com/rule_like.json", "rule_like.json", "rule_like() tests", - function(test){ + function(test) { var rule = test[0]; var pattern = test[1]; var expected = test[2]; @@ -95,16 +93,10 @@ remote_or_cache( ); - - - - -QUnit.test( "Bad operator", function( assert ) { - assert.throws( - function() { - jsonLogic.apply({"fubar": []}); - }, - /Unrecognized operation/ +QUnit.test( "Bad operator should be trated as data", function( assert ) { + assert.deepEqual( + jsonLogic.apply({"fubar": []}), + {"fubar": []} ); }); @@ -123,12 +115,10 @@ 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": []}); - }, - /Unrecognized operation/ + // Operator is not yet defined, so it's considered data + assert.deepEqual( + jsonLogic.apply({"add_to_a": []}), + {"add_to_a": []} ); // Set up some outside data, and build a basic function operator @@ -172,14 +162,12 @@ QUnit.test( "Expanding functionality with add_operator", function( assert) { 42 ); - //Remove operation: + // Remove operation: jsonLogic.rm_operation("times"); - assert.throws( - function() { - jsonLogic.apply({"times": [2,2]}); - }, - /Unrecognized operation/ + assert.deepEqual( + jsonLogic.apply({"times": [2, 2]}), + {"times": [2, 2]} ); // Calling a method that takes an array, but the inside of the array has rules, too @@ -193,9 +181,6 @@ QUnit.test( "Expanding functionality with add_operator", function( assert) { ), 42 ); - - - }); QUnit.test( "Expanding functionality with method", function( assert) { @@ -317,3 +302,11 @@ QUnit.test("Control structures don't eval depth-first", function(assert) { jsonLogic.apply({"or": [{"push": [true]}, {"push": [true]}]}); assert.deepEqual(i, [true]); }); + + +QUnit.test("Handle correctly single item objects", function(assert) { + assert.deepEqual( + jsonLogic.apply({"if": [true, {"item": 10}, "no"]}), + {"item": 10} + ); +});