jsonata-python 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jsonata/__init__.py +10 -0
- jsonata/cli/__init__.py +0 -0
- jsonata/cli/__main__.py +242 -0
- jsonata/constants.py +68 -0
- jsonata/datetimeutils.py +1133 -0
- jsonata/functions.py +2234 -0
- jsonata/jexception.py +232 -0
- jsonata/jsonata.py +2004 -0
- jsonata/parser.py +1393 -0
- jsonata/signature.py +433 -0
- jsonata/timebox.py +89 -0
- jsonata/tokenizer.py +309 -0
- jsonata/utils.py +178 -0
- jsonata_python-0.5.2.dist-info/METADATA +339 -0
- jsonata_python-0.5.2.dist-info/RECORD +17 -0
- jsonata_python-0.5.2.dist-info/WHEEL +4 -0
- jsonata_python-0.5.2.dist-info/licenses/LICENSE +202 -0
jsonata/signature.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright Robert Yokota
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# Derived from the following code:
|
|
17
|
+
#
|
|
18
|
+
# Project name: jsonata-java
|
|
19
|
+
# Copyright Dashjoin GmbH. https://dashjoin.com
|
|
20
|
+
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
21
|
+
#
|
|
22
|
+
# Project name: JSONata4Java
|
|
23
|
+
# (c) Copyright 2018, 2019 IBM Corporation
|
|
24
|
+
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
25
|
+
# 1 New Orchard Road,
|
|
26
|
+
# Armonk, New York, 10504-1722
|
|
27
|
+
# United States
|
|
28
|
+
# +1 914 499 1900
|
|
29
|
+
# support: Nathaniel Mills wnm3@us.ibm.com
|
|
30
|
+
#
|
|
31
|
+
|
|
32
|
+
import re
|
|
33
|
+
from typing import MutableSequence, Optional, Sequence, NoReturn, Any
|
|
34
|
+
|
|
35
|
+
from jsonata import jexception, utils
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Manages signature related functions
|
|
40
|
+
#
|
|
41
|
+
class Signature:
|
|
42
|
+
SERIAL_VERSION_UID = -450755246855587271
|
|
43
|
+
|
|
44
|
+
class Param:
|
|
45
|
+
|
|
46
|
+
type: Optional[str]
|
|
47
|
+
regex: Optional[str]
|
|
48
|
+
context: bool
|
|
49
|
+
array: bool
|
|
50
|
+
subtype: Optional[str]
|
|
51
|
+
context_regex: Optional[str]
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.type = None
|
|
55
|
+
self.regex = None
|
|
56
|
+
self.context = False
|
|
57
|
+
self.array = False
|
|
58
|
+
self.subtype = None
|
|
59
|
+
self.context_regex = None
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return "Param " + self.type + " regex=" + self.regex + " ctx=" + str(self.context) + " array=" + str(
|
|
63
|
+
self.array)
|
|
64
|
+
|
|
65
|
+
signature: str
|
|
66
|
+
function_name: str
|
|
67
|
+
|
|
68
|
+
_param: 'Signature.Param'
|
|
69
|
+
_params: MutableSequence['Signature.Param']
|
|
70
|
+
_prev_param: 'Signature.Param'
|
|
71
|
+
_regex: Optional[re.Pattern]
|
|
72
|
+
_signature: str
|
|
73
|
+
|
|
74
|
+
def __init__(self, signature, function):
|
|
75
|
+
self._param = Signature.Param()
|
|
76
|
+
self._params = []
|
|
77
|
+
self._prev_param = self._param
|
|
78
|
+
self._regex = None
|
|
79
|
+
self._signature = ""
|
|
80
|
+
|
|
81
|
+
self.function_name = function
|
|
82
|
+
self.parse_signature(signature)
|
|
83
|
+
|
|
84
|
+
def set_function_name(self, function_name: str) -> None:
|
|
85
|
+
self.function_name = function_name
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def main(args: Sequence[str]) -> None:
|
|
89
|
+
s = Signature("<s-:s>", "test") # <s-(sf)(sf)n?:s>");
|
|
90
|
+
print(s._params)
|
|
91
|
+
|
|
92
|
+
def find_closing_bracket(self, string: str, start: int, open_symbol: str, close_symbol: str) -> int:
|
|
93
|
+
# returns the position of the closing symbol (e.g. bracket) in a string
|
|
94
|
+
# that balances the opening symbol at position start
|
|
95
|
+
depth = 1
|
|
96
|
+
position = start
|
|
97
|
+
while position < len(string):
|
|
98
|
+
position += 1
|
|
99
|
+
symbol = string[position]
|
|
100
|
+
if symbol == close_symbol:
|
|
101
|
+
depth -= 1
|
|
102
|
+
if depth == 0:
|
|
103
|
+
# we're done
|
|
104
|
+
break # out of while loop
|
|
105
|
+
elif symbol == open_symbol:
|
|
106
|
+
depth += 1
|
|
107
|
+
return position
|
|
108
|
+
|
|
109
|
+
def get_symbol(self, value: Optional[Any]) -> str:
|
|
110
|
+
from jsonata import functions
|
|
111
|
+
if value is None:
|
|
112
|
+
symbol = "m"
|
|
113
|
+
else:
|
|
114
|
+
# first check to see if this is a function
|
|
115
|
+
if utils.Utils.is_function(value) or functions.Functions.is_lambda(value) or isinstance(value, re.Pattern):
|
|
116
|
+
symbol = "f"
|
|
117
|
+
elif isinstance(value, str):
|
|
118
|
+
symbol = "s"
|
|
119
|
+
elif isinstance(value, bool):
|
|
120
|
+
symbol = "b"
|
|
121
|
+
elif isinstance(value, (int, float)):
|
|
122
|
+
symbol = "n"
|
|
123
|
+
elif isinstance(value, list):
|
|
124
|
+
symbol = "a"
|
|
125
|
+
elif isinstance(value, dict):
|
|
126
|
+
symbol = "o"
|
|
127
|
+
elif value is None: # Uli: is this used???
|
|
128
|
+
symbol = "l"
|
|
129
|
+
else:
|
|
130
|
+
# any value can be undefined, but should be allowed to match
|
|
131
|
+
symbol = "m" # m for missing
|
|
132
|
+
return symbol
|
|
133
|
+
|
|
134
|
+
def next(self) -> None:
|
|
135
|
+
self._params.append(self._param)
|
|
136
|
+
self._prev_param = self._param
|
|
137
|
+
self._param = Signature.Param()
|
|
138
|
+
|
|
139
|
+
#
|
|
140
|
+
# Parses a function signature definition and returns a validation function
|
|
141
|
+
#
|
|
142
|
+
# @param {string}
|
|
143
|
+
# signature - the signature between the <angle brackets>
|
|
144
|
+
# @returns validation pattern
|
|
145
|
+
#
|
|
146
|
+
def parse_signature(self, signature: str) -> Optional[re.Pattern]:
|
|
147
|
+
# create a Regex that represents this signature and return a function that when
|
|
148
|
+
# invoked,
|
|
149
|
+
# returns the validated (possibly fixed-up) arguments, or throws a validation
|
|
150
|
+
# error
|
|
151
|
+
# step through the signature, one symbol at a time
|
|
152
|
+
position = 1
|
|
153
|
+
while position < len(signature):
|
|
154
|
+
symbol = signature[position]
|
|
155
|
+
if symbol == ':':
|
|
156
|
+
# TODO figure out what to do with the return type
|
|
157
|
+
# ignore it for now
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
if symbol == 's' or symbol == 'n' or symbol == 'b' or symbol == 'l' or symbol == 'o':
|
|
161
|
+
self._param.regex = ("[" + symbol + "m]")
|
|
162
|
+
self._param.type = str(symbol)
|
|
163
|
+
self.next()
|
|
164
|
+
elif symbol == 'a':
|
|
165
|
+
# normally treat any value as singleton array
|
|
166
|
+
self._param.regex = "[asnblfom]"
|
|
167
|
+
self._param.type = str(symbol)
|
|
168
|
+
self._param.array = True
|
|
169
|
+
self.next()
|
|
170
|
+
elif symbol == 'f':
|
|
171
|
+
self._param.regex = "f"
|
|
172
|
+
self._param.type = str(symbol)
|
|
173
|
+
self.next()
|
|
174
|
+
elif symbol == 'j':
|
|
175
|
+
self._param.regex = "[asnblom]"
|
|
176
|
+
self._param.type = str(symbol)
|
|
177
|
+
self.next()
|
|
178
|
+
elif symbol == 'x':
|
|
179
|
+
self._param.regex = "[asnblfom]"
|
|
180
|
+
self._param.type = str(symbol)
|
|
181
|
+
self.next()
|
|
182
|
+
elif symbol == '-':
|
|
183
|
+
self._prev_param.context = True
|
|
184
|
+
self._prev_param.regex += "?"
|
|
185
|
+
elif symbol == '?' or symbol == '+':
|
|
186
|
+
self._prev_param.regex += symbol
|
|
187
|
+
elif symbol == '(':
|
|
188
|
+
# search forward for matching ')'
|
|
189
|
+
end_paren = self.find_closing_bracket(signature, position, '(', ')')
|
|
190
|
+
choice = signature[position + 1:end_paren]
|
|
191
|
+
if choice.find("<") == -1:
|
|
192
|
+
# no _parameterized types, simple regex
|
|
193
|
+
self._param.regex = ("[" + choice + "m]")
|
|
194
|
+
else:
|
|
195
|
+
# TODO harder
|
|
196
|
+
raise RuntimeError("Choice groups containing parameterized types are not supported")
|
|
197
|
+
self._param.type = ("(" + choice + ")")
|
|
198
|
+
position = end_paren
|
|
199
|
+
self.next()
|
|
200
|
+
elif symbol == '<':
|
|
201
|
+
test = self._prev_param.type
|
|
202
|
+
if test is not None:
|
|
203
|
+
type = test # .asText();
|
|
204
|
+
if type == "a" or type == "f":
|
|
205
|
+
# search forward for matching '>'
|
|
206
|
+
end_pos = self.find_closing_bracket(signature, position, '<', '>')
|
|
207
|
+
self._prev_param.subtype = signature[position + 1:end_pos]
|
|
208
|
+
position = end_pos
|
|
209
|
+
else:
|
|
210
|
+
raise RuntimeError("Type parameters can only be applied to functions and arrays")
|
|
211
|
+
else:
|
|
212
|
+
raise RuntimeError("Type parameters can only be applied to functions and arrays")
|
|
213
|
+
position += 1 # end while processing symbols in signature
|
|
214
|
+
|
|
215
|
+
regex_str = "^"
|
|
216
|
+
for param in self._params:
|
|
217
|
+
regex_str += "(" + param.regex + ")"
|
|
218
|
+
regex_str += "$"
|
|
219
|
+
|
|
220
|
+
self._regex = re.compile(regex_str)
|
|
221
|
+
self._signature = regex_str
|
|
222
|
+
return self._regex
|
|
223
|
+
|
|
224
|
+
def throw_validation_error(self, bad_args: Optional[Sequence], bad_sig: Optional[str],
|
|
225
|
+
function_name: Optional[str]) -> NoReturn:
|
|
226
|
+
# to figure out where this went wrong we need apply each component of the
|
|
227
|
+
# regex to each argument until we get to the one that fails to match
|
|
228
|
+
partial_pattern = "^"
|
|
229
|
+
|
|
230
|
+
good_to = 0
|
|
231
|
+
for param in self._params:
|
|
232
|
+
partial_pattern += param.regex
|
|
233
|
+
tester = re.compile(partial_pattern)
|
|
234
|
+
match_ = tester.fullmatch(bad_sig)
|
|
235
|
+
if match_ is None:
|
|
236
|
+
# failed here
|
|
237
|
+
raise jexception.JException("T0410", -1, (good_to + 1), function_name)
|
|
238
|
+
good_to = match_.end()
|
|
239
|
+
# if it got this far, it's probably because of extraneous arguments (we
|
|
240
|
+
# haven't added the trailing '$' in the regex yet.
|
|
241
|
+
raise jexception.JException("T0410", -1, (good_to + 1), function_name)
|
|
242
|
+
|
|
243
|
+
def validate(self, args: Any, context: Optional[Any]) -> Optional[Any]:
|
|
244
|
+
supplied_sig = ""
|
|
245
|
+
for arg in args:
|
|
246
|
+
supplied_sig += self.get_symbol(arg)
|
|
247
|
+
|
|
248
|
+
is_valid = self._regex.fullmatch(supplied_sig)
|
|
249
|
+
if is_valid is not None:
|
|
250
|
+
validated_args = []
|
|
251
|
+
arg_index = 0
|
|
252
|
+
index = 0
|
|
253
|
+
for param in self._params:
|
|
254
|
+
arg = args[arg_index] if arg_index < len(args) else None
|
|
255
|
+
match_ = is_valid.group(index + 1)
|
|
256
|
+
if "" == match_:
|
|
257
|
+
if param.context and param.regex is not None:
|
|
258
|
+
# substitute context value for missing arg
|
|
259
|
+
# first check that the context value is the right type
|
|
260
|
+
context_type = self.get_symbol(context)
|
|
261
|
+
# test context_type against the regex for this arg (without the trailing ?)
|
|
262
|
+
if re.fullmatch(param.regex, context_type):
|
|
263
|
+
# if (param.contextRegex.test(context_type)) {
|
|
264
|
+
validated_args.append(context)
|
|
265
|
+
else:
|
|
266
|
+
# context value not compatible with this argument
|
|
267
|
+
raise jexception.JException("T0411", -1, context, arg_index + 1)
|
|
268
|
+
else:
|
|
269
|
+
validated_args.append(arg)
|
|
270
|
+
arg_index += 1
|
|
271
|
+
else:
|
|
272
|
+
# may have matched multiple args (if the regex ends with a '+'
|
|
273
|
+
# split into single tokens
|
|
274
|
+
singles = list(match_) # split on empty separator
|
|
275
|
+
for single in singles:
|
|
276
|
+
# match.split('').forEach(function (single) {
|
|
277
|
+
if param.type == "a":
|
|
278
|
+
if single == "m":
|
|
279
|
+
# missing (undefined)
|
|
280
|
+
arg = None
|
|
281
|
+
else:
|
|
282
|
+
arg = args[arg_index] if arg_index < len(args) else None
|
|
283
|
+
array_ok = True
|
|
284
|
+
# is there type information on the contents of the array?
|
|
285
|
+
if param.subtype is not None:
|
|
286
|
+
if single != "a" and match_ != param.subtype:
|
|
287
|
+
array_ok = False
|
|
288
|
+
elif single == "a":
|
|
289
|
+
arg_arr = arg
|
|
290
|
+
if arg_arr:
|
|
291
|
+
item_type = self.get_symbol(arg_arr[0])
|
|
292
|
+
if item_type != str(param.subtype[0]):
|
|
293
|
+
array_ok = False
|
|
294
|
+
else:
|
|
295
|
+
# make sure every item in the array is this type
|
|
296
|
+
for o in arg_arr:
|
|
297
|
+
if self.get_symbol(o) != item_type:
|
|
298
|
+
array_ok = False
|
|
299
|
+
break
|
|
300
|
+
if not array_ok:
|
|
301
|
+
raise jexception.JException("T0412", -1, arg, param.subtype)
|
|
302
|
+
# the function expects an array. If it's not one, make it so
|
|
303
|
+
if single != "a":
|
|
304
|
+
arg = [arg]
|
|
305
|
+
validated_args.append(arg)
|
|
306
|
+
arg_index += 1
|
|
307
|
+
else:
|
|
308
|
+
arg = args[arg_index] if arg_index < len(args) else None
|
|
309
|
+
validated_args.append(arg)
|
|
310
|
+
arg_index += 1
|
|
311
|
+
return validated_args
|
|
312
|
+
self.throw_validation_error(args, supplied_sig, self.function_name)
|
|
313
|
+
|
|
314
|
+
def get_number_of_args(self) -> int:
|
|
315
|
+
return len(self._params)
|
|
316
|
+
|
|
317
|
+
#
|
|
318
|
+
# Returns the minimum # of arguments.
|
|
319
|
+
# I.e. the # of all non-optional arguments.
|
|
320
|
+
#
|
|
321
|
+
def get_min_number_of_args(self) -> int:
|
|
322
|
+
res = 0
|
|
323
|
+
for p in self._params:
|
|
324
|
+
if "?" not in p.regex:
|
|
325
|
+
res += 1
|
|
326
|
+
return res
|
|
327
|
+
#
|
|
328
|
+
# ArrayNode validate(String functionName, ExprListContext args, ExpressionsVisitor expressionVisitor) {
|
|
329
|
+
# ArrayNode result = JsonNodeFactory.instance.arrayNode()
|
|
330
|
+
# String suppliedSig = ""
|
|
331
|
+
# for (Iterator<ExprContext> it = args.expr().iterator(); it.hasNext();) {
|
|
332
|
+
# ExprContext arg = it.next()
|
|
333
|
+
# suppliedSig += getSymbol(arg)
|
|
334
|
+
# }
|
|
335
|
+
# Matcher isValid = _regex.matcher(suppliedSig)
|
|
336
|
+
# if (isValid != null) {
|
|
337
|
+
# ArrayNode validatedArgs = JsonNodeFactory.instance.arrayNode()
|
|
338
|
+
# int argIndex = 0
|
|
339
|
+
# int index = 0
|
|
340
|
+
# for (Iterator<JsonNode> it = _params.iterator(); it.hasNext();) {
|
|
341
|
+
# ObjectNode param = (ObjectNode) it.next()
|
|
342
|
+
# JsonNode arg = expressionVisitor.visit(args.expr(argIndex))
|
|
343
|
+
# String match = isValid.group(index + 1)
|
|
344
|
+
# if ("".equals(match)) {
|
|
345
|
+
# boolean useContext = (param.get("context") != null && param.get("context").asBoolean())
|
|
346
|
+
# if (useContext) {
|
|
347
|
+
# // substitute context value for missing arg
|
|
348
|
+
# // first check that the context value is the right type
|
|
349
|
+
# JsonNode context = expressionVisitor.getVariable("$")
|
|
350
|
+
# String contextType = getSymbol(context)
|
|
351
|
+
# // test contextType against the regex for this arg (without the trailing ?)
|
|
352
|
+
# if (Pattern.matches(param.get("regex").asText(), contextType)) {
|
|
353
|
+
# validatedArgs.add(context)
|
|
354
|
+
# } else {
|
|
355
|
+
# // context value not compatible with this argument
|
|
356
|
+
# throw new EvaluateRuntimeException("Context value is not a compatible type with argument \""
|
|
357
|
+
# + argIndex + 1 + "\" of function \"" + functionName + "\"")
|
|
358
|
+
# }
|
|
359
|
+
# } else {
|
|
360
|
+
# validatedArgs.add(arg)
|
|
361
|
+
# argIndex++
|
|
362
|
+
# }
|
|
363
|
+
# } else {
|
|
364
|
+
# // may have matched multiple args (if the regex ends with a '+'
|
|
365
|
+
# // split into single tokens
|
|
366
|
+
# String[] singles = match.split("")
|
|
367
|
+
# for (String single : singles) {
|
|
368
|
+
# if ("a".equals(param.get("type").asText())) {
|
|
369
|
+
# if ("m".equals(single)) {
|
|
370
|
+
# // missing (undefined)
|
|
371
|
+
# arg = null
|
|
372
|
+
# } else {
|
|
373
|
+
# arg = expressionVisitor.visit(args.expr(argIndex))
|
|
374
|
+
# boolean arrayOK = true
|
|
375
|
+
# // is there type information on the contents of the array?
|
|
376
|
+
# String subtype = "undefined"
|
|
377
|
+
# JsonNode testSubType = param.get("subtype")
|
|
378
|
+
# if (testSubType != null) {
|
|
379
|
+
# subtype = testSubType.asText()
|
|
380
|
+
# if ("a".equals(single) == false && match.equals(subtype) == false) {
|
|
381
|
+
# arrayOK = false
|
|
382
|
+
# } else if ("a".equals(single)) {
|
|
383
|
+
# ArrayNode argArray = (ArrayNode) arg
|
|
384
|
+
# if (argArray.size() > 0) {
|
|
385
|
+
# String itemType = getSymbol(argArray.get(0))
|
|
386
|
+
# if (itemType.equals(subtype) == false) { // TODO recurse further
|
|
387
|
+
# arrayOK = false
|
|
388
|
+
# } else {
|
|
389
|
+
# // make sure every item in the array is this type
|
|
390
|
+
# ArrayNode differentItems = JsonNodeFactory.instance.arrayNode()
|
|
391
|
+
# for (Object val : argArray) {
|
|
392
|
+
# if (itemType.equals(getSymbol(val)) == false) {
|
|
393
|
+
# differentItems.add(expressionVisitor.visit((ExprListContext) val))
|
|
394
|
+
# }
|
|
395
|
+
# }
|
|
396
|
+
#
|
|
397
|
+
# arrayOK = (differentItems.size() == 0)
|
|
398
|
+
# }
|
|
399
|
+
# }
|
|
400
|
+
# }
|
|
401
|
+
# }
|
|
402
|
+
# if (!arrayOK) {
|
|
403
|
+
# JsonNode type = s_arraySignatureMapping.get(subtype)
|
|
404
|
+
# if (type == null) {
|
|
405
|
+
# type = JsonNodeFactory.instance.nullNode()
|
|
406
|
+
# }
|
|
407
|
+
# throw new EvaluateRuntimeException("Argument \"" + (argIndex + 1) + "\" of function \""
|
|
408
|
+
# + functionName + "\" must be an array of \"" + type.asText() + "\"")
|
|
409
|
+
# }
|
|
410
|
+
# // the function expects an array. If it's not one, make it so
|
|
411
|
+
# if ("a".equals(single) == false) {
|
|
412
|
+
# ArrayNode wrappedArg = JsonNodeFactory.instance.arrayNode()
|
|
413
|
+
# wrappedArg.add(arg)
|
|
414
|
+
# arg = wrappedArg
|
|
415
|
+
# }
|
|
416
|
+
# }
|
|
417
|
+
# validatedArgs.add(arg)
|
|
418
|
+
# argIndex++
|
|
419
|
+
# } else {
|
|
420
|
+
# validatedArgs.add(arg)
|
|
421
|
+
# argIndex++
|
|
422
|
+
# }
|
|
423
|
+
# }
|
|
424
|
+
# }
|
|
425
|
+
# index++
|
|
426
|
+
# }
|
|
427
|
+
# return validatedArgs
|
|
428
|
+
# }
|
|
429
|
+
# throwValidationError(args, suppliedSig, functionName)
|
|
430
|
+
# // below just for the compiler as a runtime exception is thrown above
|
|
431
|
+
# return result
|
|
432
|
+
# }
|
|
433
|
+
#
|
jsonata/timebox.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright Robert Yokota
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
# Derived from the following code:
|
|
17
|
+
#
|
|
18
|
+
# Project name: jsonata-java
|
|
19
|
+
# Copyright Dashjoin GmbH. https://dashjoin.com
|
|
20
|
+
# Licensed under the Apache License, Version 2.0 (the "License")
|
|
21
|
+
#
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from jsonata import jexception
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Configure max runtime / max recursion depth.
|
|
30
|
+
# See Frame.setRuntimeBounds - usually not used directly
|
|
31
|
+
#
|
|
32
|
+
class Timebox:
|
|
33
|
+
#
|
|
34
|
+
# Protect the process/browser from a runnaway expression
|
|
35
|
+
# i.e. Infinite loop (tail recursion), or excessive stack growth
|
|
36
|
+
#
|
|
37
|
+
# @param {Object} expr - expression to protect
|
|
38
|
+
# @param {Number} timeout - max time in ms
|
|
39
|
+
# @param {Number} max_depth - max stack depth
|
|
40
|
+
#
|
|
41
|
+
|
|
42
|
+
timeout: int
|
|
43
|
+
max_depth: int
|
|
44
|
+
time: int
|
|
45
|
+
depth: int
|
|
46
|
+
|
|
47
|
+
def __init__(self, expr, timeout=10000, max_depth=100):
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
self.max_depth = max_depth
|
|
50
|
+
self.time = Timebox.current_milli_time()
|
|
51
|
+
self.depth = 0
|
|
52
|
+
|
|
53
|
+
# register callbacks
|
|
54
|
+
def entry_callback(exp, input, env):
|
|
55
|
+
if env.is_parallel_call:
|
|
56
|
+
return
|
|
57
|
+
self.depth += 1
|
|
58
|
+
self.check_runaway()
|
|
59
|
+
|
|
60
|
+
expr.set_evaluate_entry_callback(entry_callback)
|
|
61
|
+
|
|
62
|
+
def exit_callback(exp, input, env, res):
|
|
63
|
+
if env.is_parallel_call:
|
|
64
|
+
return
|
|
65
|
+
self.depth -= 1
|
|
66
|
+
self.check_runaway()
|
|
67
|
+
|
|
68
|
+
expr.set_evaluate_exit_callback(exit_callback)
|
|
69
|
+
|
|
70
|
+
def check_runaway(self) -> None:
|
|
71
|
+
if self.depth > self.max_depth:
|
|
72
|
+
# stack too deep
|
|
73
|
+
raise jexception.JException(
|
|
74
|
+
"Stack overflow error: Check for non-terminating recursive function. Consider rewriting as tail-recursive. Depth=" + str(
|
|
75
|
+
self.depth) + " max=" + str(self.max_depth), -1)
|
|
76
|
+
# stack: new Error().stack,
|
|
77
|
+
# code: "U1001"
|
|
78
|
+
# }
|
|
79
|
+
if Timebox.current_milli_time() - self.time > self.timeout:
|
|
80
|
+
# expression has run for too long
|
|
81
|
+
raise jexception.JException(
|
|
82
|
+
"Expression evaluation timeout: " + str(self.timeout) + "ms. Check for infinite loop", -1)
|
|
83
|
+
# stack: new Error().stack,
|
|
84
|
+
# code: "U1001"
|
|
85
|
+
# }
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def current_milli_time() -> int:
|
|
89
|
+
return round(time.time() * 1000)
|