Skip to content

Modernised Module #56

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"presets": [
[
"@babel/env",
{
"modules": false
}
]
]
}
32 changes: 16 additions & 16 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -4,3 +4,6 @@ tests/*.json
# editor and IDE remnants
*~
.idea/

# Bundled
dist/
8 changes: 7 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
tests/tests.json
play.html
bower.json

src/
.babelrc
.editorconfig
.eslintrc.json
bower.json
gulpfile.js
rollup.config.js
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
15 changes: 4 additions & 11 deletions bower.json
Original file line number Diff line number Diff line change
@@ -2,18 +2,11 @@
"name": "json-logic-js",
"version": "1.2.2",
"homepage": "https://github.com/jwadhams/json-logic-js",
"authors": [
"Jeremy Wadhams <[email protected]>"
],
"authors": ["Jeremy Wadhams <[email protected]>"],
"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": [
23 changes: 14 additions & 9 deletions gulpfile.js
Original file line number Diff line number Diff line change
@@ -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"]);
});
464 changes: 0 additions & 464 deletions logic.js

This file was deleted.

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
2 changes: 1 addition & 1 deletion play.html
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ <h1>Test JsonLogic in your Browser</h1>
<!-- Latest compiled and minified JavaScript -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="logic.js"></script>
<script src="dist/logic.js"></script>
<script>
function lint(element){
var $element = $(element),
23 changes: 23 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import babel from "rollup-plugin-babel";
import resolve from "rollup-plugin-node-resolve";

export default {
input: "src/logic.js",
output: [
{
name: "jsonLogic",
file: "dist/logic.js",
format: "umd",
},
{
file: "dist/logic.mjs",
format: "es",
},
],
plugins: [
babel({
exclude: "node_modules/**",
}),
resolve(),
],
};
107 changes: 107 additions & 0 deletions src/default-operations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import truthy from "./truthy";

export default {
"==": (a, b) => a == b,
"===": (a, b) => a === b,
"!=": (a, b) => a != b,
"!==": (a, b) => a !== b,
">": (a, b) => a > b,
">=": (a, b) => a >= b,
"<": (a, b, c) => (c === undefined ? a < b : a < b && b < c),
"<=": (a, b, c) => (c === undefined ? a <= b : a <= b && b <= c),
"!!": truthy,
"!": (a) => !truthy(a),
"%": (a, b) => a % b,
"log": (a) => {
console.log(a);
return a;
},
"in": (a, b) => {
if (!b || typeof b.indexOf === "undefined") return false;
return b.indexOf(a) !== -1;
},
"cat": function() {
return Array.prototype.join.call(arguments, "");
},
"substr": (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);
},
"+": () =>
Array.prototype.reduce.call(
arguments,
(a, b) => parseFloat(a, 10) + parseFloat(b, 10),
0
),
"*": () =>
Array.prototype.reduce.call(
arguments,
(a, b) => parseFloat(a, 10) * parseFloat(b, 10)
),
"-": (a, b) => (b === undefined ? -a : a - b),
"/": (a, b) => 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, (a, b) => 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 = 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 = apply({missing: options}, this);

if (options.length - are_missing.length >= need_count) {
return [];
} else {
return are_missing;
}
},
"method": (obj, method, args) => obj[method].apply(obj, args),
};
5 changes: 5 additions & 0 deletions src/is-array-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === "[object Array]";
};
}
292 changes: 292 additions & 0 deletions src/logic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import {arrayUnique} from "./util";
import default_operations from "./default-operations";
import "./is-array-polyfill";

export {default as truthy} from "./truthy";

export function is_logic(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
);
}

export function get_operator(logic) {
return Object.keys(logic)[0];
}

export function get_values(logic) {
return logic[get_operator(logic)];
}

export function uses_data(logic) {
var collection = [];

if (is_logic(logic)) {
var op = 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, uses_data(val));
});
}
}

return arrayUnique(collection);
}

export function rule_like(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) && !is_logic(rule);
}

if (is_logic(pattern)) {
if (is_logic(rule)) {
var pattern_op = get_operator(pattern);
var rule_op = get_operator(rule);

if (pattern_op === "@" || pattern_op === rule_op) {
// echo "\nOperators match, go deeper\n";
return rule_like(get_values(rule, false), 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 (!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;
}

export class JSONLogic {
constructor() {
this.operations = {...default_operations};
}
apply(logic, data) {
const {operations} = this;
const apply = this.apply.bind(this);

// Does this array contain logic? Only one way to find out.
if (Array.isArray(logic)) {
return logic.map(function(l) {
return apply(l, data);
});
}
// You've recursed to a primitive, stop!
if (!is_logic(logic)) {
return logic;
}

data = data || {};

var op = 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 (truthy(apply(values[i], data))) {
return apply(values[i + 1], data);
}
}
if (values.length === i + 1) return apply(values[i], data);
return null;
} else if (op === "and") {
// Return first falsy, or last
for (i = 0; i < values.length; i += 1) {
current = apply(values[i], data);
if (!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 = apply(values[i], data);
if (truthy(current)) {
return current;
}
}
return current; // Last
} else if (op === "filter") {
scopedData = 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 truthy(apply(scopedLogic, datum));
});
} else if (op === "map") {
scopedData = apply(values[0], data);
scopedLogic = values[1];

if (!Array.isArray(scopedData)) {
return [];
}

return scopedData.map(function(datum) {
return apply(scopedLogic, datum);
});
} else if (op === "reduce") {
scopedData = 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 apply(scopedLogic, {
current: current,
accumulator: accumulator,
});
}, initial);
} else if (op === "all") {
scopedData = 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 (!truthy(apply(scopedLogic, scopedData[i]))) {
return false; // First falsy, short circuit
}
}
return true; // All were truthy
} else if (op === "none") {
filtered = apply({filter: values}, data);
return filtered.length === 0;
} else if (op === "some") {
filtered = apply({filter: values}, data);
return filtered.length > 0;
}

// Everyone else gets immediate depth-first recursion
values = values.map(function(val) {
return 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);
}

add_operation(name, code) {
this.operations[name] = code;
}

rm_operation(name) {
delete this.operations[name];
}
}

const defaultInstance = new JSONLogic();

export function apply(logic, data) {
return defaultInstance.apply.call(defaultInstance, logic, data);
}

export function add_operation(name, code) {
return defaultInstance.add_operation.call(defaultInstance, name, code);
}

export function rm_operation(name) {
return defaultInstance.rm_operation.call(defaultInstance, name);
}
11 changes: 11 additions & 0 deletions src/truthy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
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
*/
export default function truthy(value) {
if (Array.isArray(value) && value.length === 0) {
return false;
}
return !!value;
}
14 changes: 14 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Return an array that contains no duplicates (original not modified)
* @param {array} array Original reference array
* @return {array} New array with no duplicates
*/
export function arrayUnique(array) {
var a = [];
for (var i = 0, l = array.length; i < l; i++) {
if (a.indexOf(array[i]) === -1) {
a.push(array[i]);
}
}
return a;
}
23 changes: 13 additions & 10 deletions tests/testrunner.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
var testrunner = require("qunit");

testrunner.setup({
log: {
summary: true,
errors: true
}
log: {
summary: true,
errors: true,
},
});

// specify dependency
testrunner.run({
code: "../logic.js",
tests: "tests.js"
}, function(err, report) {
if(err) console.dir(err);
testrunner.run(
{
code: "../dist/logic.js",
tests: "tests.js",
},
function(err, report) {
if (err) console.dir(err);
// console.dir(report);
});
}
);
234 changes: 112 additions & 122 deletions tests/tests.js

Large diffs are not rendered by default.

3,068 changes: 3,068 additions & 0 deletions yarn.lock

Large diffs are not rendered by default.