jsonata 2.2.2.1

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.
@@ -0,0 +1,1669 @@
1
+ # frozen_string_literal: true
2
+
3
+ # (c) Copyright IBM Corp. 2016, 2017 All Rights Reserved
4
+ # Project name: JSONata
5
+ # This project is licensed under the MIT License, see LICENSE
6
+
7
+ # Port of jsonata-js src/jsonata.js
8
+ #
9
+ # The JavaScript implementation is asynchronous (async/await); this port is synchronous.
10
+
11
+ require 'json'
12
+
13
+ module Jsonata
14
+ # Environment frame (jsonata-js: createFrame)
15
+ class Frame
16
+ attr_accessor :timestamp, :async, :is_parallel_call, :global, :base,
17
+ # the following are only set on the base (execution) environment
18
+ :depth, :options, :guardrails, :regex_engine
19
+
20
+ def initialize(enclosing_environment)
21
+ @bindings = {}
22
+ @enclosing_environment = enclosing_environment
23
+ @timestamp = enclosing_environment&.timestamp
24
+ @async = enclosing_environment ? enclosing_environment.async : false
25
+ @is_parallel_call = enclosing_environment ? enclosing_environment.is_parallel_call : false
26
+ @global = enclosing_environment ? enclosing_environment.global : {:ancestry => [nil]}
27
+
28
+ return unless enclosing_environment
29
+
30
+ frame_push_callback = enclosing_environment.lookup(:'jsonata.__createFrame_push')
31
+ frame_push_callback&.call(enclosing_environment, self)
32
+ @base = enclosing_environment.base
33
+ end
34
+
35
+ def bind(name, value)
36
+ @bindings[name] = value
37
+ end
38
+
39
+ def lookup(name)
40
+ frame = self
41
+ while frame
42
+ bindings = frame.bindings
43
+ return bindings[name] if bindings.key?(name)
44
+
45
+ frame = frame.enclosing_environment
46
+ end
47
+ nil
48
+ end
49
+
50
+ protected
51
+
52
+ attr_reader :bindings, :enclosing_environment
53
+
54
+ public
55
+
56
+ # (jsonata-js: exec_env.createSequence)
57
+ def create_sequence(*args)
58
+ sequence = JArray.new
59
+ sequence.max_length = options[:sequence] if options && options[:sequence]
60
+ sequence.sequence = true
61
+ sequence.push(args[0]) if args.length == 1
62
+ sequence
63
+ end
64
+ end
65
+
66
+ # The `this` object that native functions are invoked with (jsonata-js: the `focus` object)
67
+ class Focus
68
+ include Functions
69
+ include DateTime
70
+
71
+ attr_reader :environment, :input
72
+
73
+ def initialize(environment = nil, input = nil)
74
+ @environment = environment
75
+ @input = input
76
+ end
77
+
78
+ def options
79
+ @environment&.base&.options
80
+ end
81
+
82
+ def create_sequence(*args)
83
+ base = @environment&.base
84
+ if base
85
+ base.create_sequence(*args)
86
+ else
87
+ sequence = JArray.new
88
+ sequence.sequence = true
89
+ sequence.push(args[0]) if args.length == 1
90
+ sequence
91
+ end
92
+ end
93
+
94
+ # parses and evaluates the supplied expression
95
+ # @param expr - expression to evaluate
96
+ # @returns result of evaluating the expression
97
+ def function_eval(expr, focus)
98
+ # undefined inputs always return undefined
99
+ return nil if expr.nil?
100
+
101
+ input = self.input
102
+ unless focus.nil?
103
+ input = focus
104
+ # if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input
105
+ if input.kind_of?(Array) && !Utils.sequence?(input)
106
+ input = create_sequence(input)
107
+ input.outer_wrapper = true
108
+ end
109
+ end
110
+
111
+ begin
112
+ ast = Parser.parse(expr, false)
113
+ rescue Jsonata::Error => e
114
+ # error parsing the expression passed to $eval
115
+ Evaluator.populate_message(e)
116
+ raise Jsonata::Error.new('D3120', :value => e.message, :error => e)
117
+ end
118
+ begin
119
+ result = Evaluator.evaluate(ast, input, environment)
120
+ rescue Jsonata::Error => e
121
+ # error evaluating the expression passed to $eval
122
+ Evaluator.populate_message(e)
123
+ raise Jsonata::Error.new('D3121', :value => e.message, :error => e)
124
+ end
125
+
126
+ result
127
+ end
128
+
129
+ # Clones an object
130
+ # @param arg - object to clone (deep copy)
131
+ # @returns the cloned object
132
+ def function_clone(arg)
133
+ # undefined inputs always return undefined
134
+ return nil if arg.nil?
135
+
136
+ Utils.from_ruby(JSON.parse(string(arg)))
137
+ end
138
+ end
139
+
140
+ module Evaluator
141
+ module_function
142
+
143
+ # Evaluate expression against input data
144
+ # @param expr - JSONata expression
145
+ # @param input - Input data to evaluate against
146
+ # @param environment - Environment
147
+ # @returns Evaluated input data
148
+ def evaluate(expr, input, environment)
149
+ environment.base.depth += 1
150
+ environment.base.guardrails.call
151
+ entry_callback = environment.lookup(:'jsonata.__evaluate_entry')
152
+ entry_callback&.call(expr, input, environment)
153
+
154
+ result = case expr[:type]
155
+ when 'path'
156
+ evaluate_path(expr, input, environment)
157
+ when 'binary'
158
+ evaluate_binary(expr, input, environment)
159
+ when 'unary'
160
+ evaluate_unary(expr, input, environment)
161
+ when 'name'
162
+ evaluate_name(expr, input, environment)
163
+ when 'string', 'number', 'value'
164
+ evaluate_literal(expr)
165
+ when 'wildcard'
166
+ evaluate_wildcard(expr, input, environment)
167
+ when 'descendant'
168
+ evaluate_descendants(expr, input, environment)
169
+ when 'parent'
170
+ environment.lookup(expr[:slot][:label])
171
+ when 'condition'
172
+ evaluate_condition(expr, input, environment)
173
+ when 'block'
174
+ evaluate_block(expr, input, environment)
175
+ when 'bind'
176
+ evaluate_bind_expression(expr, input, environment)
177
+ when 'regex'
178
+ evaluate_regex(expr, input, environment)
179
+ when 'function'
180
+ evaluate_function(expr, input, environment)
181
+ when 'variable'
182
+ evaluate_variable(expr, input, environment)
183
+ when 'lambda'
184
+ evaluate_lambda(expr, input, environment)
185
+ when 'partial'
186
+ evaluate_partial_application(expr, input, environment)
187
+ when 'apply'
188
+ evaluate_apply_expression(expr, input, environment)
189
+ when 'transform'
190
+ evaluate_transform_expression(expr, input, environment)
191
+ end
192
+
193
+ if expr.key?(:predicate)
194
+ expr[:predicate].each do |predicate|
195
+ result = evaluate_filter(predicate[:expr], result, environment)
196
+ end
197
+ end
198
+
199
+ if expr[:type] != 'path' && expr.key?(:group)
200
+ result = evaluate_group_expression(expr[:group], result, environment)
201
+ end
202
+
203
+ exit_callback = environment.lookup(:'jsonata.__evaluate_exit')
204
+ exit_callback&.call(expr, input, environment, result)
205
+
206
+ if Utils.sequence?(result) && !result.tuple_stream
207
+ result.keep_singleton = true if expr[:keep_array]
208
+ if result.empty?
209
+ result = nil
210
+ elsif result.length == 1
211
+ result = result[0] unless result.keep_singleton
212
+ end
213
+ end
214
+
215
+ environment.base.depth -= 1
216
+
217
+ result
218
+ end
219
+
220
+ # Evaluate path expression against input data
221
+ def evaluate_path(expr, input, environment)
222
+ # expr is an array of steps
223
+ # if the first step is a variable reference ($...), including root reference ($$),
224
+ # then the path is absolute rather than relative
225
+ input_sequence = if input.kind_of?(Array) && expr[:steps][0][:type] != 'variable'
226
+ input
227
+ else
228
+ # if input is not an array, make it so
229
+ environment.base.create_sequence(input)
230
+ end
231
+
232
+ result_sequence = nil
233
+ is_tuple_stream = false
234
+ tuple_bindings = nil
235
+
236
+ # evaluate each step in turn
237
+ expr[:steps].each_with_index do |step, ii|
238
+ is_tuple_stream = true if step[:tuple]
239
+
240
+ # if the first step is an explicit array constructor, then just evaluate that (i.e. don't iterate over a context array)
241
+ if ii.zero? && step[:consarray]
242
+ result_sequence = evaluate(step, input_sequence, environment)
243
+ elsif is_tuple_stream
244
+ tuple_bindings = evaluate_tuple_step(step, input_sequence, tuple_bindings, environment)
245
+ else
246
+ result_sequence = evaluate_step(step, input_sequence, environment, ii == expr[:steps].length - 1)
247
+ end
248
+
249
+ break if !is_tuple_stream && (result_sequence.nil? || (result_sequence.kind_of?(Array) && result_sequence.empty?))
250
+
251
+ input_sequence = result_sequence unless step.key?(:focus)
252
+ end
253
+
254
+ if is_tuple_stream
255
+ if expr[:tuple]
256
+ # tuple stream is carrying ancestry information - keep this
257
+ result_sequence = tuple_bindings
258
+ else
259
+ result_sequence = environment.base.create_sequence
260
+ tuple_bindings.each do |tuple|
261
+ result_sequence.push(tuple['@'])
262
+ end
263
+ end
264
+ end
265
+
266
+ if expr[:keep_singleton_array]
267
+ # if the array is explicitly constructed in the expression and marked to promote singleton sequences to array
268
+ if result_sequence.kind_of?(JArray) && result_sequence.cons && !result_sequence.sequence
269
+ result_sequence = environment.base.create_sequence(result_sequence)
270
+ end
271
+ if result_sequence.kind_of?(Array)
272
+ result_sequence = JArray.new(result_sequence) unless result_sequence.kind_of?(JArray)
273
+ result_sequence.keep_singleton = true
274
+ end
275
+ end
276
+
277
+ if expr.key?(:group)
278
+ result_sequence = evaluate_group_expression(expr[:group], is_tuple_stream ? tuple_bindings : result_sequence,
279
+ environment)
280
+ end
281
+
282
+ result_sequence
283
+ end
284
+
285
+ def create_frame_from_tuple(environment, tuple)
286
+ frame = create_frame(environment)
287
+ Utils.keys(tuple).each do |prop|
288
+ frame.bind(prop, tuple[prop])
289
+ end
290
+ frame
291
+ end
292
+
293
+ # Evaluate a step within a path
294
+ # @param last_step - flag the last step in a path
295
+ def evaluate_step(expr, input, environment, last_step)
296
+ if expr[:type] == 'sort'
297
+ result = evaluate_sort_expression(expr, input, environment)
298
+ result = evaluate_stages(expr[:stages], result, environment) if expr[:stages]
299
+ return result
300
+ end
301
+
302
+ result = environment.base.create_sequence
303
+
304
+ input.each do |item|
305
+ res = evaluate(expr, item, environment)
306
+ expr[:stages]&.each do |stage|
307
+ res = evaluate_filter(stage[:expr], res, environment)
308
+ end
309
+ result.push(res) unless res.nil?
310
+ end
311
+
312
+ result_sequence = environment.base.create_sequence
313
+ if last_step && result.length == 1 && result[0].kind_of?(Array) && !Utils.sequence?(result[0])
314
+ result_sequence = result[0]
315
+ else
316
+ # flatten the sequence
317
+ result.each do |res|
318
+ if !res.kind_of?(Array) || (res.kind_of?(JArray) && res.cons)
319
+ # it's not an array - just push into the result sequence
320
+ result_sequence.push(res)
321
+ else
322
+ # res is a sequence - flatten it into the parent sequence
323
+ res.each { |val| result_sequence.push(val) }
324
+ end
325
+ end
326
+ end
327
+
328
+ result_sequence
329
+ end
330
+
331
+ def evaluate_stages(stages, input, environment)
332
+ result = input
333
+ stages.each do |stage|
334
+ case stage[:type]
335
+ when 'filter'
336
+ result = evaluate_filter(stage[:expr], result, environment)
337
+ when 'index'
338
+ result.each_with_index do |tuple, ee|
339
+ tuple[stage[:value]] = ee
340
+ end
341
+ end
342
+ end
343
+ result
344
+ end
345
+
346
+ # Evaluate a step within a path
347
+ # @param tuple_bindings - The tuple stream
348
+ def evaluate_tuple_step(expr, input, tuple_bindings, environment)
349
+ if expr[:type] == 'sort'
350
+ if tuple_bindings
351
+ result = evaluate_sort_expression(expr, tuple_bindings, environment)
352
+ else
353
+ sorted = evaluate_sort_expression(expr, input, environment)
354
+ result = environment.base.create_sequence
355
+ result.tuple_stream = true
356
+ sorted.each_with_index do |item, ss|
357
+ tuple = {'@' => item}
358
+ tuple[expr[:index]] = ss
359
+ result.push(tuple)
360
+ end
361
+ end
362
+ result = evaluate_stages(expr[:stages], result, environment) if expr[:stages]
363
+ return result
364
+ end
365
+
366
+ result = environment.base.create_sequence
367
+ result.tuple_stream = true
368
+ tuple_bindings = input.map { |item| {'@' => item} } if tuple_bindings.nil?
369
+
370
+ tuple_bindings.each do |binding|
371
+ step_env = create_frame_from_tuple(environment, binding)
372
+ res = evaluate(expr, binding['@'], step_env)
373
+ # res is the binding sequence for the output tuple stream
374
+ next if res.nil?
375
+
376
+ res = [res] unless res.kind_of?(Array)
377
+ res.each_with_index do |item, bb|
378
+ tuple = {}
379
+ tuple.merge!(binding)
380
+ if res.kind_of?(JArray) && res.tuple_stream
381
+ tuple.merge!(item)
382
+ else
383
+ if expr[:focus]
384
+ tuple[expr[:focus]] = item
385
+ tuple['@'] = binding['@']
386
+ else
387
+ tuple['@'] = item
388
+ end
389
+ tuple[expr[:index]] = bb if expr[:index]
390
+ tuple[expr[:ancestor][:label]] = binding['@'] if expr[:ancestor]
391
+ end
392
+ result.push(tuple)
393
+ end
394
+ end
395
+
396
+ result = evaluate_stages(expr[:stages], result, environment) if expr[:stages]
397
+
398
+ result
399
+ end
400
+
401
+ # Apply filter predicate to input data
402
+ # @param predicate - filter expression
403
+ # @param input - Input data to apply predicates against
404
+ # @returns Result after applying predicates
405
+ def evaluate_filter(predicate, input, environment)
406
+ results = environment.base.create_sequence
407
+ results.tuple_stream = true if input.kind_of?(JArray) && input.tuple_stream
408
+ input = environment.base.create_sequence(input) unless input.kind_of?(Array)
409
+ if predicate[:type] == 'number'
410
+ index = predicate[:value].floor # round it down
411
+ if index.negative?
412
+ # count in from end of array
413
+ index = input.length + index
414
+ end
415
+ item = index.negative? ? nil : input[index]
416
+ unless item.nil?
417
+ if item.kind_of?(Array)
418
+ results = item
419
+ else
420
+ results.push(item)
421
+ end
422
+ end
423
+ else
424
+ input.each_with_index do |item, index|
425
+ context = item
426
+ env = environment
427
+ if input.kind_of?(JArray) && input.tuple_stream
428
+ context = item['@']
429
+ env = create_frame_from_tuple(environment, item)
430
+ end
431
+ res = evaluate(predicate, context, env)
432
+ res = [res] if Utils.numeric?(res)
433
+ if Utils.array_of_numbers?(res)
434
+ res.each do |ires|
435
+ # round it down
436
+ ii = ires.floor
437
+ if ii.negative?
438
+ # count in from end of array
439
+ ii = input.length + ii
440
+ end
441
+ results.push(item) if ii == index
442
+ end
443
+ elsif FN.boolean(res) # truthy
444
+ results.push(item)
445
+ end
446
+ end
447
+ end
448
+ results
449
+ end
450
+
451
+ # Evaluate binary expression against input data
452
+ def evaluate_binary(expr, input, environment)
453
+ lhs = evaluate(expr[:lhs], input, environment)
454
+ op = expr[:value]
455
+
456
+ # defer evaluation of RHS to allow short-circuiting
457
+ evalrhs = -> { evaluate(expr[:rhs], input, environment) }
458
+ if op == 'and' || op == 'or'
459
+ begin
460
+ return evaluate_boolean_expression(lhs, evalrhs, op)
461
+ rescue Jsonata::Error => e
462
+ e.position = expr[:position]
463
+ e.token = op
464
+ raise
465
+ end
466
+ end
467
+
468
+ rhs = evalrhs.call
469
+ begin
470
+ case op
471
+ when '+', '-', '*', '/', '%'
472
+ evaluate_numeric_expression(lhs, rhs, op)
473
+ when '=', '!='
474
+ evaluate_equality_expression(lhs, rhs, op)
475
+ when '<', '<=', '>', '>='
476
+ evaluate_comparison_expression(lhs, rhs, op)
477
+ when '&'
478
+ evaluate_string_concat(lhs, rhs)
479
+ when '..'
480
+ evaluate_range_expression(lhs, rhs, environment)
481
+ when 'in'
482
+ evaluate_includes_expression(lhs, rhs)
483
+ end
484
+ rescue Jsonata::Error => e
485
+ e.position = expr[:position]
486
+ e.token = op
487
+ raise
488
+ end
489
+ end
490
+
491
+ # Evaluate unary expression against input data
492
+ def evaluate_unary(expr, input, environment)
493
+ result = nil
494
+
495
+ focus = Focus.new(environment)
496
+ case expr[:value]
497
+ when '-'
498
+ result = evaluate(expr[:expression], input, environment)
499
+ if result.nil?
500
+ result = nil
501
+ elsif Utils.numeric?(result)
502
+ result = -result
503
+ else
504
+ raise Jsonata::Error.new('D1002', :position => expr[:position], :token => expr[:value], :value => result)
505
+ end
506
+ when '['
507
+ # array constructor - evaluate each item
508
+ result = JArray.new
509
+ generators = expr[:expressions].each_with_index.map do |item, idx|
510
+ environment.is_parallel_call = idx.positive?
511
+ [item, evaluate(item, input, environment)]
512
+ end
513
+ generators.each do |item, value|
514
+ next if value.nil?
515
+
516
+ if item[:value] == '['
517
+ result.push(value)
518
+ else
519
+ result = focus.append(result, value)
520
+ end
521
+ end
522
+ if expr[:consarray]
523
+ result = JArray.new(result) unless result.kind_of?(JArray)
524
+ result.cons = true
525
+ end
526
+ when '{'
527
+ # object constructor - apply grouping
528
+ result = evaluate_group_expression(expr, input, environment)
529
+ end
530
+ result
531
+ end
532
+
533
+ # Evaluate name object against input data
534
+ def evaluate_name(expr, input, environment)
535
+ # lookup the 'name' item in the input
536
+ Focus.new(environment).lookup(input, expr[:value])
537
+ end
538
+
539
+ # Evaluate literal against input data
540
+ def evaluate_literal(expr)
541
+ expr[:value]
542
+ end
543
+
544
+ # Evaluate wildcard against input data
545
+ def evaluate_wildcard(_expr, input, environment)
546
+ focus = Focus.new(environment)
547
+ results = focus.create_sequence
548
+ input = input[0] if input.kind_of?(JArray) && input.outer_wrapper && !input.empty?
549
+ if input.kind_of?(Hash) || input.kind_of?(Array)
550
+ Utils.keys(input).each do |key|
551
+ value = input.kind_of?(Array) ? input[key.to_i] : input[key]
552
+ if value.kind_of?(Array)
553
+ value = flatten(value)
554
+ results = focus.append(results, value)
555
+ else
556
+ results.push(value)
557
+ end
558
+ end
559
+ end
560
+
561
+ # result = normalizeSequence(results);
562
+ results
563
+ end
564
+
565
+ # Returns a flattened array
566
+ # @param arg - the array to be flatten
567
+ # @param flattened - carries the flattened array - if not defined, will initialize to []
568
+ def flatten(arg, flattened = [])
569
+ if arg.kind_of?(Array)
570
+ arg.each do |item|
571
+ flatten(item, flattened)
572
+ end
573
+ else
574
+ flattened.push(arg)
575
+ end
576
+ flattened
577
+ end
578
+
579
+ # Evaluate descendants against input data
580
+ def evaluate_descendants(_expr, input, environment)
581
+ result = nil
582
+ result_sequence = environment.base.create_sequence
583
+ unless input.nil?
584
+ # traverse all descendants of this object/array
585
+ recurse_descendants(input, result_sequence)
586
+ result = if result_sequence.length == 1
587
+ result_sequence[0]
588
+ else
589
+ result_sequence
590
+ end
591
+ end
592
+ result
593
+ end
594
+
595
+ # Recurse through descendants
596
+ def recurse_descendants(input, results)
597
+ # this is the equivalent of //* in XPath
598
+ results.push(input) unless input.kind_of?(Array)
599
+ if input.kind_of?(Array)
600
+ input.each do |member|
601
+ recurse_descendants(member, results)
602
+ end
603
+ elsif input.kind_of?(Hash)
604
+ Utils.keys(input).each do |key|
605
+ recurse_descendants(input[key], results)
606
+ end
607
+ end
608
+ end
609
+
610
+ # Evaluate numeric expression against input data
611
+ def evaluate_numeric_expression(lhs, rhs, op)
612
+ raise Jsonata::Error.new('T2001', :value => lhs) if !lhs.nil? && !Utils.numeric?(lhs)
613
+ raise Jsonata::Error.new('T2002', :value => rhs) if !rhs.nil? && !Utils.numeric?(rhs)
614
+
615
+ # if either side is undefined, the result is undefined
616
+ return nil if lhs.nil? || rhs.nil?
617
+
618
+ lhs = lhs.to_f
619
+ rhs = rhs.to_f
620
+ result = case op
621
+ when '+' then lhs + rhs
622
+ when '-' then lhs - rhs
623
+ when '*' then lhs * rhs
624
+ when '/' then lhs / rhs
625
+ when '%' then rhs.zero? ? Float::NAN : lhs.remainder(rhs)
626
+ end
627
+ Utils.normalize_number(result)
628
+ end
629
+
630
+ # Evaluate equality expression against input data
631
+ def evaluate_equality_expression(lhs, rhs, op)
632
+ # if either side is undefined, the result is false
633
+ return false if lhs.nil? || rhs.nil?
634
+
635
+ case op
636
+ when '='
637
+ Utils.deep_equal?(lhs, rhs)
638
+ when '!='
639
+ !Utils.deep_equal?(lhs, rhs)
640
+ end
641
+ end
642
+
643
+ # Evaluate comparison expression against input data
644
+ def evaluate_comparison_expression(lhs, rhs, op)
645
+ # type checks
646
+ ltype = Utils.typeof(lhs)
647
+ rtype = Utils.typeof(rhs)
648
+
649
+ lcomparable = %w[undefined string number].include?(ltype)
650
+ rcomparable = %w[undefined string number].include?(rtype)
651
+
652
+ # if either aa or bb are not comparable (string or numeric) values, then throw an error
653
+ unless lcomparable && rcomparable
654
+ raise Jsonata::Error.new('T2010', :value => %w[string number].include?(ltype) ? rhs : lhs)
655
+ end
656
+
657
+ # if either side is undefined, the result is undefined
658
+ return nil if ltype == 'undefined' || rtype == 'undefined'
659
+
660
+ # if aa and bb are not of the same type
661
+ raise Jsonata::Error.new('T2009', :value => lhs, :value2 => rhs) if ltype != rtype
662
+
663
+ case op
664
+ when '<' then lhs < rhs
665
+ when '<=' then lhs <= rhs
666
+ when '>' then lhs > rhs
667
+ when '>=' then lhs >= rhs
668
+ end
669
+ end
670
+
671
+ # Inclusion operator - in
672
+ # @returns true if lhs is a member of rhs
673
+ def evaluate_includes_expression(lhs, rhs)
674
+ # if either side is undefined, the result is false
675
+ return false if lhs.nil? || rhs.nil?
676
+
677
+ rhs = [rhs] unless rhs.kind_of?(Array)
678
+
679
+ rhs.any? { |item| Utils.strict_equal?(item, lhs) }
680
+ end
681
+
682
+ # Evaluate boolean expression against input data
683
+ # @param evalrhs - function to evaluate RHS value
684
+ def evaluate_boolean_expression(lhs, evalrhs, op)
685
+ l_bool = boolize(lhs)
686
+
687
+ case op
688
+ when 'and'
689
+ l_bool && boolize(evalrhs.call)
690
+ when 'or'
691
+ l_bool || boolize(evalrhs.call)
692
+ end
693
+ end
694
+
695
+ def boolize(value)
696
+ booled_value = FN.boolean(value)
697
+ booled_value.nil? ? false : booled_value
698
+ end
699
+
700
+ # Evaluate string concatenation against input data
701
+ def evaluate_string_concat(lhs, rhs)
702
+ lstr = ''
703
+ rstr = ''
704
+ lstr = FN.string(lhs) unless lhs.nil?
705
+ rstr = FN.string(rhs) unless rhs.nil?
706
+
707
+ lstr + rstr
708
+ end
709
+
710
+ # Evaluate group expression against input data
711
+ def evaluate_group_expression(expr, input, environment)
712
+ result = {}
713
+ groups = {}
714
+ reduce = input.kind_of?(JArray) && input.tuple_stream ? true : false
715
+ focus = Focus.new(environment)
716
+ # group the input sequence by 'key' expression
717
+ input = focus.create_sequence(input) unless input.kind_of?(Array)
718
+ # if the array is empty, add an undefined entry to enable literal JSON object to be generated
719
+ input.push(nil) if input.empty? && !reduce
720
+
721
+ input.each do |item|
722
+ env = reduce ? create_frame_from_tuple(environment, item) : environment
723
+ expr[:lhs].each_with_index do |pair, pair_index|
724
+ key = evaluate(pair[0], reduce ? item['@'] : item, env)
725
+ # key has to be a string
726
+ if !key.kind_of?(String) && !key.nil?
727
+ raise Jsonata::Error.new('T1003', :position => expr[:position], :value => key)
728
+ end
729
+
730
+ next if key.nil?
731
+
732
+ entry = {:data => item, :expr_index => pair_index}
733
+ if groups.key?(key)
734
+ # a value already exists in this slot
735
+ if groups[key][:expr_index] != pair_index
736
+ # this key has been generated by another expression in this group
737
+ # when multiple key expressions evaluate to the same key, then error D1009 must be thrown
738
+ raise Jsonata::Error.new('D1009', :position => expr[:position], :value => key)
739
+ end
740
+
741
+ # append it as an array
742
+ groups[key][:data] = focus.append(groups[key][:data], item)
743
+ else
744
+ groups[key] = entry
745
+ end
746
+ end
747
+ end
748
+
749
+ # iterate over the groups to evaluate the 'value' expression
750
+ generators = groups.keys.each_with_index.map do |key, idx|
751
+ entry = groups[key]
752
+ context = entry[:data]
753
+ env = environment
754
+ if reduce
755
+ tuple = reduce_tuple_stream(entry[:data], environment)
756
+ context = tuple.delete('@')
757
+ env = create_frame_from_tuple(environment, tuple)
758
+ end
759
+ environment.is_parallel_call = idx.positive?
760
+ [key, evaluate(expr[:lhs][entry[:expr_index]][1], context, env)]
761
+ end
762
+
763
+ generators.each do |key, value|
764
+ result[key] = value unless value.nil?
765
+ end
766
+
767
+ result
768
+ end
769
+
770
+ def reduce_tuple_stream(tuple_stream, environment)
771
+ return tuple_stream unless tuple_stream.kind_of?(Array)
772
+
773
+ result = {}
774
+ focus = Focus.new(environment)
775
+ result.merge!(tuple_stream[0])
776
+ tuple_stream.drop(1).each do |tuple|
777
+ Utils.keys(tuple).each do |prop|
778
+ result[prop] = focus.append(result[prop], tuple[prop])
779
+ end
780
+ end
781
+ result
782
+ end
783
+
784
+ # Evaluate range expression against input data
785
+ # @returns Resultant array
786
+ def evaluate_range_expression(lhs, rhs, environment)
787
+ raise Jsonata::Error.new('T2003', :value => lhs) if !lhs.nil? && !Utils.integer?(lhs)
788
+ raise Jsonata::Error.new('T2004', :value => rhs) if !rhs.nil? && !Utils.integer?(rhs)
789
+
790
+ # if either side is undefined, the result is undefined
791
+ return nil if lhs.nil? || rhs.nil?
792
+
793
+ # if the lhs is greater than the rhs, return undefined
794
+ return nil if lhs > rhs
795
+
796
+ # limit the size of the array to ten million entries (1e7)
797
+ # this is an implementation defined limit to protect against
798
+ # memory and performance issues. This value may increase in the future.
799
+ size = (rhs - lhs + 1).to_i
800
+ raise Jsonata::Error.new('D2014', :value => size) if size > 1e7
801
+
802
+ options = environment.base.options
803
+ if options && options[:sequence] && size > options[:sequence]
804
+ raise Jsonata::Error.new('D2015', :value => size)
805
+ end
806
+
807
+ result = JArray.new((lhs.to_i..rhs.to_i).to_a)
808
+ result.sequence = true
809
+ result
810
+ end
811
+
812
+ # Evaluate bind expression against input data
813
+ def evaluate_bind_expression(expr, input, environment)
814
+ # The RHS is the expression to evaluate
815
+ # The LHS is the name of the variable to bind to - should be a VARIABLE token (enforced by parser)
816
+ value = evaluate(expr[:rhs], input, environment)
817
+ environment.bind(expr[:lhs][:value], value)
818
+ value
819
+ end
820
+
821
+ # Evaluate condition against input data
822
+ def evaluate_condition(expr, input, environment)
823
+ result = nil
824
+ condition = evaluate(expr[:condition], input, environment)
825
+ if FN.boolean(condition)
826
+ result = evaluate(expr[:then], input, environment)
827
+ elsif expr.key?(:else)
828
+ result = evaluate(expr[:else], input, environment)
829
+ end
830
+ result
831
+ end
832
+
833
+ # Evaluate block against input data
834
+ def evaluate_block(expr, input, environment)
835
+ result = nil
836
+ # create a new frame to limit the scope of variable assignments
837
+ # TODO, only do this if the post-parse stage has flagged this as required
838
+ frame = create_frame(environment)
839
+ # invoke each expression in turn
840
+ # only return the result of the last one
841
+ expr[:expressions].each do |expression|
842
+ result = evaluate(expression, input, frame)
843
+ end
844
+
845
+ result
846
+ end
847
+
848
+ # Prepare a regex
849
+ # @param expr - expression containing regex
850
+ # @returns Higher order function representing prepared regex
851
+ def evaluate_regex(expr, _input, environment)
852
+ engine = environment.base.regex_engine
853
+ re = engine ? engine.call(expr[:value].source, expr[:value].flags) : expr[:value].regexp
854
+ last_index = 0
855
+ closure = nil
856
+ closure = lambda do |str, from_index = nil|
857
+ result = nil
858
+ last_index = from_index || 0
859
+ match = last_index > str.length ? nil : re.match(str, last_index)
860
+ if match.nil?
861
+ last_index = 0
862
+ else
863
+ last_index = match.end(0)
864
+ result = {
865
+ 'match' => match[0],
866
+ 'start' => match.begin(0),
867
+ 'end' => match.end(0),
868
+ 'groups' => match.captures
869
+ }
870
+ result['next'] = lambda do
871
+ if last_index >= str.length
872
+ nil
873
+ else
874
+ nxt = closure.call(str, last_index)
875
+ if nxt && nxt['match'] == ''
876
+ # matches zero length string; this will never progress
877
+ raise Jsonata::Error.new('D1004', :position => expr[:position], :value => expr[:value].source)
878
+ end
879
+
880
+ nxt
881
+ end
882
+ end
883
+ end
884
+
885
+ result
886
+ end
887
+ closure
888
+ end
889
+
890
+ # Evaluate variable against input data
891
+ def evaluate_variable(expr, input, environment)
892
+ # lookup the variable value in the environment
893
+ # if the variable name is empty string, then it refers to context value
894
+ if expr[:value] == ''
895
+ input.kind_of?(JArray) && input.outer_wrapper ? input[0] : input
896
+ else
897
+ environment.lookup(expr[:value])
898
+ end
899
+ end
900
+
901
+ # sort / order-by operator
902
+ # @param expr - AST for operator
903
+ # @returns Ordered sequence
904
+ def evaluate_sort_expression(expr, input, environment)
905
+ # evaluate the lhs, then sort the results in order according to rhs expression
906
+ lhs = input
907
+ is_tuple_sort = input.kind_of?(JArray) && input.tuple_stream ? true : false
908
+
909
+ # sort the lhs array
910
+ # use comparator function
911
+ comparator = lambda do |a, b|
912
+ # expr.terms is an array of order-by in priority order
913
+ comp = 0
914
+ expr[:terms].each do |term|
915
+ break unless comp.zero?
916
+
917
+ # evaluate the sort term in the context of a
918
+ context = a
919
+ env = environment
920
+ if is_tuple_sort
921
+ context = a['@']
922
+ env = create_frame_from_tuple(environment, a)
923
+ end
924
+ aa = evaluate(term[:expression], context, env)
925
+ # evaluate the sort term in the context of b
926
+ context = b
927
+ env = environment
928
+ if is_tuple_sort
929
+ context = b['@']
930
+ env = create_frame_from_tuple(environment, b)
931
+ end
932
+ bb = evaluate(term[:expression], context, env)
933
+
934
+ # type checks
935
+ atype = Utils.typeof(aa)
936
+ btype = Utils.typeof(bb)
937
+ # undefined should be last in sort order
938
+ if atype == 'undefined'
939
+ # swap them, unless btype is also undefined
940
+ comp = btype == 'undefined' ? 0 : 1
941
+ next
942
+ end
943
+ if btype == 'undefined'
944
+ comp = -1
945
+ next
946
+ end
947
+
948
+ # if aa or bb are not string or numeric values, then throw an error
949
+ if !(atype == 'string' || atype == 'number') || !(btype == 'string' || btype == 'number')
950
+ raise Jsonata::Error.new('T2008', :position => expr[:position],
951
+ :value => !(atype == 'string' || atype == 'number') ? aa : bb)
952
+ end
953
+
954
+ # if aa and bb are not of the same type
955
+ raise Jsonata::Error.new('T2007', :position => expr[:position], :value => aa, :value2 => bb) if atype != btype
956
+
957
+ # both the same - move on to next term
958
+ next if aa == bb
959
+
960
+ comp = aa < bb ? -1 : 1
961
+ comp = -comp if term[:descending] == true
962
+ end
963
+ # only swap a & b if comp equals 1
964
+ comp == 1
965
+ end
966
+
967
+ focus = Focus.new(environment, input)
968
+ # the `focus` is passed in as the `this` for the invoked function
969
+ focus.sort(lhs, comparator)
970
+ end
971
+
972
+ # create a transformer function
973
+ # @param expr - AST for operator
974
+ # @returns tranformer function
975
+ def evaluate_transform_expression(expr, _input, environment)
976
+ # create a function to implement the transform definition
977
+ transformer = lambda do |obj| # signature <(oa):o>
978
+ # undefined inputs always return undefined
979
+ next nil if obj.nil?
980
+
981
+ # this function returns a copy of obj with changes specified by the pattern/operation
982
+ clone_function = environment.lookup('clone')
983
+ unless Utils.function?(clone_function)
984
+ # throw type error
985
+ raise Jsonata::Error.new('T2013', :position => expr[:position])
986
+ end
987
+
988
+ result = apply(clone_function, [obj], nil, environment)
989
+ matches = evaluate(expr[:pattern], result, environment)
990
+ unless matches.nil?
991
+ matches = [matches] unless matches.kind_of?(Array)
992
+ matches.each do |match|
993
+ # evaluate the update value for each match
994
+ update = evaluate(expr[:update], match, environment)
995
+ # update must be an object
996
+ unless update.nil?
997
+ unless update.kind_of?(Hash)
998
+ # throw type error
999
+ raise Jsonata::Error.new('T2011', :position => expr[:update][:position], :value => update)
1000
+ end
1001
+
1002
+ # merge the update
1003
+ if match.kind_of?(Hash)
1004
+ Utils.keys(update).each do |prop|
1005
+ match[prop] = update[prop]
1006
+ end
1007
+ end
1008
+ end
1009
+
1010
+ # delete, if specified, must be an array of strings (or single string)
1011
+ next unless expr.key?(:delete)
1012
+
1013
+ deletions = evaluate(expr[:delete], match, environment)
1014
+ next if deletions.nil?
1015
+
1016
+ val = deletions
1017
+ deletions = [deletions] unless deletions.kind_of?(Array)
1018
+ unless Utils.array_of_strings?(deletions)
1019
+ # throw type error
1020
+ raise Jsonata::Error.new('T2012', :position => expr[:delete][:position], :value => val)
1021
+ end
1022
+
1023
+ deletions.each do |deletion|
1024
+ match.delete(deletion) if match.kind_of?(Hash)
1025
+ end
1026
+ end
1027
+ end
1028
+
1029
+ result
1030
+ end
1031
+
1032
+ define_function(transformer, '<(oa):o>')
1033
+ end
1034
+
1035
+ # Apply the function on the RHS using the sequence on the LHS as the first argument
1036
+ def evaluate_apply_expression(expr, input, environment)
1037
+ lhs = evaluate(expr[:lhs], input, environment)
1038
+ if expr[:rhs][:type] == 'function'
1039
+ # this is a function _invocation_; invoke it with lhs expression as the first argument
1040
+ evaluate_function(expr[:rhs], input, environment, {:context => lhs})
1041
+ else
1042
+ func = evaluate(expr[:rhs], input, environment)
1043
+
1044
+ raise Jsonata::Error.new('T2006', :position => expr[:position], :value => func) unless Utils.function?(func)
1045
+
1046
+ if Utils.function?(lhs)
1047
+ # this is function chaining (func1 ~> func2)
1048
+ # function($f, $g) { function($x){ $g($f($x)) } }
1049
+ chain = evaluate(CHAIN_AST, nil, environment)
1050
+ apply(chain, [lhs, func], nil, environment)
1051
+ else
1052
+ apply(func, [lhs], nil, environment)
1053
+ end
1054
+ end
1055
+ end
1056
+
1057
+ # Evaluate function against input data
1058
+ def evaluate_function(expr, input, environment, applyto = nil)
1059
+ # create the procedure
1060
+ # can't assume that expr.procedure is a lambda type directly
1061
+ # could be an expression that evaluates to a function (e.g. variable reference, parens expr etc.
1062
+ # evaluate it generically first, then check that it is a function. Throw error if not.
1063
+ proc = evaluate(expr[:procedure], input, environment)
1064
+
1065
+ if proc.nil? && expr[:procedure][:type] == 'path' &&
1066
+ Utils.truthy?(environment.lookup(expr[:procedure][:steps][0][:value]))
1067
+ # help the user out here if they simply forgot the leading $
1068
+ raise Jsonata::Error.new('T1005', :position => expr[:position], :token => expr[:procedure][:steps][0][:value])
1069
+ end
1070
+
1071
+ evaluated_args = []
1072
+ evaluated_args.push(applyto[:context]) unless applyto.nil?
1073
+ # eager evaluation - evaluate the arguments
1074
+ expr[:arguments].each do |argument|
1075
+ arg = evaluate(argument, input, environment)
1076
+ if Utils.function?(arg)
1077
+ # wrap this in a closure
1078
+ closure = Closure.new(Utils.get_function_arity(arg)) do |*params|
1079
+ # invoke func
1080
+ apply(arg, params, nil, environment)
1081
+ end
1082
+ evaluated_args.push(closure)
1083
+ else
1084
+ evaluated_args.push(arg)
1085
+ end
1086
+ end
1087
+ # apply the procedure
1088
+ proc_name = expr[:procedure][:type] == 'path' ? expr[:procedure][:steps][0][:value] : expr[:procedure][:value]
1089
+ begin
1090
+ if proc.kind_of?(Lambda) || proc.kind_of?(NativeFunction)
1091
+ proc.token = proc_name
1092
+ proc.position = expr[:position]
1093
+ end
1094
+ apply(proc, evaluated_args, input, environment)
1095
+ rescue Jsonata::Error => e
1096
+ # add the position field to the error
1097
+ e.position = expr[:position] unless Utils.truthy?(e.position)
1098
+ # and the function identifier
1099
+ e.token = proc_name unless Utils.truthy?(e.token)
1100
+ raise
1101
+ end
1102
+ end
1103
+
1104
+ # Apply procedure or function
1105
+ # @param proc - Procedure
1106
+ # @param args - Arguments
1107
+ # @returns Result of procedure
1108
+ def apply(proc, args, input, environment)
1109
+ result = apply_inner(proc, args, input, environment)
1110
+ while Utils.lambda?(result) && result.thunk == true
1111
+ # trampoline loop - this gets invoked as a result of tail-call optimization
1112
+ # the function returned a tail-call thunk
1113
+ # unpack it, evaluate its arguments, and apply the tail call
1114
+ nxt = evaluate(result.body[:procedure], result.input, result.environment)
1115
+ if nxt.kind_of?(Lambda) || nxt.kind_of?(NativeFunction)
1116
+ nxt.token = result.body[:procedure][:value] if result.body[:procedure][:type] == 'variable'
1117
+ nxt.position = result.body[:procedure][:position]
1118
+ end
1119
+ evaluated_args = result.body[:arguments].map do |argument|
1120
+ evaluate(argument, result.input, result.environment)
1121
+ end
1122
+
1123
+ result = apply_inner(nxt, evaluated_args, input, environment)
1124
+ end
1125
+ result
1126
+ end
1127
+
1128
+ # Apply procedure or function
1129
+ # @param proc - Procedure
1130
+ # @param args - Arguments
1131
+ # @returns Result of procedure
1132
+ def apply_inner(proc, args, input, environment)
1133
+ validated_args = args
1134
+ if Utils.truthy?(proc) && (proc.kind_of?(Lambda) || proc.kind_of?(NativeFunction))
1135
+ validated_args = validate_arguments(proc.signature, args, input)
1136
+ end
1137
+
1138
+ if Utils.lambda?(proc)
1139
+ apply_procedure(proc, validated_args)
1140
+ elsif proc.kind_of?(NativeFunction)
1141
+ focus = Focus.new(environment, input)
1142
+ # the `focus` is passed in as the `this` for the invoked function
1143
+ Utils.invoke(proc.implementation, validated_args, focus)
1144
+ elsif Utils.callable?(proc)
1145
+ # typically these are functions that are returned by the invocation of plugin functions
1146
+ Utils.invoke(proc, validated_args)
1147
+ else
1148
+ raise Jsonata::Error.new('T1006')
1149
+ end
1150
+ rescue Jsonata::Error => e
1151
+ if proc.kind_of?(Lambda) || proc.kind_of?(NativeFunction)
1152
+ e.token = proc.token if e.token.nil? && !proc.token.nil?
1153
+ e.position = proc.position if Utils.truthy?(proc.position)
1154
+ end
1155
+ raise
1156
+ end
1157
+
1158
+ # Evaluate lambda against input data
1159
+ def evaluate_lambda(expr, input, environment)
1160
+ # make a function (closure)
1161
+ procedure = Lambda.new(
1162
+ :input => input,
1163
+ :environment => environment,
1164
+ :arguments => expr[:arguments],
1165
+ :signature => expr[:signature],
1166
+ :body => expr[:body]
1167
+ )
1168
+ procedure.thunk = true if expr[:thunk] == true
1169
+ procedure.apply_proc = lambda do |this, args|
1170
+ apply(procedure, args, input, this ? this.environment : environment)
1171
+ end
1172
+ procedure
1173
+ end
1174
+
1175
+ # Evaluate partial application
1176
+ def evaluate_partial_application(expr, input, environment)
1177
+ # partially apply a function
1178
+ # evaluate the arguments
1179
+ evaluated_args = expr[:arguments].map do |arg|
1180
+ if arg[:type] == 'operator' && arg[:value] == '?'
1181
+ arg
1182
+ else
1183
+ evaluate(arg, input, environment)
1184
+ end
1185
+ end
1186
+ # lookup the procedure
1187
+ proc = evaluate(expr[:procedure], input, environment)
1188
+ if proc.nil? && expr[:procedure][:type] == 'path' &&
1189
+ Utils.truthy?(environment.lookup(expr[:procedure][:steps][0][:value]))
1190
+ # help the user out here if they simply forgot the leading $
1191
+ raise Jsonata::Error.new('T1007', :position => expr[:position], :token => expr[:procedure][:steps][0][:value])
1192
+ end
1193
+
1194
+ if Utils.lambda?(proc)
1195
+ partial_apply_procedure(proc, evaluated_args, environment)
1196
+ elsif proc.kind_of?(NativeFunction)
1197
+ partial_apply_native_function(proc.implementation, evaluated_args, environment)
1198
+ elsif Utils.callable?(proc)
1199
+ partial_apply_native_function(proc, evaluated_args, environment)
1200
+ else
1201
+ raise Jsonata::Error.new('T1008', :position => expr[:position],
1202
+ :token => if expr[:procedure][:type] == 'path'
1203
+ expr[:procedure][:steps][0][:value]
1204
+ else
1205
+ expr[:procedure][:value]
1206
+ end)
1207
+ end
1208
+ end
1209
+
1210
+ # Validate the arguments against the signature validator (if it exists)
1211
+ # @param signature - validator function
1212
+ # @param args - function arguments
1213
+ # @param context - context value
1214
+ # @returns validated arguments
1215
+ def validate_arguments(signature, args, context)
1216
+ # nothing to validate
1217
+ return args if signature.nil?
1218
+
1219
+ signature.validate(args, context)
1220
+ end
1221
+
1222
+ # Apply procedure
1223
+ def apply_procedure(proc, args)
1224
+ env = create_frame(proc.environment)
1225
+ proc.arguments.each_with_index do |param, index|
1226
+ env.bind(param[:value], args[index])
1227
+ end
1228
+ if proc.body.kind_of?(Hash)
1229
+ evaluate(proc.body, proc.input, env)
1230
+ else
1231
+ # this is a lambda that wraps a native function - generated by partially evaluating a native
1232
+ apply_native_function(proc.body, env)
1233
+ end
1234
+ end
1235
+
1236
+ # Partially apply procedure
1237
+ # @param proc - Procedure (a Lambda, or the AST of a lambda wrapping a native function)
1238
+ # @returns Result of partially applied procedure
1239
+ def partial_apply_procedure(proc, args, environment)
1240
+ proc = Lambda.new(:arguments => proc[:arguments], :body => proc[:body]) if proc.kind_of?(Hash)
1241
+ # create a closure, bind the supplied parameters and return a function that takes the remaining (?) parameters
1242
+ env = create_frame(proc.environment || environment)
1243
+ unbound_args = []
1244
+ proc.arguments.each_with_index do |param, index|
1245
+ arg = args[index]
1246
+ if arg.kind_of?(Hash) && arg[:type] == 'operator' && arg[:value] == '?'
1247
+ unbound_args.push(param)
1248
+ else
1249
+ env.bind(param[:value], arg)
1250
+ end
1251
+ end
1252
+ Lambda.new(
1253
+ :input => proc.input,
1254
+ :environment => env,
1255
+ :arguments => unbound_args,
1256
+ :body => proc.body
1257
+ )
1258
+ end
1259
+
1260
+ # Partially apply native function
1261
+ # @param native - Native function
1262
+ # @returns Result of partially applying native function
1263
+ def partial_apply_native_function(native, args, environment)
1264
+ # create a lambda function that wraps and invokes the native function
1265
+ # get the list of declared arguments from the native function
1266
+ sig_args = get_native_function_arguments(native)
1267
+ sig_args = sig_args.map { |sig_arg| "$#{sig_arg.strip}" }
1268
+ body = "function(#{sig_args.join(', ')}){ _ }"
1269
+
1270
+ body_ast = Parser.parse(body)
1271
+ body_ast[:body] = native
1272
+
1273
+ partial_apply_procedure(body_ast, args, environment)
1274
+ end
1275
+
1276
+ # Apply native function
1277
+ def apply_native_function(proc, env)
1278
+ sig_args = get_native_function_arguments(proc)
1279
+ # generate the array of arguments for invoking the function - look them up in the environment
1280
+ args = sig_args.map { |sig_arg| env.lookup(sig_arg.strip) }
1281
+
1282
+ focus = Focus.new(env)
1283
+ Utils.invoke(proc, args, focus)
1284
+ end
1285
+
1286
+ # Get native function arguments
1287
+ # @param func - Function
1288
+ # @returns Native function arguments
1289
+ def get_native_function_arguments(func)
1290
+ Utils.parameter_names(func)
1291
+ end
1292
+
1293
+ # Creates a function definition
1294
+ # @param func - function implementation in Ruby
1295
+ # @param signature - JSONata function signature definition
1296
+ # @returns function definition
1297
+ def define_function(func, signature = nil)
1298
+ NativeFunction.new(func, signature.nil? ? nil : Signature.new(signature))
1299
+ end
1300
+
1301
+ # Create frame
1302
+ # @param enclosing_environment - Enclosing environment
1303
+ # @returns Created frame
1304
+ def create_frame(enclosing_environment)
1305
+ Frame.new(enclosing_environment)
1306
+ end
1307
+
1308
+ # lookup a message template from the catalog and substitute the inserts.
1309
+ # Populates `err.message` with the substituted message. Leaves `err.message`
1310
+ # untouched if code lookup fails.
1311
+ # @param err - error to populate
1312
+ def populate_message(err)
1313
+ template = ERROR_CODES[err.code]
1314
+ return if template.nil?
1315
+
1316
+ field = lambda do |name|
1317
+ name = 'function_name' if name == 'functionName'
1318
+ err.respond_to?(name) ? err.public_send(name) : nil
1319
+ end
1320
+ # if there are any handlebars, replace them with the field references
1321
+ # triple braces - replace with value
1322
+ # double braces - replace with json stringified value
1323
+ message = template.gsub(/\{\{\{([^}]+)}}}/) do
1324
+ val = field.call(Regexp.last_match(1))
1325
+ val.nil? ? 'undefined' : val.to_s
1326
+ end
1327
+ message = message.gsub(/\{\{([^}]+)}}/) do
1328
+ Utils.json_stringify(field.call(Regexp.last_match(1))) || 'undefined'
1329
+ end
1330
+ err.message = message
1331
+ # Otherwise retain the original `err.message`
1332
+ end
1333
+
1334
+ # Error codes
1335
+ #
1336
+ # Sxxxx - Static errors (compile time)
1337
+ # Txxxx - Type errors
1338
+ # Dxxxx - Dynamic errors (evaluate time)
1339
+ # 01xx - tokenizer
1340
+ # 02xx - parser
1341
+ # 03xx - regex parser
1342
+ # 04xx - function signature parser/evaluator
1343
+ # 10xx - evaluator
1344
+ # 20xx - operators
1345
+ # 3xxx - functions (blocks of 10 for each function)
1346
+ ERROR_CODES = {
1347
+ 'S0101' => 'String literal must be terminated by a matching quote',
1348
+ 'S0102' => 'Number out of range: {{token}}',
1349
+ 'S0103' => 'Unsupported escape sequence: \\{{token}}',
1350
+ 'S0104' => 'The escape sequence \\u must be followed by 4 hex digits',
1351
+ 'S0105' => 'Quoted property name must be terminated with a backquote (`)',
1352
+ 'S0106' => 'Comment has no closing tag',
1353
+ 'S0201' => 'Syntax error: {{token}}',
1354
+ 'S0202' => 'Expected {{value}}, got {{token}}',
1355
+ 'S0203' => 'Expected {{value}} before end of expression',
1356
+ 'S0204' => 'Unknown operator: {{token}}',
1357
+ 'S0205' => 'Unexpected token: {{token}}',
1358
+ 'S0206' => 'Unknown expression type: {{token}}',
1359
+ 'S0207' => 'Unexpected end of expression',
1360
+ 'S0208' => 'Parameter {{value}} of function definition must be a variable name (start with $)',
1361
+ 'S0209' => 'A predicate cannot follow a grouping expression in a step',
1362
+ 'S0210' => 'Each step can only have one grouping expression',
1363
+ 'S0211' => 'The symbol {{token}} cannot be used as a unary operator',
1364
+ 'S0212' => 'The left side of := must be a variable name (start with $)',
1365
+ 'S0213' => 'The literal value {{value}} cannot be used as a step within a path expression',
1366
+ 'S0214' => 'The right side of {{token}} must be a variable name (start with $)',
1367
+ 'S0215' => 'A context variable binding must precede any predicates on a step',
1368
+ 'S0216' => "A context variable binding must precede the 'order-by' clause on a step",
1369
+ 'S0217' => "The object representing the 'parent' cannot be derived from this expression",
1370
+ 'S0301' => 'Empty regular expressions are not allowed',
1371
+ 'S0302' => 'No terminating / in regular expression',
1372
+ 'S0402' => 'Choice groups containing parameterized types are not supported',
1373
+ 'S0401' => 'Type parameters can only be applied to functions and arrays',
1374
+ 'S0500' => 'Attempted to evaluate an expression containing syntax error(s)',
1375
+ 'T0410' => 'Argument {{index}} of function {{token}} does not match function signature',
1376
+ 'T0411' => 'Context value is not a compatible type with argument {{index}} of function {{token}}',
1377
+ 'T0412' => 'Argument {{index}} of function {{token}} must be an array of {{type}}',
1378
+ 'D1001' => 'Number out of range: {{value}}',
1379
+ 'D1002' => 'Cannot negate a non-numeric value: {{value}}',
1380
+ 'T1003' => 'Key in object structure must evaluate to a string; got: {{value}}',
1381
+ 'D1004' => 'Regular expression matches zero length string',
1382
+ 'T1005' => 'Attempted to invoke a non-function. Did you mean ${{{token}}}?',
1383
+ 'T1006' => 'Attempted to invoke a non-function',
1384
+ 'T1007' => 'Attempted to partially apply a non-function. Did you mean ${{{token}}}?',
1385
+ 'T1008' => 'Attempted to partially apply a non-function',
1386
+ 'D1009' => 'Multiple key definitions evaluate to same key: {{value}}',
1387
+ 'T1010' => 'The matcher function argument passed to function {{token}} does not return the correct object structure',
1388
+ 'D1011' => 'Stack overflow. Check for non-terminating recursive function. Consider rewriting as tail-recursive',
1389
+ 'D1012' => 'Evaluation timeout after {{value}} milliseconds. Check for infinite loop',
1390
+ 'D1013' => 'Object property names starting with _jsonata_ are reserved for internal use: {{value}}',
1391
+ 'T2001' => 'The left side of the {{token}} operator must evaluate to a number',
1392
+ 'T2002' => 'The right side of the {{token}} operator must evaluate to a number',
1393
+ 'T2003' => 'The left side of the range operator (..) must evaluate to an integer',
1394
+ 'T2004' => 'The right side of the range operator (..) must evaluate to an integer',
1395
+ 'D2005' => 'The left side of := must be a variable name (start with $)', # defunct - replaced by S0212 parser error
1396
+ 'T2006' => 'The right side of the function application operator ~> must be a function',
1397
+ 'T2007' => 'Type mismatch when comparing values {{value}} and {{value2}} in order-by clause',
1398
+ 'T2008' => 'The expressions within an order-by clause must evaluate to numeric or string values',
1399
+ 'T2009' => 'The values {{value}} and {{value2}} either side of operator {{token}} must be of the same data type',
1400
+ 'T2010' => 'The expressions either side of operator {{token}} must evaluate to numeric or string values',
1401
+ 'T2011' => 'The insert/update clause of the transform expression must evaluate to an object: {{value}}',
1402
+ 'T2012' => 'The delete clause of the transform expression must evaluate to a string or array of strings: {{value}}',
1403
+ 'T2013' => 'The transform expression clones the input object using the $clone() function. This has been overridden in the current scope by a non-function.',
1404
+ 'D2014' => 'The size of the sequence allocated by the range operator (..) must not exceed 1e7. Attempted to allocate {{value}}.',
1405
+ 'D2015' => 'The maximum sequence length of {{value}} was exceeded.',
1406
+ 'D3001' => 'Attempting to invoke string function on Infinity or NaN',
1407
+ 'D3010' => 'Second argument of replace function cannot be an empty string',
1408
+ 'D3011' => 'Fourth argument of replace function must evaluate to a positive number',
1409
+ 'D3012' => 'Attempted to replace a matched string with a non-string value',
1410
+ 'D3020' => 'Third argument of split function must evaluate to a positive number',
1411
+ 'D3030' => 'Unable to cast value to a number: {{value}}',
1412
+ 'D3040' => 'Third argument of match function must evaluate to a positive number',
1413
+ 'D3050' => 'The second argument of reduce function must be a function with at least two arguments',
1414
+ 'D3060' => 'The sqrt function cannot be applied to a negative number: {{value}}',
1415
+ 'D3061' => 'The power function has resulted in a value that cannot be represented as a JSON number: base={{value}}, exponent={{exp}}',
1416
+ 'D3070' => 'The single argument form of the sort function can only be applied to an array of strings or an array of numbers. Use the second argument to specify a comparison function',
1417
+ 'D3080' => 'The picture string must only contain a maximum of two sub-pictures',
1418
+ 'D3081' => "The sub-picture must not contain more than one instance of the 'decimal-separator' character",
1419
+ 'D3082' => "The sub-picture must not contain more than one instance of the 'percent' character",
1420
+ 'D3083' => "The sub-picture must not contain more than one instance of the 'per-mille' character",
1421
+ 'D3084' => "The sub-picture must not contain both a 'percent' and a 'per-mille' character",
1422
+ 'D3085' => "The mantissa part of a sub-picture must contain at least one character that is either an 'optional digit character' or a member of the 'decimal digit family'",
1423
+ 'D3086' => 'The sub-picture must not contain a passive character that is preceded by an active character and that is followed by another active character',
1424
+ 'D3087' => "The sub-picture must not contain a 'grouping-separator' character that appears adjacent to a 'decimal-separator' character",
1425
+ 'D3088' => "The sub-picture must not contain a 'grouping-separator' at the end of the integer part",
1426
+ 'D3089' => "The sub-picture must not contain two adjacent instances of the 'grouping-separator' character",
1427
+ 'D3090' => "The integer part of the sub-picture must not contain a member of the 'decimal digit family' that is followed by an instance of the 'optional digit character'",
1428
+ 'D3091' => "The fractional part of the sub-picture must not contain an instance of the 'optional digit character' that is followed by a member of the 'decimal digit family'",
1429
+ 'D3092' => "A sub-picture that contains a 'percent' or 'per-mille' character must not contain a character treated as an 'exponent-separator'",
1430
+ 'D3093' => "The exponent part of the sub-picture must comprise only of one or more characters that are members of the 'decimal digit family'",
1431
+ 'D3100' => 'The radix of the formatBase function must be between 2 and 36. It was given {{value}}',
1432
+ 'D3110' => 'The argument of the toMillis function must be an ISO 8601 formatted timestamp. Given {{value}}',
1433
+ 'D3120' => 'Syntax error in expression passed to function eval: {{value}}',
1434
+ 'D3121' => 'Dynamic error evaluating the expression passed to function eval: {{value}}',
1435
+ 'D3130' => 'Formatting or parsing an integer as a sequence starting with {{value}} is not supported by this implementation',
1436
+ 'D3131' => 'In a decimal digit pattern, all digits must be from the same decimal group',
1437
+ 'D3132' => 'Unknown component specifier {{value}} in date/time picture string',
1438
+ 'D3133' => "The 'name' modifier can only be applied to months and days in the date/time picture string, not {{value}}",
1439
+ 'D3134' => 'The timezone integer format specifier cannot have more than four digits',
1440
+ 'D3135' => "No matching closing bracket ']' in date/time picture string",
1441
+ 'D3136' => 'The date/time picture string is missing specifiers required to parse the timestamp',
1442
+ 'D3137' => '{{{message}}}',
1443
+ 'D3138' => 'The $single() function expected exactly 1 matching result. Instead it matched more.',
1444
+ 'D3139' => 'The $single() function expected exactly 1 matching result. Instead it matched 0.',
1445
+ 'D3140' => 'Malformed URL passed to ${{{functionName}}}(): {{value}}',
1446
+ 'D3141' => '{{{message}}}'
1447
+ }.freeze
1448
+
1449
+ # A focus object for invoking functions that don't require one
1450
+ FN = Focus.new
1451
+
1452
+ CHAIN_AST = Parser.parse('function($f, $g) { function($x){ $g($f($x)) } }')
1453
+
1454
+ STATIC_FRAME = Frame.new(nil)
1455
+
1456
+ # Function registration
1457
+ {
1458
+ 'sum' => [:sum, '<a<n>:n>'],
1459
+ 'count' => [:count, '<a:n>'],
1460
+ 'max' => [:max, '<a<n>:n>'],
1461
+ 'min' => [:min, '<a<n>:n>'],
1462
+ 'average' => [:average, '<a<n>:n>'],
1463
+ 'string' => [:string, '<x-b?:s>'],
1464
+ 'substring' => [:substring, '<s-nn?:s>'],
1465
+ 'substringBefore' => [:substring_before, '<s-s:s>'],
1466
+ 'substringAfter' => [:substring_after, '<s-s:s>'],
1467
+ 'lowercase' => [:lowercase, '<s-:s>'],
1468
+ 'uppercase' => [:uppercase, '<s-:s>'],
1469
+ 'length' => [:length, '<s-:n>'],
1470
+ 'trim' => [:trim, '<s-:s>'],
1471
+ 'pad' => [:pad, '<s-ns?:s>'],
1472
+ 'match' => [:match, '<s-f<s:o>n?:a<o>>'],
1473
+ 'contains' => [:contains, '<s-(sf):b>'], # TODO <s-(sf<s:o>):b>
1474
+ 'replace' => [:replace, '<s-(sf)(sf)n?:s>'], # TODO <s-(sf<s:o>)(sf<o:s>)n?:s>
1475
+ 'split' => [:split, '<s-(sf)n?:a<s>>'], # TODO <s-(sf<s:o>)n?:a<s>>
1476
+ 'join' => [:join, '<a<s>s?:s>'],
1477
+ 'formatNumber' => [:format_number, '<n-so?:s>'],
1478
+ 'formatBase' => [:format_base, '<n-n?:s>'],
1479
+ 'formatInteger' => [:format_integer, '<n-s:s>'],
1480
+ 'parseInteger' => [:parse_integer, '<s-s:n>'],
1481
+ 'number' => [:number, '<(nsb)-:n>'],
1482
+ 'floor' => [:floor, '<n-:n>'],
1483
+ 'ceil' => [:ceil, '<n-:n>'],
1484
+ 'round' => [:round, '<n-n?:n>'],
1485
+ 'abs' => [:abs, '<n-:n>'],
1486
+ 'sqrt' => [:sqrt, '<n-:n>'],
1487
+ 'power' => [:power, '<n-n:n>'],
1488
+ 'random' => [:random, '<:n>'],
1489
+ 'boolean' => [:boolean, '<x-:b>'],
1490
+ 'not' => [:not, '<x-:b>'],
1491
+ 'map' => [:map, '<af>'],
1492
+ 'zip' => [:zip, '<a+>'],
1493
+ 'filter' => [:filter, '<af>'],
1494
+ 'single' => [:single, '<af?>'],
1495
+ 'reduce' => [:fold_left, '<afj?:j>'], # TODO <f<jj:j>a<j>j?:j>
1496
+ 'sift' => [:sift, '<o-f?:o>'],
1497
+ 'keys' => [:keys, '<x-:a<s>>'],
1498
+ 'lookup' => [:lookup, '<x-s:x>'],
1499
+ 'append' => [:append, '<xx:a>'],
1500
+ 'exists' => [:exists, '<x:b>'],
1501
+ 'spread' => [:spread, '<x-:a<o>>'],
1502
+ 'merge' => [:merge, '<a<o>:o>'],
1503
+ 'reverse' => [:reverse, '<a:a>'],
1504
+ 'each' => [:each, '<o-f:a>'],
1505
+ 'error' => [:error, '<s?:x>'],
1506
+ 'assert' => [:assert, '<bs?:x>'],
1507
+ 'type' => [:type, '<x:s>'],
1508
+ 'sort' => [:sort, '<af?:a>'],
1509
+ 'shuffle' => [:shuffle, '<a:a>'],
1510
+ 'distinct' => [:distinct, '<x:x>'],
1511
+ 'base64encode' => [:base64encode, '<s-:s>'],
1512
+ 'base64decode' => [:base64decode, '<s-:s>'],
1513
+ 'encodeUrlComponent' => [:encode_url_component, '<s-:s>'],
1514
+ 'encodeUrl' => [:encode_url, '<s-:s>'],
1515
+ 'decodeUrlComponent' => [:decode_url_component, '<s-:s>'],
1516
+ 'decodeUrl' => [:decode_url, '<s-:s>'],
1517
+ 'eval' => [:function_eval, '<sx?:x>'],
1518
+ 'toMillis' => [:to_millis, '<s-s?:n>'],
1519
+ 'fromMillis' => [:from_millis, '<n-s?s?:s>'],
1520
+ 'clone' => [:function_clone, '<(oa)-:o>']
1521
+ }.each do |name, (method, signature)|
1522
+ STATIC_FRAME.bind(name, define_function(Focus.instance_method(method), signature))
1523
+ end
1524
+ end
1525
+
1526
+ # A compiled JSONata expression (jsonata-js: the object returned by `jsonata(expr, options)`)
1527
+ class Expression
1528
+ # @param expr - JSONata expression
1529
+ # @param options
1530
+ # @option options [Boolean] :recover attempt to recover on parse error
1531
+ # @option options [Proc] :regex_engine callable(source, flags) returning an object that responds to `match(str, pos)`
1532
+ # @option options [Integer] :timeout evaluation timeout (milliseconds)
1533
+ # @option options [Integer] :stack max stack depth
1534
+ # @option options [Integer] :sequence max sequence length
1535
+ def initialize(expr, options = nil)
1536
+ @options = options
1537
+ begin
1538
+ @ast = Parser.parse(expr, options && options[:recover])
1539
+ @errors = @ast.delete(:errors)
1540
+ rescue Jsonata::Error => e
1541
+ # insert error message into structure
1542
+ Evaluator.populate_message(e) # possible side-effects on `err`
1543
+ raise
1544
+ end
1545
+ @environment = Evaluator.create_frame(Evaluator::STATIC_FRAME)
1546
+
1547
+ @timestamp = current_time # will be overridden on each call to evalute()
1548
+ @environment.bind('now', Evaluator.define_function(lambda do |picture, timezone|
1549
+ Evaluator::FN.from_millis(millis(@timestamp), picture, timezone)
1550
+ end, '<s?s?:s>'))
1551
+ @environment.bind('millis', Evaluator.define_function(lambda do
1552
+ millis(@timestamp)
1553
+ end, '<:n>'))
1554
+ end
1555
+
1556
+ # Evaluates the expression against the input, returning plain Ruby values
1557
+ # (JSON null and undefined are both returned as nil)
1558
+ # @param input - Input data to evaluate against
1559
+ # @param bindings - Hash of variable bindings
1560
+ def evaluate(input = nil, bindings = nil, &)
1561
+ Utils.to_ruby(evaluate_raw(input.nil? ? nil : Utils.from_ruby(input), bindings, &))
1562
+ end
1563
+
1564
+ # Evaluates the expression, returning the internal representation of the result, in which JSON null is
1565
+ # represented by Jsonata::NULL and undefined by nil.
1566
+ # Nested nils in the input are treated as JSON null, a top-level nil input is treated as undefined.
1567
+ def evaluate_raw(input = nil, bindings = nil, &callback)
1568
+ # throw if the expression compiled with syntax errors
1569
+ unless @errors.nil?
1570
+ err = Jsonata::Error.new('S0500', :position => 0)
1571
+ Evaluator.populate_message(err) # possible side-effects on `err`
1572
+ raise err
1573
+ end
1574
+
1575
+ exec_env = Evaluator.create_frame(@environment)
1576
+
1577
+ # add any variable bindings may have been passed in
1578
+ bindings&.each do |name, value|
1579
+ exec_env.bind(name.kind_of?(Symbol) ? name.to_s : name, value.nil? ? nil : Utils.from_ruby(value))
1580
+ end
1581
+
1582
+ # put the input document into the environment as the root object
1583
+ exec_env.bind('$', input)
1584
+
1585
+ # capture the timestamp and put it in the execution environment
1586
+ # the $now() and $millis() functions will return this value - whenever it is called
1587
+ @timestamp = current_time
1588
+ exec_env.timestamp = @timestamp
1589
+ exec_env.options = @options
1590
+
1591
+ exec_env.base = exec_env
1592
+
1593
+ # if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input
1594
+ if input.kind_of?(Array) && !Utils.sequence?(input)
1595
+ input = exec_env.create_sequence(input)
1596
+ input.outer_wrapper = true
1597
+ end
1598
+
1599
+ exec_env.guardrails = if @options && (@options[:timeout] || @options[:stack])
1600
+ time = current_millis
1601
+ options = @options
1602
+ lambda do
1603
+ if options[:stack].to_i.positive? && exec_env.depth > options[:stack]
1604
+ # stack too deep
1605
+ raise Jsonata::Error.new('D1011', :value => options[:stack])
1606
+ end
1607
+ if options[:timeout].to_i.positive? && current_millis - time > options[:timeout]
1608
+ # expression has run for too long
1609
+ raise Jsonata::Error.new('D1012', :value => options[:timeout])
1610
+ end
1611
+ end
1612
+ else
1613
+ -> {}
1614
+ end
1615
+ exec_env.depth = 0
1616
+
1617
+ exec_env.regex_engine = @options && @options[:regex_engine]
1618
+
1619
+ begin
1620
+ it = Evaluator.evaluate(@ast, input, exec_env)
1621
+ callback&.call(nil, it)
1622
+ it
1623
+ rescue Jsonata::Error => e
1624
+ # insert error message into structure
1625
+ Evaluator.populate_message(e) # possible side-effects on `err`
1626
+ raise
1627
+ rescue SystemStackError
1628
+ # (Ruby specific) report exhaustion of the Ruby stack as a JSONata stack overflow error
1629
+ err = Jsonata::Error.new('D1011')
1630
+ Evaluator.populate_message(err)
1631
+ raise err
1632
+ end
1633
+ end
1634
+
1635
+ def assign(name, value)
1636
+ @environment.bind(name.kind_of?(Symbol) && !name.to_s.start_with?('jsonata.') ? name.to_s : name,
1637
+ value.nil? ? nil : Utils.from_ruby(value))
1638
+ end
1639
+
1640
+ # Registers a Ruby function, which may be given as a callable or a block
1641
+ # @example
1642
+ # expr.register_function('add', ->(a, b) { a + b }, '<nn:n>')
1643
+ # expr.register_function('greet', '<s:s>') { |name| "Hello, #{name}" }
1644
+ def register_function(name, implementation = nil, signature = nil, &block)
1645
+ if block && (implementation.nil? || implementation.kind_of?(String))
1646
+ signature = implementation
1647
+ implementation = block
1648
+ end
1649
+ func = Evaluator.define_function(implementation, signature)
1650
+ @environment.bind(name.to_s, func)
1651
+ end
1652
+
1653
+ attr_reader :ast, :errors
1654
+
1655
+ private
1656
+
1657
+ def current_time
1658
+ Time.at(Rational(current_millis, 1000), :in => 'UTC')
1659
+ end
1660
+
1661
+ def current_millis
1662
+ Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
1663
+ end
1664
+
1665
+ def millis(time)
1666
+ (time.to_r * 1000).floor
1667
+ end
1668
+ end
1669
+ end