From e98ce4250632798fac0f297edd2d84653bbe0354 Mon Sep 17 00:00:00 2001 From: Iddan Aharonson Date: Sat, 19 May 2018 00:05:44 +0300 Subject: [PATCH 1/3] - ES6 syntax - Exposing default_operations - Modulised & Bundled - Smarter state management & Exposed class - Using unpkg for bundle download link - Providing .mjs file for tree shaking tools --- .babelrc | 10 + .eslintrc.json | 32 +- .gitignore | 3 + README.md | 4 +- bower.json | 15 +- gulpfile.js | 23 +- logic.js | 464 ------ package.json | 13 +- play.html | 2 +- rollup.config.js | 23 + src/default-operations.js | 146 ++ src/is-array-polyfill.js | 5 + src/logic.js | 302 ++++ src/util.js | 14 + tests/testrunner.js | 23 +- tests/tests.js | 234 ++- yarn.lock | 3068 +++++++++++++++++++++++++++++++++++++ 17 files changed, 3743 insertions(+), 638 deletions(-) create mode 100644 .babelrc delete mode 100644 logic.js create mode 100644 rollup.config.js create mode 100644 src/default-operations.js create mode 100644 src/is-array-polyfill.js create mode 100644 src/logic.js create mode 100644 src/util.js create mode 100644 yarn.lock diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..8c0cc47 --- /dev/null +++ b/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@babel/env", + { + "modules": false + } + ] + ] +} diff --git a/.eslintrc.json b/.eslintrc.json index 12bf434..d494dac 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,33 +1,33 @@ { "extends": "google", - + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true + } + }, "rules": { "no-var": "off", - "indent": [ - "error", - 2 - ], - "linebreak-style": [ - "error", - "unix" - ], + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], "quotes": [ "error", - "double", { + "double", + { "avoidEscape": true } ], - "semi": [ - "error", - "always" - ], + "semi": ["error", "always"], "max-len": [ - "warn", { + "warn", + { "ignoreComments": true } ], "prefer-spread": ["off"], "prefer-rest-params": ["off"], - "camelcase" : ["off"] + "camelcase": ["off"] } } diff --git a/.gitignore b/.gitignore index ab3a2e7..0814b0a 100755 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ tests/*.json # editor and IDE remnants *~ .idea/ + +# Bundled +dist/ diff --git a/README.md b/README.md index cf86a90..aeef1d4 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ npm install json-logic-js Note that this project uses a [module loader](http://ricostacruz.com/cheatsheets/umdjs.html) that also makes it suitable for RequireJS projects. -If that doesn't suit you, and you want to manage updates yourself, the entire library is self-contained in `logic.js` and you can download it straight into your project as you see fit. +If that doesn't suit you, and you want to manage updates yourself, the entire library is self-contained and you can download it straight into your project as you see fit. ```bash -curl -O https://raw.githubusercontent.com/jwadhams/json-logic-js/master/logic.js +curl -O https://unpkg.com/json-logic-js@1.2.2 ``` ## Examples diff --git a/bower.json b/bower.json index b7842d1..49cca6f 100644 --- a/bower.json +++ b/bower.json @@ -2,18 +2,11 @@ "name": "json-logic-js", "version": "1.2.2", "homepage": "https://github.com/jwadhams/json-logic-js", - "authors": [ - "Jeremy Wadhams " - ], + "authors": ["Jeremy Wadhams "], "description": "Serialize complex logic in JSON, run it in JavaScript", - "main": "logic.js", - "moduleType": [ - "globals" - ], - "keywords": [ - "json", - "logic" - ], + "main": "dist/logic.js", + "moduleType": ["globals"], + "keywords": ["json", "logic"], "license": "MIT", "private": false, "ignore": [ diff --git a/gulpfile.js b/gulpfile.js index 75c7add..1a5219b 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,18 +1,23 @@ var gulp = require("gulp"); var exec = require("child_process").exec; +gulp.task("build", function(cb) { + exec("npm run build", function(err, stdout, stderr) { + console.log(stdout); + console.log(stderr); + cb(err); + }); +}); + gulp.task("test", function(cb) { - exec( - "node testrunner.js", - {cwd: "tests"}, - function(err, stdout, stderr) { - console.log(stdout); - console.log(stderr); - cb(err); - } - ); + exec("node testrunner.js", {cwd: "tests"}, function(err, stdout, stderr) { + console.log(stdout); + console.log(stderr); + cb(err); + }); }); gulp.task("default", function() { + gulp.watch(["src/*.js"], ["build"]); gulp.watch(["**/*.js", "tests/tests.json"], ["test"]); }); diff --git a/logic.js b/logic.js deleted file mode 100644 index 6b827df..0000000 --- a/logic.js +++ /dev/null @@ -1,464 +0,0 @@ -/* 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) { - define(factory); - } 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]"; - }; - } - - /** - * Return an array that contains no duplicates (original not modified) - * @param {array} array Original reference array - * @return {array} New array with no duplicates - */ - function arrayUnique(array) { - var a = []; - for (var i=0, l=array.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(!b || typeof b.indexOf === "undefined") return false; - return (b.indexOf(a) !== -1); - }, - "cat": function() { - return Array.prototype.join.call(arguments, ""); - }, - "substr":function(source, start, end) { - if(end < 0){ - // JavaScript doesn't support negative end, this emulates PHP behavior - var temp = String(source).substr(start); - return temp.substr(0, temp.length + end); - } - return String(source).substr(start, end); - }, - "+": 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) { - 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 data = this; - if(typeof a === "undefined" || a==="" || a===null) { - return data; - } - var sub_props = String(a).split("."); - for(var i = 0; i < sub_props.length; i++) { - if(data === null) { - return not_found; - } - // 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); - } - } - - 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); - }, - - }; - - 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 - ); - }; - - /* - 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. - - Spec and rationale here: http://jsonlogic.com/truthy - */ - jsonLogic.truthy = function(value) { - if(Array.isArray(value) && value.length === 0) { - return false; - } - return !! value; - }; - - - jsonLogic.get_operator = function(logic) { - return Object.keys(logic)[0]; - }; - - jsonLogic.get_values = function(logic) { - return logic[jsonLogic.get_operator(logic)]; - }; - - 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; - } - - data = data || {}; - - var op = jsonLogic.get_operator(logic); - var values = logic[op]; - var i; - var current; - var scopedLogic, scopedData, filtered, initial; - - // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} - 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' 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)) { - return current; - } - } - 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) ) { - return current; - } - } - return current; // Last - - - - - }else if(op === 'filter'){ - scopedData = jsonLogic.apply(values[0], data); - scopedLogic = values[1]; - - 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)); - }); - }else if(op === 'map'){ - scopedData = jsonLogic.apply(values[0], data); - scopedLogic = values[1]; - - if ( ! Array.isArray(scopedData)) { - return []; - } - - return scopedData.map(function(datum){ - return jsonLogic.apply(scopedLogic, datum); - }); - - }else if(op === 'reduce'){ - scopedData = jsonLogic.apply(values[0], data); - scopedLogic = values[1]; - initial = typeof values[2] !== 'undefined' ? values[2] : null; - - if ( ! Array.isArray(scopedData)) { - return initial; - } - - return scopedData.reduce( - function(accumulator, current){ - return jsonLogic.apply( - scopedLogic, - {'current':current, 'accumulator':accumulator} - ); - }, - initial - ); - - }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) { - return false; - } - 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); - return filtered.length === 0; - - }else if(op === "some") { - filtered = jsonLogic.apply({'filter' : values}, data); - return filtered.length > 0; - } - - // Everyone else gets immediate depth-first recursion - values = values.map(function(val) { - return jsonLogic.apply(val, data); - }); - - - // 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") { - 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); - } - - throw new Error("Unrecognized operation " + op ); - }; - - jsonLogic.uses_data = function(logic) { - var collection = []; - - if( jsonLogic.is_logic(logic) ) { - var op = jsonLogic.get_operator(logic); - var 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 arrayUnique(collection); - }; - - jsonLogic.add_operation = function(name, code) { - operations[name] = code; - }; - - jsonLogic.rm_operation = function(name) { - delete operations[name]; - }; - - jsonLogic.rule_like = function(rule, pattern) { - // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?"); - if(pattern === rule) { - return true; - } // TODO : Deep object equivalency? - if(pattern === "@") { - return true; - } // Wildcard! - if(pattern === "number") { - return (typeof rule === "number"); - } - if(pattern === "string") { - return (typeof rule === "string"); - } - if(pattern === "array") { - // !logic test might be superfluous in JavaScript - return Array.isArray(rule) && ! 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"; - return jsonLogic.rule_like( - jsonLogic.get_values(rule, false), - jsonLogic.get_values(pattern, false) - ); - } - } - 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) { - 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) { - // If any fail, we fail - if( ! jsonLogic.rule_like(rule[i], pattern[i])) { - return false; - } - } - return true; // If they *all* passed, we pass - }else{ - return false; // Pattern is array, rule isn't - } - } - - // Not logic, not array, not a === match for rule. - return false; - }; - - return jsonLogic; -})); diff --git a/package.json b/package.json index 201ef3b..b4a197d 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,26 @@ "name": "json-logic-js", "version": "1.2.2", "description": "Build complex rules, serialize them as JSON, and execute them in JavaScript", - "main": "logic.js", + "main": "dist/logic.js", + "module": "dist/logic.mjs", + "unpkg": "dist/logic.js", "directories": { "test": "tests" }, "dependencies": {}, "devDependencies": { - "eslint": "^3.9.1", + "@babel/core": "^7.0.0-beta.47", + "@babel/preset-env": "^7.0.0-beta.47", + "eslint": "^4.19.1", "eslint-config-google": "^0.7.0", "gulp": "^3.9.0", "qunit": "^0.7.7", - "request": "^2.65.0" + "rollup": "^0.59.1", + "rollup-plugin-babel": "^4.0.0-beta.4", + "rollup-plugin-node-resolve": "^3.3.0" }, "scripts": { + "build": "rollup -c", "test": "gulp test" }, "repository": { diff --git a/play.html b/play.html index f239440..3119aa5 100644 --- a/play.html +++ b/play.html @@ -60,7 +60,7 @@

Test JsonLogic in your Browser

- +