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.
- checksums.yaml +7 -0
- data/README.md +183 -0
- data/Rakefile +11 -0
- data/lib/spot_feel/configuration.rb +12 -0
- data/lib/spot_feel/dmn/decision.rb +50 -0
- data/lib/spot_feel/dmn/decision_table.rb +53 -0
- data/lib/spot_feel/dmn/definitions.rb +68 -0
- data/lib/spot_feel/dmn/information_requirement.rb +29 -0
- data/lib/spot_feel/dmn/input.rb +28 -0
- data/lib/spot_feel/dmn/literal_expression.rb +370 -0
- data/lib/spot_feel/dmn/output.rb +29 -0
- data/lib/spot_feel/dmn/rule.rb +63 -0
- data/lib/spot_feel/dmn/unary_tests.rb +27 -0
- data/lib/spot_feel/dmn/variable.rb +27 -0
- data/lib/spot_feel/dmn.rb +17 -0
- data/lib/spot_feel/nodes.rb +684 -0
- data/lib/spot_feel/parser.rb +29 -0
- data/lib/spot_feel/spot_feel.treetop +659 -0
- data/lib/spot_feel/version.rb +5 -0
- data/lib/spot_feel.rb +63 -0
- metadata +317 -0
@@ -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
|