dmn 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +374 -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 +654 -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
|