For business reasons we need to externalize some conditional logic into external files: preferably JSON.
A simple filter-by scenario could be handled by adding a node a
My colleague suggested this possible solution:
"all OR conditions would be an array while AND conditions would be objects,
For example,OR can match any of the objects in the array:
[
{
"var1":"value1"
},
{
"var2":"value2"
},
{
"var3":"value3"
}
]
AND would be
{
"var1":"val1",
"var2":"val2",
"var3":"val3"
}
By the way, IBM DB2 supports logic statements encoded in JSON.
Boolean operations look like a cross between cHao's solution and Amazon CloudFormation:
{"$and":[{"age":5},{"name":"Joe"}]}
Comparison operations look, to me, like transliterated SQL. (Instead of Amazon or Russellg or cHao's movement toward an abstract syntax tree.)
{"age":{"$lt":3}}
I needed a format that would:
So I built up a format I'm calling JsonLogic. A rule is a JSON object, with the operator in the key position, and one or an array of arguments in the value position. (Inspired by Amazon CloudFormation functions.) Any argument can be another rule, so you can build arbitrarily deep logic.
I've also written two parsers for it: JsonLogic for JavaScript and JsonLogic for PHP.
cHao's example would be written as
{ "and", [
{"==", [ {"var" : "var1"}, "value1" ]},
{ "or", [
{"==", [ {"var" : "var2"}, "value2" ]},
{"==", [ {"var" : "var3"}, "value3" ]}
]}
]}
var
here is the operator to get a property of the "data" object, passed along with the "rule" object to the parser, e.g.:
jsonLogic(
{"==", [{"var":"filling"}, "apple"]} // rule, is this pie apple?
{"filling":"apple", "temperature":100} // data, a pie I'm inspecting
);
// true
There are lots more possible operators (greater than, not-equals, in-array, ternary, etc) and both parsers are available on GitHub (with unit tests and documentation).
Please check out (JSL)[https://www.npmjs.com/package/lib-jsl ]. It seems to fit the description given.
Here is a sample :
var JSL = require('lib-jsl');
var bugs = [
[{ bug : { desc: 'this is bug1 open', status : 'open' } }],
[{ bug : { desc: 'this is bug2 resolved', status : 'resolved' } }],
[{ bug : { desc: 'this is bug3 closed' , status : 'closed' } }],
[{ bug : { desc: 'this is bug4 open', status : 'open' } }],
[{ bug : { desc: 'this is bug5 resolved', status : 'resolved' } }],
[{ bug : { desc: 'this is bug6 open', status : 'open' } }],
[ { workInProgress : '$bug'},
{ bug : '$bug'},
{ $or : [
{ $bind : [ '$bug', { status : 'open'} ] },
{ $bind : [ '$bug', { status : 'resolved'} ] }
] }
]
];
var query = [{workInProgress : '$wip'}]
var transform = '$wip'
var jsl = new JSL ({
rules : bugs,
query : query,
transform : transform
});
var retval = jsl.run();
console.log(JSON.stringify(retval, null,2));
The response is :
[
{
"desc": "this is bug1 open",
"status": "open"
},
{
"desc": "this is bug2 resolved",
"status": "resolved"
},
{
"desc": "this is bug4 open",
"status": "open"
},
{
"desc": "this is bug5 resolved",
"status": "resolved"
},
{
"desc": "this is bug6 open",
"status": "open"
}
]
The main work is done by the query defined in the rule workInProgress :
[ { workInProgress : '$bug'},
{ bug : '$bug'},
{ $or : [
{ $bind : [ '$bug', { status : 'open'} ] },
{ $bind : [ '$bug', { status : 'resolved'} ] }
] }
]
This rule can be read as :
To satisfy the query with workInProgress, we define a variable {workInProgress : '$bug'}
, which we then proceed to match against all bugs in the database using the next part of the rule {bug : '$bug'}
. This part matches all bugs since the shape of the object (it's keys: 'bug') matches the bug records in the database. The rule further asks the $bug variable to be $bind(ed) against patterns containing relevant status values (open and closed) within a $or. Only those bug records whose status value in $bug satisfies all parts of the rule's body qualify for the result.
The result is finally transformed using the transform specification : transform : '$wip'
which literally asks for an array of all values returned in the $wip variable of the query.
I came up with this format with the primary goal of reading as close as possible to actually SQL.
Here's the Type def in typescript:
type LogicalOperator = 'AND' | 'OR';
type Operator = '=' | '<=' | '>=' | '>' | '<' | 'LIKE' | 'IN' | 'NOT IN';
type ConditionParams = {field: string, opp: Operator, val: string | number | boolean};
type Conditions = ConditionParams | LogicalOperator | ConditionsList;
interface ConditionsList extends Array<Conditions> { }
Or BNF (ish? my cs teachers wouldn't be proud)
WHEREGROUP: = [ CONDITION | ('AND'|'OR') | WHEREGROUP ]
CONDITION: = {field, opp, val}
With the following Parsing Rules:
AND
is optional (I typically add it for readability). If logical LogicalOperator
is left out between conditions, it will automatically joins them with AND
()
)Here are some examples (typescript playground link):
1 AND 2 (AND inferred)
[
{ field: 'name', opp: '=', val: '123' },
{ field: 'otherfield', opp: '>=', val: 123 }
]
1 OR 2
[
{ field: 'name', opp: '=', val: '123' },
'OR',
{ field: 'annualRevenue', opp: '>=', val: 123 }
]
(1 OR 2) AND (3 OR 4)
[
[
{ field: 'name', opp: '=', val: '123' },
'OR',
{ field: 'name', opp: '=', val: '456' }
],
'AND',
[
{ field: 'annualRevenue', opp: '>=', val: 123 },
'OR',
{ field: 'active', opp: '=', val: true }
]
]
1 AND (2 OR 3)
[
{ field: 'name', opp: '=', val: '123' },
'AND',
[
{ field: 'annualRevenue', opp: '>=', val: 123 },
'OR',
{ field: 'active', opp: '=', val: true }
]
]
1 AND 2 OR 3
[
{ field: 'name', opp: '=', val: '123' },
'AND',
{ field: 'annualRevenue', opp: '>=', val: 123 },
'OR',
{ field: 'active', opp: '=', val: true }
]
1 OR (2 AND (3 OR 4))
[
{ field: 'name', opp: '=', val: '123' },
'OR',
[
{ field: 'annualRevenue', opp: '>=', val: 123 },
'AND',
[
{ field: 'active', opp: '=', val: true },
'OR',
{ field: 'accountSource', opp: '=', val: 'web' }
]
]
]
As you can see, if you were to remove ,
and property names, then just replace the []
with ()
, you'd basically have the condition in SQL format
Logic can be implemented with "logicOp": "Operator" on a "set": ["a","b" ...] For cHau's example:
"var": {
"logicOp": "And",
"set": ["value1",
{
"LogicOp": "Or",
"set": ["value2", "value3"]
}
]
}
There can also be other attributes/operations for the set for example
"val": { "operators": ["min": 0, "max": 2], "set": ["a", "b", "c"] }
For a sunday with two scoops of one or more icecream types, 1 toppings and whipcream
"sunday": {
"icecream": {
"operators": [ "num": 2,
"multipleOfSingleItem": "true"],
"set": ["chocolate", "strawberry", "vanilla"]
},
"topping": {
"operators": ["num": 1],
"set": ["fudge", "caramel"]
},
"whipcream": "true"
}