-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjson_logic.py
210 lines (173 loc) · 5.76 KB
/
json_logic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# This is a Python implementation of the following jsonLogic JS library:
# https://github.com/jwadhams/json-logic-js
#
# Original code from https://github.com/nadirizr/json-logic-py
#
# Need to use this as the version on pip is not up-to-date
# https://pypi.org/project/json-logic/
# Also this file != https://github.com/nadirizr/json-logic-py/blob/master/json_logic/__init__.py
# because we're opinionated on Python3 and thus no longer need the package 'six'
# and can rely on functools.reduce instead
from __future__ import unicode_literals
import sys
import logging
logger = logging.getLogger(__name__)
try:
unicode
except NameError:
pass
else:
# Python 2 fallback.
str = unicode
def if_(*args):
"""Implements the 'if' operator with support for multiple elseif-s."""
for i in range(0, len(args) - 1, 2):
if args[i]:
return args[i + 1]
if len(args) % 2:
return args[-1]
else:
return None
def soft_equals(a, b):
"""Implements the '==' operator, which does type JS-style coertion."""
if isinstance(a, str) or isinstance(b, str):
return str(a) == str(b)
if isinstance(a, bool) or isinstance(b, bool):
return bool(a) is bool(b)
return a == b
def hard_equals(a, b):
"""Implements the '===' operator."""
if type(a) != type(b):
return False
return a == b
def less(a, b, *args):
"""Implements the '<' operator with JS-style type coertion."""
types = set([type(a), type(b)])
if float in types or int in types:
try:
a, b = float(a), float(b)
except TypeError:
# NaN
return False
return a < b and (not args or less(b, *args))
def less_or_equal(a, b, *args):
"""Implements the '<=' operator with JS-style type coertion."""
return (
less(a, b) or soft_equals(a, b)
) and (not args or less_or_equal(b, *args))
def to_numeric(arg):
"""
Converts a string either to int or to float.
This is important, because e.g. {"!==": [{"+": "0"}, 0.0]}
"""
if isinstance(arg, str):
if '.' in arg:
return float(arg)
else:
return int(arg)
return arg
def plus(*args):
"""Sum converts either to ints or to floats."""
return sum(to_numeric(arg) for arg in args)
def minus(*args):
"""Also, converts either to ints or to floats."""
if len(args) == 1:
return -to_numeric(args[0])
return to_numeric(args[0]) - to_numeric(args[1])
def merge(*args):
"""Implements the 'merge' operator for merging lists."""
ret = []
for arg in args:
if isinstance(arg, list) or isinstance(arg, tuple):
ret += list(arg)
else:
ret.append(arg)
return ret
def get_var(data, var_name, not_found=None):
"""Gets variable value from data dictionary."""
try:
for key in str(var_name).split('.'):
try:
data = data[key]
except TypeError:
data = data[int(key)]
except (KeyError, TypeError, ValueError):
return not_found
else:
return data
def missing(data, *args):
"""Implements the missing operator for finding missing variables."""
not_found = object()
if args and isinstance(args[0], list):
args = args[0]
ret = []
for arg in args:
if get_var(data, arg, not_found) is not_found:
ret.append(arg)
return ret
def missing_some(data, min_required, args):
"""Implements the missing_some operator for finding missing variables."""
if min_required < 1:
return []
found = 0
not_found = object()
ret = []
for arg in args:
if get_var(data, arg, not_found) is not_found:
ret.append(arg)
else:
found += 1
if found >= min_required:
return []
return ret
operations = {
"==": soft_equals,
"===": hard_equals,
"!=": lambda a, b: not soft_equals(a, b),
"!==": lambda a, b: not hard_equals(a, b),
">": lambda a, b: less(b, a),
">=": lambda a, b: less(b, a) or soft_equals(a, b),
"<": less,
"<=": less_or_equal,
"!": lambda a: not a,
"!!": bool,
"%": lambda a, b: a % b,
"and": lambda *args: reduce(lambda total, arg: total and arg, args, True),
"or": lambda *args: reduce(lambda total, arg: total or arg, args, False),
"?:": lambda a, b, c: b if a else c,
"if": if_,
"log": lambda a: logger.info(a) or a,
"in": lambda a, b: a in b if "__contains__" in dir(b) else False,
"cat": lambda *args: "".join(str(arg) for arg in args),
"+": plus,
"*": lambda *args: reduce(lambda total, arg: total * float(arg), args, 1),
"-": minus,
"/": lambda a, b=None: a if b is None else float(a) / float(b),
"min": lambda *args: min(args),
"max": lambda *args: max(args),
"merge": merge,
"count": lambda *args: sum(1 if a else 0 for a in args),
}
def jsonLogic(tests, data=None):
"""Executes the json-logic with given data."""
# You've recursed to a primitive, stop!
if tests is None or not isinstance(tests, dict):
return tests
data = data or {}
operator = list(tests.keys())[0]
values = tests[operator]
# Easy syntax for unary operators, like {"var": "x"} instead of strict
# {"var": ["x"]}
if not isinstance(values, list) and not isinstance(values, tuple):
values = [values]
# Recursion!
values = [jsonLogic(val, data) for val in values]
if operator == 'var':
return get_var(data, *values)
if operator == 'missing':
return missing(data, *values)
if operator == 'missing_some':
return missing_some(data, *values)
if operator not in operations:
raise ValueError("Unrecognized operation %s" % operator)
return operations[operator](*values)