Skip to content

Commit e90eace

Browse files
committed
Passing shared tests from http://jsonlogic.com/tests.json.
Fixed some type coertion issues. Make unary operator syntax sugar work correctly by 'missing' to receive a list as first argument.
1 parent 13de94b commit e90eace

File tree

2 files changed

+106
-20
lines changed

2 files changed

+106
-20
lines changed

json_logic/__init__.py

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
# This is a Python implementation of the following jsonLogic JS library:
22
# https://github.com/jwadhams/json-logic-js
3+
from __future__ import unicode_literals
34

45
import sys
56
from six.moves import reduce
67
import logging
78

89
logger = logging.getLogger(__name__)
910

11+
try:
12+
unicode
13+
except NameError:
14+
pass
15+
else:
16+
# Python 2 fallback.
17+
str = unicode
18+
1019

1120
def if_(*args):
1221
"""Implements the 'if' operator with support for multiple elseif-s."""
13-
assert len(args) >= 2
1422
for i in range(0, len(args) - 1, 2):
1523
if args[i]:
1624
return args[i + 1]
@@ -29,6 +37,56 @@ def soft_equals(a, b):
2937
return a == b
3038

3139

40+
def hard_equals(a, b):
41+
"""Implements the '===' operator."""
42+
if type(a) != type(b):
43+
return False
44+
return a == b
45+
46+
47+
def less(a, b, *args):
48+
"""Implements the '<' operator with JS-style type coertion."""
49+
types = set([type(a), type(b)])
50+
if float in types or int in types:
51+
try:
52+
a, b = float(a), float(b)
53+
except TypeError:
54+
# NaN
55+
return False
56+
return a < b and (not args or less(b, *args))
57+
58+
59+
def less_or_equal(a, b, *args):
60+
"""Implements the '<=' operator with JS-style type coertion."""
61+
return (
62+
less(a, b) or soft_equals(a, b)
63+
) and (not args or less_or_equal(b, *args))
64+
65+
66+
def to_numeric(arg):
67+
"""
68+
Converts a string either to int or to float.
69+
This is important, because e.g. {"!==": [{"+": "0"}, 0.0]}
70+
"""
71+
if isinstance(arg, str):
72+
if '.' in arg:
73+
return float(arg)
74+
else:
75+
return int(arg)
76+
return arg
77+
78+
def plus(*args):
79+
"""Sum converts either to ints or to floats."""
80+
return sum(to_numeric(arg) for arg in args)
81+
82+
83+
def minus(*args):
84+
"""Also, converts either to ints or to floats."""
85+
if len(args) == 1:
86+
return -to_numeric(args[0])
87+
return to_numeric(args[0]) - to_numeric(args[1])
88+
89+
3290
def merge(*args):
3391
"""Implements the 'merge' operator for merging lists."""
3492
ret = []
@@ -57,6 +115,8 @@ def get_var(data, var_name, not_found=None):
57115
def missing(data, *args):
58116
"""Implements the missing operator for finding missing variables."""
59117
not_found = object()
118+
if args and isinstance(args[0], list):
119+
args = args[0]
60120
ret = []
61121
for arg in args:
62122
if get_var(data, arg, not_found) is not_found:
@@ -83,28 +143,29 @@ def missing_some(data, min_required, args):
83143

84144
operations = {
85145
"==": soft_equals,
86-
"===": lambda a, b: a is b,
146+
"===": hard_equals,
87147
"!=": lambda a, b: not soft_equals(a, b),
88-
"!==": lambda a, b: a is not b,
89-
">": lambda a, b: a > b,
90-
">=": lambda a, b: a >= b,
91-
"<": lambda a, b, c=None: a < b if c is None else a < b < c,
92-
"<=": lambda a, b, c=None: a <= b if c is None else a <= b <= c,
148+
"!==": lambda a, b: not hard_equals(a, b),
149+
">": lambda a, b: less(b, a),
150+
">=": lambda a, b: less(b, a) or soft_equals(a, b),
151+
"<": less,
152+
"<=": less_or_equal,
93153
"!": lambda a: not a,
154+
"!!": bool,
94155
"%": lambda a, b: a % b,
95156
"and": lambda *args: reduce(lambda total, arg: total and arg, args, True),
96157
"or": lambda *args: reduce(lambda total, arg: total or arg, args, False),
97158
"?:": lambda a, b, c: b if a else c,
98159
"if": if_,
99160
"log": lambda a: logger.info(a) or a,
100161
"in": lambda a, b: a in b if "__contains__" in dir(b) else False,
101-
"cat": lambda *args: "".join(args),
102-
"+": lambda *args: sum(float(arg) for arg in args),
162+
"cat": lambda *args: "".join(str(arg) for arg in args),
163+
"+": plus,
103164
"*": lambda *args: reduce(lambda total, arg: total * float(arg), args, 1),
104-
"-": lambda a, b=None: -a if b is None else a - b,
165+
"-": minus,
105166
"/": lambda a, b=None: a if b is None else float(a) / float(b),
106-
"min": min,
107-
"max": max,
167+
"min": lambda *args: min(args),
168+
"max": lambda *args: max(args),
108169
"merge": merge,
109170
"count": lambda *args: sum(1 if a else 0 for a in args),
110171
}
@@ -124,14 +185,10 @@ def jsonLogic(tests, data=None):
124185
# Easy syntax for unary operators, like {"var": "x"} instead of strict
125186
# {"var": ["x"]}
126187
if not isinstance(values, list) and not isinstance(values, tuple):
127-
values = jsonLogic(values, data)
128-
# Let's do recursion first. If it's still not a list after processing,
129-
# then it means it's unary syntax sugar.
130-
if not isinstance(values, list) and not isinstance(values, tuple):
131-
values = [values]
132-
else:
133-
# Recursion!
134-
values = [jsonLogic(val, data) for val in values]
188+
values = [values]
189+
190+
# Recursion!
191+
values = [jsonLogic(val, data) for val in values]
135192

136193
if operator == 'var':
137194
return get_var(data, *values)

tests.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
"""
22
Tests for jsonLogic.
33
"""
4+
from __future__ import unicode_literals
5+
6+
import json
47
import unittest
8+
try:
9+
from urllib.request import urlopen
10+
except ImportError:
11+
from urllib2 import urlopen
512
from json_logic import jsonLogic
613

714

@@ -356,3 +363,25 @@ def test_log(self):
356363
This can be especially helpful when debugging a large rule.
357364
"""
358365
self.assertEqual(jsonLogic({"log": "apple"}), "apple")
366+
367+
368+
class SharedTests(unittest.TestCase):
369+
"""This runs the tests from http://jsonlogic.com/tests.json."""
370+
cnt = 0
371+
@classmethod
372+
def create_test(cls, logic, data, expected):
373+
"""Adds new test to the class."""
374+
def test(self):
375+
"""Actual test function."""
376+
self.assertEqual(jsonLogic(logic, data), expected)
377+
test.__doc__ = "{}, {} => {}".format(logic, data, expected)
378+
setattr(cls, "test_{}".format(cls.cnt), test)
379+
cls.cnt += 1
380+
381+
382+
SHARED_TESTS = json.loads(
383+
urlopen("http://jsonlogic.com/tests.json").read().decode('utf-8')
384+
)
385+
for item in SHARED_TESTS:
386+
if isinstance(item, list):
387+
SharedTests.create_test(*item)

0 commit comments

Comments
 (0)