spot_feel 0.0.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,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