dmn 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,684 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFeel
4
+ class Node < Treetop::Runtime::SyntaxNode
5
+ #
6
+ # Takes a context hash and returns an array of qualified names
7
+ # { "person": { "name": { "first": "Eric", "last": "Carlson" }, "age": 60 } } => ["person", "person.name.first", "person.name.last", "person.age"]
8
+ #
9
+ def qualified_names_in_context(hash = {}, prefix = '', qualified_names = Set.new)
10
+ hash.each do |key, value|
11
+ new_prefix = prefix.empty? ? "#{key}" : "#{prefix}.#{key}"
12
+ if value.is_a?(Hash)
13
+ qualified_names_in_context(value, new_prefix, qualified_names)
14
+ else
15
+ qualified_names.add(new_prefix)
16
+ end
17
+ end if hash
18
+
19
+ qualified_names.to_a
20
+ end
21
+
22
+ def raise_evaluation_error(missing_name, ctx = {})
23
+ names = qualified_names_in_context(ctx)
24
+ checker = DidYouMean::SpellChecker.new(dictionary: names)
25
+ guess = checker.correct(missing_name)
26
+ suffix = " Did you mean #{guess.first}?" unless guess.empty?
27
+ raise EvaluationError.new("Identifier #{missing_name} not found.#{suffix}")
28
+ end
29
+ end
30
+
31
+ #
32
+ # 1. expression =
33
+ # 1.a textual expression |
34
+ # 1.b boxed expression ;
35
+ #
36
+
37
+ #
38
+ # 2. textual expression =
39
+ # 2.a function definition | for expression | if expression | quantified expression |
40
+ # 2.b disjunction |
41
+ # 2.c conjunction |
42
+ # 2.d comparison |
43
+ # 2.e arithmetic expression |
44
+ # 2.f instance of |
45
+ # 2.g path expression |
46
+ # 2.h filter expression | function invocation |
47
+ # 2.i literal | simple positive unary test | name | "(" , textual expression , ")" ;
48
+ #
49
+
50
+ #
51
+ # 3. textual expressions = textual expression , { "," , textual expression } ;
52
+ #
53
+
54
+ #
55
+ # 4. arithmetic expression =
56
+ # 4.a addition | subtraction |
57
+ # 4.b multiplication | division |
58
+ # 4.c exponentiation |
59
+ # 4.d arithmetic negation ;
60
+ #
61
+
62
+ #
63
+ # 5. simple expression = arithmetic expression | simple value ;
64
+ #
65
+
66
+ #
67
+ # 6. simple expressions = simple expression , { "," , simple expression } ;
68
+ #
69
+ class SimpleExpressions < Node
70
+ def eval(context = {})
71
+ [expr.eval(context)] + more_exprs.elements.map { |element| element.eval(context) }
72
+ end
73
+ end
74
+
75
+ #
76
+ # 7. simple positive unary test =
77
+ # 7.a [ "<" | "<=" | ">" | ">=" ] , endpoint |
78
+ # 7.b interval ;
79
+ #
80
+ class SimplePositiveUnaryTest < Node
81
+ def eval(context = {})
82
+ operator = head.text_value.strip
83
+ endpoint = tail.eval(context)
84
+
85
+ case operator
86
+ when "<"
87
+ ->(input) { input < endpoint }
88
+ when "<="
89
+ ->(input) { input <= endpoint }
90
+ when ">"
91
+ ->(input) { input > endpoint }
92
+ when ">="
93
+ ->(input) { input >= endpoint }
94
+ else
95
+ ->(input) { input == endpoint }
96
+ end
97
+ end
98
+ end
99
+
100
+ class UnaryOperator < Treetop::Runtime::SyntaxNode
101
+ def eval(_context = {})
102
+ text_value.strip
103
+ end
104
+ end
105
+
106
+ #
107
+ # 8. interval = ( open interval start | closed interval start ) , endpoint , ".." , endpoint , ( open interval end | closed interval end ) ;
108
+ #
109
+ class Interval < Node
110
+ def eval(context = {})
111
+ start = start_token.text_value
112
+ finish = end_token.text_value
113
+ first_val = first.eval(context)
114
+ second_val = second.eval(context)
115
+
116
+ case [start, finish]
117
+ when ['(', ')']
118
+ ->(input) { first_val < input && input < second_val }
119
+ when ['[', ']']
120
+ ->(input) { first_val <= input && input <= second_val }
121
+ when ['(', ']']
122
+ ->(input) { first_val < input && input <= second_val }
123
+ when ['[', ')']
124
+ ->(input) { first_val <= input && input < second_val }
125
+ end
126
+ end
127
+ end
128
+
129
+ #
130
+ # 9. open interval start = "(" | "]" ;
131
+ #
132
+ class OpenIntervalStart < Node
133
+ def eval(_context = {})
134
+ text_value.strip
135
+ end
136
+ end
137
+
138
+ #
139
+ # 10. closed interval start = "[" ;
140
+ #
141
+ class ClosedIntervalStart < Node
142
+ def eval(_context = {})
143
+ text_value.strip
144
+ end
145
+ end
146
+
147
+ #
148
+ # 11. open interval end = ")" | "[" ;
149
+ #
150
+ class OpenIntervalEnd < Node
151
+ def eval(_context = {})
152
+ text_value.strip
153
+ end
154
+ end
155
+
156
+ #
157
+ # 12. closed interval end = "]" ;
158
+ #
159
+ class ClosedIntervalEnd < Node
160
+ def eval(_context = {})
161
+ text_value.strip
162
+ end
163
+ end
164
+
165
+ #
166
+ # 13. simple positive unary tests = simple positive unary test , { "," , simple positive unary test } ;
167
+ #
168
+ class SimplePositiveUnaryTests < Node
169
+ def eval(context = {})
170
+ tests = [test.eval(context)]
171
+ more_tests.elements.each do |more_test|
172
+ tests << more_test.simple_positive_unary_test.eval(context)
173
+ end
174
+ tests
175
+ end
176
+ end
177
+
178
+ #
179
+ # 14. simple unary tests =
180
+ # 14.a simple positive unary tests |
181
+ # 14.b "not", "(", simple positive unary tests, ")" |
182
+ # 14.c "-";
183
+ #
184
+ class SimpleUnaryTests < Node
185
+ def eval(context = {})
186
+ if defined?(expr) && expr.present?
187
+ tests = Array.wrap(expr.eval(context))
188
+ if defined?(negate) && negate.present?
189
+ ->(input) { !tests.any? { |test| test.call(input) } }
190
+ else
191
+ ->(input) { tests.any? { |test| test.call(input) } }
192
+ end
193
+ else
194
+ ->(_input) { true }
195
+ end
196
+ end
197
+ end
198
+
199
+ #
200
+ # 15. positive unary test = simple positive unary test | "null" ;
201
+ #
202
+
203
+ #
204
+ # 16. positive unary tests = positive unary test , { "," , positive unary test } ;
205
+ #
206
+
207
+ #
208
+ # 17. unary tests =
209
+ # 17.a positive unary tests |
210
+ # 17.b "not", " (", positive unary tests, ")" |
211
+ # 17.c "-"
212
+ #
213
+
214
+ #
215
+ # 18. endpoint = simple value ;
216
+ #
217
+
218
+ #
219
+ # 19. simple value = qualified name | simple literal ;
220
+ #
221
+
222
+ #
223
+ # 20. qualified name = name , { "." , name } ;
224
+ #
225
+ class QualifiedName < Node
226
+ def eval(context = {})
227
+ if tail.empty?
228
+ raise_evaluation_error(head.text_value, context) if SpotFeel.config.strict && !context.key?(head.text_value.to_sym)
229
+ context[head.text_value.to_sym]
230
+ else
231
+ tail.elements.flat_map { |element| element.name.text_value.split('.') }.inject(context[head.text_value.to_sym]) do |hash, key|
232
+ raise_evaluation_error("#{head.text_value}#{tail.text_value}", context) if SpotFeel.config.strict && (hash.blank? || !hash.key?(key.to_sym))
233
+ return nil unless hash
234
+ hash[key.to_sym]
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ #
241
+ # 21. addition = expression , "+" , expression ;
242
+ #
243
+ class Addition < Node
244
+ def eval(context = {})
245
+ head_val = head.eval(context)
246
+ tail_val = tail.eval(context)
247
+ return nil if head_val.nil? || tail_val.nil?
248
+ head.eval(context) + tail.eval(context)
249
+ end
250
+ end
251
+
252
+ #
253
+ # 22. subtraction = expression , "-" , expression ;
254
+ #
255
+ class Subtraction < Node
256
+ def eval(context = {})
257
+ head.eval(context) - tail.eval(context)
258
+ end
259
+ end
260
+
261
+ #
262
+ # 23. multiplication = expression , "\*" , expression ;
263
+ #
264
+ class Multiplication < Node
265
+ def eval(context = {})
266
+ head.eval(context) * tail.eval(context)
267
+ end
268
+ end
269
+
270
+ #
271
+ # 24. division = expression , "/" , expression ;
272
+ #
273
+ class Division < Node
274
+ def eval(context = {})
275
+ head.eval(context) / tail.eval(context)
276
+ end
277
+ end
278
+
279
+ #
280
+ # 25. exponentiation = expression, "\*\*", expression ;
281
+ #
282
+ class Exponentiation < Node
283
+ def eval(context = {})
284
+ head.eval(context) ** tail.eval(context)
285
+ end
286
+ end
287
+
288
+ #
289
+ # 26. arithmetic negation = "-" , expression ;
290
+ #
291
+
292
+ #
293
+ # 27. name = name start , { name part | additional name symbols } ;
294
+ #
295
+ class Name < Node
296
+ def eval(_context = {})
297
+ #head + tail.map{|t| t[1]}.join("")
298
+ text_value.strip
299
+ end
300
+ end
301
+
302
+ #
303
+ # 28. name start = name start char, { name part char } ;
304
+ #
305
+
306
+ #
307
+ # 29. name part = name part char , { name part char } ;
308
+ #
309
+
310
+ #
311
+ # 30. name start char = "?" | [A-Z] | "\_" | [a-z] | [\uC0-\uD6] | [\uD8-\uF6] | [\uF8-\u2FF] | [\u370-\u37D] | [\u37F-\u1FFF] | [\u200C-\u200D] | [\u2070-\u218F] | [\u2C00-\u2FEF] | [\u3001-\uD7FF] | [\uF900-\uFDCF] | [\uFDF0-\uFFFD] | [\u10000-\uEFFFF] ;
312
+ #
313
+
314
+ #
315
+ # 31. name part char = name start char | digit | \uB7 | [\u0300-\u036F] | [\u203F-\u2040] ;
316
+ #
317
+
318
+ #
319
+ # 32. additional name symbols = "." | "/" | "-" | "’" | "+" | "\*" ;
320
+ #
321
+
322
+ #
323
+ # 33. literal = simple literal | "null" ;
324
+ #
325
+
326
+ #
327
+ # 34. simple literal = numeric literal | string literal | boolean literal | date time literal ;
328
+ #
329
+
330
+ #
331
+ # 35. string literal = '"' , { character – ('"' | vertical space) }, '"' ;
332
+ #
333
+ class StringLiteral < Node
334
+ def eval(_context = {})
335
+ text_value[1..-2]
336
+ end
337
+ end
338
+
339
+ #
340
+ # 36. Boolean literal = "true" | "false" ;
341
+ #
342
+ class BooleanLiteral < Node
343
+ def eval(_context = {})
344
+ case text_value
345
+ when "true"
346
+ true
347
+ when "false"
348
+ false
349
+ end
350
+ end
351
+ end
352
+
353
+ #
354
+ # 37. numeric literal = [ "-" ] , ( digits , [ ".", digits ] | "." , digits ) ;
355
+ #
356
+ class NumericLiteral < Node
357
+ def eval(_context = {})
358
+ if text_value.include?(".")
359
+ text_value.to_f
360
+ else
361
+ text_value.to_i
362
+ end
363
+ end
364
+ end
365
+
366
+ class NullLiteral < Node
367
+ def eval(_context = {})
368
+ nil
369
+ end
370
+ end
371
+
372
+ #
373
+ # 38. digit = [0-9] ;
374
+ #
375
+
376
+ #
377
+ # 39. digits = digit , {digit} ;
378
+ #
379
+
380
+ #
381
+ # 40. function invocation = expression , parameters ;
382
+ #
383
+ class FunctionInvocation < Node
384
+ def eval(context = {})
385
+ fn = context[fn_name.text_value.to_sym]
386
+
387
+ unless fn
388
+ raise_evaluation_error(fn_name.text_value, context) if SpotFeel.config.strict
389
+ return nil
390
+ end
391
+
392
+ args = params.present? ? params.eval(context) : []
393
+
394
+ fn.call(*args)
395
+ end
396
+ end
397
+
398
+ #
399
+ # 41. parameters = "(" , ( named parameters | positional parameters ) , ")" ;
400
+ #
401
+
402
+ #
403
+ # 42. named parameters = parameter name , ":" , expression , { "," , parameter name , ":" , expression } ;
404
+ #
405
+
406
+ #
407
+ # 43. parameter name = name ;
408
+ #
409
+
410
+ #
411
+ # 44. positional parameters = [ expression , { "," , expression } ] ;
412
+ #
413
+ class PositionalParameters < Node
414
+ def eval(context = {})
415
+ expressions.inject([]) { |arr, exp| arr << exp.eval(context) }
416
+ end
417
+
418
+ def expressions
419
+ [expression] + more_expressions.elements.map { |e| e.expression }
420
+ end
421
+ end
422
+
423
+ #
424
+ # 45. path expression = expression , "." , name ;
425
+ #
426
+
427
+ #
428
+ # 46. for expression = "for" , name , "in" , expression { "," , name , "in" , expression } , "return" , expression ;
429
+ #
430
+
431
+ #
432
+ # 47. if expression = "if" , expression , "then" , expression , "else" expression ;
433
+ #
434
+ class IfExpression < Node
435
+ def eval(context = {})
436
+ if condition.eval(context)
437
+ true_case.eval(context)
438
+ else
439
+ false_case.eval(context)
440
+ end
441
+ end
442
+ end
443
+
444
+ #
445
+ # 48. quantified expression = ("some" | "every") , name , "in" , expression , { name , "in" , expression } , "satisfies" , expression ;
446
+ #
447
+ class QuantifiedExpression < Node
448
+ def eval(context = {})
449
+ if quantifier.text_value == "some"
450
+ quantified_some(context)
451
+ else
452
+ quantified_every(context)
453
+ end
454
+ end
455
+
456
+ def quantified_some(context)
457
+ quantified_expression = quantified_expression(context)
458
+ quantified_expression.any? { |input| satisfies(input, context) }
459
+ end
460
+
461
+ def quantified_every(context)
462
+ quantified_expression = quantified_expression(context)
463
+ quantified_expression.all? { |input| satisfies(input, context) }
464
+ end
465
+ end
466
+
467
+ #
468
+ # 49. disjunction = expression , "or" , expression ;
469
+ #
470
+ class Disjunction < Node
471
+ def eval(context = {})
472
+ head.eval(context) || tail.eval(context)
473
+ end
474
+ end
475
+
476
+ #
477
+ # 50. conjunction = expression , "and" , expression ;
478
+ #
479
+ class Conjunction < Node
480
+ def eval(context = {})
481
+ head.eval(context) && tail.eval(context)
482
+ end
483
+ end
484
+
485
+ #
486
+ # 51. comparison =
487
+ # 51.a expression , ( "=" | "!=" | "<" | "<=" | ">" | ">=" ) , expression |
488
+ # 51.b expression , "between" , expression , "and" , expression |
489
+ # 51.c expression , "in" , positive unary test ;
490
+ # 51.d expression , "in" , " (", positive unary tests, ")" ;
491
+ #
492
+ class Comparison < Node
493
+ def eval(context = {})
494
+ case operator.text_value
495
+ when '<' then left.eval(context) < right.eval(context)
496
+ when '<=' then left.eval(context) <= right.eval(context)
497
+ when '>=' then left.eval(context) >= right.eval(context)
498
+ when '>' then left.eval(context) > right.eval(context)
499
+ when '!=' then left.eval(context) != right.eval(context)
500
+ when '=' then left.eval(context) == right.eval(context)
501
+ end
502
+ end
503
+ end
504
+
505
+ class ComparisonOperator < Node
506
+ def eval(_context = {})
507
+ text_value.strip
508
+ end
509
+ end
510
+
511
+ #
512
+ # 52. filter expression = expression , "[" , expression , "]" ;
513
+ #
514
+ class FilterExpression < Node
515
+ def eval(context = {})
516
+ filter_expression = filter_expression(context)
517
+ filter_expression.select { |input| filter(input, context) }
518
+ end
519
+ end
520
+
521
+ #
522
+ # 53. instance of = expression , "instance" , "of" , type ;
523
+ #
524
+ class InstanceOf < Node
525
+ def eval(context = {})
526
+ case type.text_value
527
+ when "string"
528
+ ->(input) { input.is_a?(String) }
529
+ when "number"
530
+ ->(input) { input.is_a?(Numeric) }
531
+ when "boolean"
532
+ ->(input) { input.is_a?(TrueClass) || input.is_a?(FalseClass) }
533
+ when "date"
534
+ ->(input) { input.is_a?(Date) }
535
+ when "time"
536
+ ->(input) { input.is_a?(Time) }
537
+ when "date and time"
538
+ ->(input) { input.is_a?(DateTime) }
539
+ when "duration"
540
+ ->(input) { input.is_a?(ActiveSupport::Duration) }
541
+ when "years and months duration"
542
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:months, :years] }
543
+ when "days and time duration"
544
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:days, :hours, :minutes, :seconds] }
545
+ when "years duration"
546
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:years] }
547
+ when "months duration"
548
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:months] }
549
+ when "days duration"
550
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:days] }
551
+ when "hours duration"
552
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:hours] }
553
+ when "minutes duration"
554
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:minutes] }
555
+ when "seconds duration"
556
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:seconds] }
557
+ when "time duration"
558
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:hours, :minutes, :seconds] }
559
+ when "years and months duration"
560
+ ->(input) { input.is_a?(ActiveSupport::Duration) && input.parts.keys.sort == [:months, :years] }
561
+ when "list"
562
+ ->(input) { input.is_a?(Array) }
563
+ when "interval"
564
+ ->(input) { input.is_a?(Range) }
565
+ when "context"
566
+ ->(input) { input.is_a?(Hash) }
567
+ when "any"
568
+ ->(_input) { true }
569
+ end
570
+ end
571
+ end
572
+
573
+ #
574
+ # 54. type = qualified name ;
575
+ #
576
+
577
+ #
578
+ # 55. boxed expression = list | function definition | context ;
579
+ #
580
+
581
+ #
582
+ # 56. list = "[" [ expression , { "," , expression } ] , "]" ;
583
+ #
584
+ class List < Node
585
+ def eval(context = {})
586
+ return [] unless defined?(list_entries)
587
+ if list_entries.present?
588
+ list_entries.eval(context)
589
+ else
590
+ []
591
+ end
592
+ end
593
+ end
594
+
595
+ class ListEntries < Node
596
+ def eval(context = {})
597
+ expressions.inject([]) { |arr, exp| arr << exp.eval(context) }
598
+ end
599
+
600
+ def expressions
601
+ [expression] + more_expressions.elements.map { |e| e.expression }
602
+ end
603
+ end
604
+
605
+ #
606
+ # 57. function definition = "function" , "(" , [ formal parameter { "," , formal parameter } ] , ")" , [ "external" ] , expression ;
607
+ #
608
+ class FunctionDefinition < Node
609
+ end
610
+
611
+ #
612
+ # 58. formal parameter = parameter name ;
613
+ #
614
+ class FormalParameter < Node
615
+ end
616
+
617
+ #
618
+ # 59. context = "{" , [context entry , { "," , context entry } ] , "}" ;
619
+ #
620
+ class Context < Node
621
+ def eval(context = {})
622
+ if entries&.present?
623
+ entries.eval(context)
624
+ else
625
+ {}
626
+ end
627
+ end
628
+ end
629
+
630
+ class ContextEntryList < Node
631
+ def eval(context = {})
632
+ context_entries.inject({}) do |hash, entry|
633
+ hash.merge(entry.eval(context))
634
+ end
635
+ end
636
+
637
+ def context_entries
638
+ [context_entry] + tail.elements.map { |e| e.context_entry }
639
+ end
640
+ end
641
+
642
+ #
643
+ # 60. context entry = key , ":" , expression ;
644
+ #
645
+ class ContextEntry < Node
646
+ def eval(context = {})
647
+ { context_key.eval(context) => context_value.eval(context) }
648
+ end
649
+ end
650
+
651
+ #
652
+ # 61. key = name | string literal ;
653
+ #
654
+
655
+ #
656
+ # 62. date time literal = ( "date" | "time" | "date and time" | "duration" ) , "(" , string literal , ")" ;
657
+ #
658
+ class DateTimeLiteral < Node
659
+ def eval(context = {})
660
+ head_val = head.eval(context)
661
+ return nil if head_val.nil?
662
+ return head_val if head_val.is_a?(ActiveSupport::Duration) || head_val.is_a?(DateTime) || head_val.is_a?(Date) || head_val.is_a?(Time)
663
+
664
+ case keyword.text_value
665
+ when "date and time"
666
+ DateTime.parse(head_val)
667
+ when "date"
668
+ Date.parse(head_val)
669
+ when "time"
670
+ Time.parse(head_val)
671
+ when "duration"
672
+ if defined?(tail) && tail.present?
673
+ (Date.parse(tail.text_value) - Date.parse(head.text_value)).to_i.days
674
+ else
675
+ ActiveSupport::Duration.parse(head_val)
676
+ end
677
+ end
678
+ end
679
+
680
+ def duration_range(start_date, end_date)
681
+ (Date.parse(end_date) - Date.parse(start_date)).to_i
682
+ end
683
+ end
684
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpotFeel
4
+ class Parser
5
+ # Load the Treetop grammar from the 'spot_feel' file, and create a new
6
+ # instance of that parser as a class variable so we don't have to re-create
7
+ # it every time we need to parse a string
8
+ Treetop.load(File.expand_path(File.join(File.dirname(__FILE__), 'spot_feel.treetop')))
9
+ @@parser = SpotFeelParser.new
10
+
11
+ def self.parse(expression, root: nil)
12
+ @@parser.parse(expression, root:).tap do |ast|
13
+ raise SyntaxError, "Invalid expression: #{expression.inspect}" unless ast
14
+ end
15
+ end
16
+
17
+ def self.parse_test(expression)
18
+ @@parser.parse(expression || '-', root: :simple_unary_tests).tap do |ast|
19
+ raise SyntaxError, "Invalid unary test: #{expression.inspect}" unless ast
20
+ end
21
+ end
22
+
23
+ def self.clean_tree(root_node)
24
+ return if(root_node.elements.nil?)
25
+ root_node.elements.delete_if{|node| node.class.name == "Treetop::Runtime::SyntaxNode" }
26
+ root_node.elements.each {|node| self.clean_tree(node) }
27
+ end
28
+ end
29
+ end