dentaku 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -3
  3. data/CHANGELOG.md +28 -0
  4. data/README.md +5 -4
  5. data/dentaku.gemspec +2 -0
  6. data/lib/dentaku/ast/access.rb +6 -0
  7. data/lib/dentaku/ast/arithmetic.rb +5 -1
  8. data/lib/dentaku/ast/array.rb +4 -0
  9. data/lib/dentaku/ast/bitwise.rb +8 -0
  10. data/lib/dentaku/ast/case/case_conditional.rb +4 -0
  11. data/lib/dentaku/ast/case/case_else.rb +6 -0
  12. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  13. data/lib/dentaku/ast/case/case_then.rb +6 -0
  14. data/lib/dentaku/ast/case/case_when.rb +10 -0
  15. data/lib/dentaku/ast/case.rb +6 -0
  16. data/lib/dentaku/ast/comparators.rb +25 -35
  17. data/lib/dentaku/ast/function.rb +6 -8
  18. data/lib/dentaku/ast/functions/all.rb +6 -19
  19. data/lib/dentaku/ast/functions/any.rb +6 -19
  20. data/lib/dentaku/ast/functions/duration.rb +2 -2
  21. data/lib/dentaku/ast/functions/enum.rb +37 -0
  22. data/lib/dentaku/ast/functions/filter.rb +23 -0
  23. data/lib/dentaku/ast/functions/if.rb +4 -0
  24. data/lib/dentaku/ast/functions/map.rb +5 -18
  25. data/lib/dentaku/ast/functions/mul.rb +2 -3
  26. data/lib/dentaku/ast/functions/pluck.rb +8 -7
  27. data/lib/dentaku/ast/functions/ruby_math.rb +6 -3
  28. data/lib/dentaku/ast/functions/sum.rb +2 -3
  29. data/lib/dentaku/ast/functions/xor.rb +44 -0
  30. data/lib/dentaku/ast/identifier.rb +11 -3
  31. data/lib/dentaku/ast/literal.rb +10 -0
  32. data/lib/dentaku/ast/negation.rb +4 -0
  33. data/lib/dentaku/ast/nil.rb +4 -0
  34. data/lib/dentaku/ast/node.rb +4 -0
  35. data/lib/dentaku/ast/operation.rb +9 -0
  36. data/lib/dentaku/ast/string.rb +7 -0
  37. data/lib/dentaku/ast.rb +2 -0
  38. data/lib/dentaku/bulk_expression_solver.rb +5 -3
  39. data/lib/dentaku/calculator.rb +1 -1
  40. data/lib/dentaku/parser.rb +5 -3
  41. data/lib/dentaku/print_visitor.rb +101 -0
  42. data/lib/dentaku/token_scanner.rb +1 -1
  43. data/lib/dentaku/version.rb +1 -1
  44. data/lib/dentaku/visitor/infix.rb +82 -0
  45. data/lib/dentaku.rb +4 -3
  46. data/spec/ast/all_spec.rb +25 -0
  47. data/spec/ast/any_spec.rb +23 -0
  48. data/spec/ast/comparator_spec.rb +6 -9
  49. data/spec/ast/filter_spec.rb +25 -0
  50. data/spec/ast/function_spec.rb +5 -0
  51. data/spec/ast/map_spec.rb +27 -0
  52. data/spec/ast/max_spec.rb +13 -0
  53. data/spec/ast/min_spec.rb +13 -0
  54. data/spec/ast/mul_spec.rb +3 -2
  55. data/spec/ast/pluck_spec.rb +32 -0
  56. data/spec/ast/sum_spec.rb +3 -2
  57. data/spec/ast/xor_spec.rb +35 -0
  58. data/spec/bulk_expression_solver_spec.rb +10 -0
  59. data/spec/calculator_spec.rb +66 -2
  60. data/spec/parser_spec.rb +18 -3
  61. data/spec/print_visitor_spec.rb +66 -0
  62. data/spec/tokenizer_spec.rb +6 -0
  63. data/spec/visitor/infix_spec.rb +31 -0
  64. data/spec/visitor_spec.rb +137 -0
  65. metadata +43 -6
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/map'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::Map do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+ it 'operates on each value in an array' do
8
+ result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
9
+ expect(result).to eq(9)
10
+ end
11
+
12
+ it 'works with an empty array' do
13
+ result = Dentaku('MAP(vals, val, val + 1)', vals: [])
14
+ expect(result).to eq([])
15
+ end
16
+
17
+ it 'works with a single value if needed for some reason' do
18
+ result = Dentaku('MAP(vals, val, val + 1)', vals: 1)
19
+ expect(result).to eq([2])
20
+ end
21
+
22
+ it 'raises argument error if a string is passed as identifier' do
23
+ expect { calculator.evaluate!('MAP({1, 2, 3}, "val", val + 1)') }.to raise_error(
24
+ Dentaku::ArgumentError, 'MAP() requires second argument to be an identifier'
25
+ )
26
+ end
27
+ end
data/spec/ast/max_spec.rb CHANGED
@@ -17,4 +17,17 @@ describe 'Dentaku::AST::Function::Max' do
17
17
  result = Dentaku('MAX(1, x, 1.8)', x: [1.5, 2.3, 1.7])
18
18
  expect(result).to eq(2.3)
19
19
  end
20
+
21
+ it 'returns the largest value if only an Array is passed' do
22
+ result = Dentaku('MAX(x)', x: [1.5, 2.3, 1.7])
23
+ expect(result).to eq(2.3)
24
+ end
25
+
26
+ context 'checking errors' do
27
+ let(:calculator) { Dentaku::Calculator.new }
28
+
29
+ it 'does not raise an error if an empty array is passed' do
30
+ expect(calculator.evaluate!('MAX(x)', x: [])).to eq(nil)
31
+ end
32
+ end
20
33
  end
data/spec/ast/min_spec.rb CHANGED
@@ -17,4 +17,17 @@ describe 'Dentaku::AST::Function::Min' do
17
17
  result = Dentaku('MIN(1, x, 1.8)', x: [1.5, 0.3, 1.7])
18
18
  expect(result).to eq(0.3)
19
19
  end
20
+
21
+ it 'returns the smallest value if only an Array is passed' do
22
+ result = Dentaku('MIN(x)', x: [1.5, 2.3, 1.7])
23
+ expect(result).to eq(1.5)
24
+ end
25
+
26
+ context 'checking errors' do
27
+ let(:calculator) { Dentaku::Calculator.new }
28
+
29
+ it 'does not raise an error if an empty array is passed' do
30
+ expect(calculator.evaluate!('MIN(x)', x: [])).to eq(nil)
31
+ end
32
+ end
20
33
  end
data/spec/ast/mul_spec.rb CHANGED
@@ -35,8 +35,9 @@ describe 'Dentaku::AST::Function::Mul' do
35
35
  expect { calculator.evaluate!('MUL()') }.to raise_error(Dentaku::ArgumentError)
36
36
  end
37
37
 
38
- it 'raises an error if an empty array is passed' do
39
- expect { calculator.evaluate!('MUL(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
38
+ it 'does not raise an error if an empty array is passed' do
39
+ result = calculator.evaluate!('MUL(x)', x: [])
40
+ expect(result).to eq(1)
40
41
  end
41
42
  end
42
43
  end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/pluck'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::Pluck do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+ it 'operates on each value in an array' do
8
+ result = Dentaku('PLUCK(users, age)', users: [
9
+ {name: "Bob", age: 44},
10
+ {name: "Jane", age: 27}
11
+ ])
12
+ expect(result).to eq([44, 27])
13
+ end
14
+
15
+ it 'works with an empty array' do
16
+ result = Dentaku('PLUCK(users, age)', users: [])
17
+ expect(result).to eq([])
18
+ end
19
+
20
+ it 'raises argument error if a string is passed as identifier' do
21
+ expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
22
+ {name: "Bob", age: 44},
23
+ {name: "Jane", age: 27}
24
+ ]) end.to raise_error(Dentaku::ArgumentError, 'PLUCK() requires second argument to be an identifier')
25
+ end
26
+
27
+ it 'raises argument error if a non array of hashes is passed as collection' do
28
+ expect { calculator.evaluate!('PLUCK({1, 2, 3}, age)') }.to raise_error(
29
+ Dentaku::ArgumentError, 'PLUCK() requires first argument to be an array of hashes'
30
+ )
31
+ end
32
+ end
data/spec/ast/sum_spec.rb CHANGED
@@ -35,8 +35,9 @@ describe 'Dentaku::AST::Function::Sum' do
35
35
  expect { calculator.evaluate!('SUM()') }.to raise_error(Dentaku::ArgumentError)
36
36
  end
37
37
 
38
- it 'raises an error if an empty array is passed' do
39
- expect { calculator.evaluate!('SUM(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
38
+ it 'does not raise an error if an empty array is passed' do
39
+ result = calculator.evaluate!('SUM(x)', x: [])
40
+ expect(result).to eq(0)
40
41
  end
41
42
  end
42
43
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/ast/functions/or'
4
+
5
+ describe 'Dentaku::AST::Xor' do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+
8
+ it 'returns false if all of the arguments are false' do
9
+ result = Dentaku('XOR(false, false)')
10
+ expect(result).to eq(false)
11
+ end
12
+
13
+ it 'returns true if only one of the arguments is true' do
14
+ result = Dentaku('XOR(false, true)')
15
+ expect(result).to eq(true)
16
+ end
17
+
18
+ it 'returns false if more than one of the arguments is true' do
19
+ result = Dentaku('XOR(false, true, true)')
20
+ expect(result).to eq(false)
21
+ end
22
+
23
+ it 'supports nested expressions' do
24
+ result = Dentaku('XOR(y = 1, x = 1)', x: 1, y: 2)
25
+ expect(result).to eq(true)
26
+ end
27
+
28
+ it 'raises an error if no arguments are passed' do
29
+ expect { calculator.evaluate!('XOR()') }.to raise_error(Dentaku::ParseError)
30
+ end
31
+
32
+ it 'raises an error if a non logical argument is passed' do
33
+ expect { calculator.evaluate!('XOR("r")') }.to raise_error(Dentaku::ArgumentError)
34
+ end
35
+ end
@@ -56,6 +56,16 @@ RSpec.describe Dentaku::BulkExpressionSolver do
56
56
  expect(solver.solve!).to eq(x: 6, y: 12) # x = 6 by the time y is calculated
57
57
  end
58
58
 
59
+ it "does not execute functions unnecessarily" do
60
+ calls = 0
61
+ external = ->() { calls += 1 }
62
+ hash = {test: 'EXTERNAL()'}
63
+ calculator = Dentaku::Calculator.new
64
+ calculator.add_function(:external, :numeric, external)
65
+ calculator.solve(hash)
66
+ expect(calls).to eq(1)
67
+ end
68
+
59
69
  it "evaluates expressions in hashes and arrays, and expands the results" do
60
70
  calculator.store(
61
71
  fruit_quantities: {
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
  require 'dentaku'
3
3
  describe Dentaku::Calculator do
4
4
  let(:calculator) { described_class.new }
5
+ let(:with_case_sensitivity) { described_class.new(case_sensitive: true) }
5
6
  let(:with_memory) { described_class.new.store(apples: 3) }
6
7
  let(:with_aliases) { described_class.new(aliases: { round: ['rrround'] }) }
7
8
  let(:without_nested_data) { described_class.new(nested_data_support: false) }
@@ -186,6 +187,11 @@ describe Dentaku::Calculator do
186
187
  it "finds no dependencies in array literals" do
187
188
  expect(calculator.dependencies([1, 2, 3])).to eq([])
188
189
  end
190
+
191
+ it "finds dependencies in item expressions" do
192
+ expect(calculator.dependencies('MAP(vals, val, val + step)')).to eq(['vals', 'step'])
193
+ expect(calculator.dependencies('ALL(people, person, person.age < adult)')).to eq(['people', 'adult'])
194
+ end
189
195
  end
190
196
 
191
197
  describe 'solve!' do
@@ -257,8 +263,13 @@ describe Dentaku::Calculator do
257
263
 
258
264
  describe 'solve' do
259
265
  it "returns :undefined when variables are unbound" do
260
- expressions = {more_apples: "apples + 1"}
261
- expect(calculator.solve(expressions)).to eq(more_apples: :undefined)
266
+ expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
267
+ expect(calculator.solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
268
+ end
269
+
270
+ it "returns :undefined when variables are nil" do
271
+ expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
272
+ expect(calculator.store(apples: nil).solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
262
273
  end
263
274
 
264
275
  it "allows passing in a custom value to an error handler" do
@@ -304,6 +315,20 @@ describe Dentaku::Calculator do
304
315
  )
305
316
  }.not_to raise_error
306
317
  end
318
+
319
+ it "integrates with custom functions" do
320
+ calculator.add_function(:custom, :integer, -> { 1 })
321
+
322
+ result = calculator.solve(
323
+ a: "1",
324
+ b: "CUSTOM() - a"
325
+ )
326
+
327
+ expect(result).to eq(
328
+ a: 1,
329
+ b: 0
330
+ )
331
+ end
307
332
  end
308
333
 
309
334
  it 'evaluates a statement with no variables' do
@@ -502,6 +527,10 @@ describe Dentaku::Calculator do
502
527
  {"name" => "Bob", "age" => 44},
503
528
  {"name" => "Jane", "age" => 27}
504
529
  ])).to eq(["Bob", "Jane"])
530
+ expect(calculator.evaluate!('map(users, u, IF(u.age < 30, u, null))', users: [
531
+ {"name" => "Bob", "age" => 44},
532
+ {"name" => "Jane", "age" => 27}
533
+ ])).to eq([nil, { "name" => "Jane", "age" => 27 }])
505
534
  end
506
535
  end
507
536
 
@@ -662,6 +691,31 @@ describe Dentaku::Calculator do
662
691
  expect(value).to eq(5)
663
692
  end
664
693
 
694
+ it 'handles nested case statements with case-sensitivity' do
695
+ formula = <<-FORMULA
696
+ CASE fruit
697
+ WHEN 'apple'
698
+ THEN 1 * quantity
699
+ WHEN 'banana'
700
+ THEN
701
+ CASE QUANTITY
702
+ WHEN 1 THEN 2
703
+ WHEN 10 THEN
704
+ CASE type
705
+ WHEN 'organic' THEN 5
706
+ END
707
+ END
708
+ END
709
+ FORMULA
710
+ value = with_case_sensitivity.evaluate(
711
+ formula,
712
+ type: 'organic',
713
+ quantity: 1,
714
+ QUANTITY: 10,
715
+ fruit: 'banana')
716
+ expect(value).to eq(5)
717
+ end
718
+
665
719
  it 'handles multiple nested case statements' do
666
720
  formula = <<-FORMULA
667
721
  CASE fruit
@@ -696,12 +750,22 @@ describe Dentaku::Calculator do
696
750
  it method do
697
751
  if Math.method(method).arity == 2
698
752
  expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq(Math.send(method, 1, 2))
753
+ expect(calculator.evaluate("#{method}(x,y) + 1", x: 1, y: '2')).to be_within(0.00001).of(Math.send(method, 1, 2) + 1)
699
754
  expect { calculator.evaluate!("#{method}(x)", x: 1) }.to raise_error(Dentaku::ParseError)
700
755
  else
701
756
  expect(calculator.evaluate("#{method}(1)")).to eq(Math.send(method, 1))
757
+ unless [:atanh, :frexp, :lgamma].include?(method)
758
+ expect(calculator.evaluate("#{method}(1) + 1")).to be_within(0.00001).of(Math.send(method, 1) + 1)
759
+ end
702
760
  end
703
761
  end
704
762
  end
763
+
764
+ it 'are defined with a properly named class that represents it to support AST marshaling' do
765
+ expect {
766
+ Marshal.dump(calculator.ast('SQRT(20)'))
767
+ }.not_to raise_error
768
+ end
705
769
  end
706
770
 
707
771
  describe 'disable_cache' do
data/spec/parser_spec.rb CHANGED
@@ -71,6 +71,11 @@ describe Dentaku::Parser do
71
71
  expect(node.value("x" => 3)).to eq(4)
72
72
  end
73
73
 
74
+ it 'evaluates a nested case statement with case-sensitivity' do
75
+ node = parse('CASE x WHEN 1 THEN CASE Y WHEN "A" THEN 2 WHEN "B" THEN 3 END END', { case_sensitive: true }, { case_sensitive: true })
76
+ expect(node.value("x" => 1, "y" => "A", "Y" => "B")).to eq(3)
77
+ end
78
+
74
79
  it 'evaluates arrays' do
75
80
  node = parse('{1, 2, 3}')
76
81
  expect(node.value).to eq([1, 2, 3])
@@ -89,6 +94,16 @@ describe Dentaku::Parser do
89
94
  }.to raise_error(Dentaku::ParseError)
90
95
  end
91
96
 
97
+ it 'raises a parse error for too many operands' do
98
+ expect {
99
+ parse("IF(1, 0, IF(1, 2, 3, 4))")
100
+ }.to raise_error(Dentaku::ParseError)
101
+
102
+ expect {
103
+ parse("CASE a WHEN 1 THEN true ELSE THEN IF(1, 2, 3, 4) END")
104
+ }.to raise_error(Dentaku::ParseError)
105
+ end
106
+
92
107
  it 'raises a parse error for bad grouping structure' do
93
108
  expect {
94
109
  parse(",")
@@ -144,8 +159,8 @@ describe Dentaku::Parser do
144
159
 
145
160
  private
146
161
 
147
- def parse(expr)
148
- tokens = Dentaku::Tokenizer.new.tokenize(expr)
149
- described_class.new(tokens).parse
162
+ def parse(expr, parser_options = {}, tokenizer_options = {})
163
+ tokens = Dentaku::Tokenizer.new.tokenize(expr, tokenizer_options)
164
+ described_class.new(tokens, parser_options).parse
150
165
  end
151
166
  end
@@ -0,0 +1,66 @@
1
+ require 'dentaku/print_visitor'
2
+ require 'dentaku/tokenizer'
3
+ require 'dentaku/parser'
4
+
5
+ describe Dentaku::PrintVisitor do
6
+ it 'prints a representation of an AST' do
7
+ repr = roundtrip('5+4')
8
+ expect(repr).to eq('5 + 4')
9
+ end
10
+
11
+ it 'quotes string literals' do
12
+ repr = roundtrip('Concat(\'a\', "B")')
13
+ expect(repr).to eq('CONCAT("a", "B")')
14
+ end
15
+
16
+ it 'handles unary operations on literals' do
17
+ repr = roundtrip('- 4')
18
+ expect(repr).to eq('-4')
19
+ end
20
+
21
+ it 'handles unary operations on trees' do
22
+ repr = roundtrip('- (5 + 5)')
23
+ expect(repr).to eq('-(5 + 5)')
24
+ end
25
+
26
+ it 'handles a complex arithmetic expression' do
27
+ repr = roundtrip('(((1 + 7) * (8 ^ 2)) / - (3.0 - apples))')
28
+ expect(repr).to eq('(1 + 7) * 8 ^ 2 / -(3.0 - apples)')
29
+ end
30
+
31
+ it 'handles a complex logical expression' do
32
+ repr = roundtrip('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
33
+ expect(repr).to eq('1 < 2 and 3 <= 4 or 5 > 6 and 7 >= 8 or 9 != 10 and true')
34
+ end
35
+
36
+ it 'handles a function call' do
37
+ repr = roundtrip('IF(a[0] = NULL, "five", \'seven\')')
38
+ expect(repr).to eq('IF(a[0] = NULL, "five", "seven")')
39
+ end
40
+
41
+ it 'handles a case statement' do
42
+ repr = roundtrip('case (a % 5) when 0 then a else b end')
43
+ expect(repr).to eq('CASE a % 5 WHEN 0 THEN a ELSE b END')
44
+ end
45
+
46
+ it 'handles a bitwise operators' do
47
+ repr = roundtrip('0xCAFE & 0xDECAF | 0xBEEF')
48
+ expect(repr).to eq('0xCAFE & 0xDECAF | 0xBEEF')
49
+ end
50
+
51
+ it 'handles a datetime literal' do
52
+ repr = roundtrip('2017-12-24 23:59:59')
53
+ expect(repr).to eq('2017-12-24 23:59:59')
54
+ end
55
+
56
+ private
57
+
58
+ def roundtrip(string)
59
+ described_class.new(parsed(string)).to_s
60
+ end
61
+
62
+ def parsed(string)
63
+ tokens = Dentaku::Tokenizer.new.tokenize(string)
64
+ Dentaku::Parser.new(tokens).parse
65
+ end
66
+ end
@@ -25,6 +25,12 @@ describe Dentaku::Tokenizer do
25
25
  expect(tokens.map(&:category)).to eq([:numeric])
26
26
  expect(tokens.map(&:value)).to eq([6.02e23])
27
27
  end
28
+
29
+ tokens = tokenizer.tokenize('6E23')
30
+ expect(tokens.map(&:value)).to eq([0.6e24])
31
+
32
+ tokens = tokenizer.tokenize('6e-23')
33
+ expect(tokens.map(&:value)).to eq([0.6e-22])
28
34
  end
29
35
 
30
36
  it 'tokenizes addition' do
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ require 'dentaku/visitor/infix'
4
+
5
+ class ArrayProcessor
6
+ attr_reader :expression
7
+ include Dentaku::Visitor::Infix
8
+
9
+ def initialize
10
+ @expression = []
11
+ end
12
+
13
+ def process(node)
14
+ @expression << node.to_s
15
+ end
16
+ end
17
+
18
+ RSpec.describe Dentaku::Visitor::Infix do
19
+ it 'generates array representation of operation' do
20
+ processor = ArrayProcessor.new
21
+ processor.visit(ast('5 + 3'))
22
+ expect(processor.expression).to eq ['5', '+', '3']
23
+ end
24
+
25
+ private
26
+
27
+ def ast(expression)
28
+ tokens = Dentaku::Tokenizer.new.tokenize(expression)
29
+ Dentaku::Parser.new(tokens).parse
30
+ end
31
+ end
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+ require 'set'
3
+
4
+ class TestVisitor
5
+ attr_reader :visited
6
+
7
+ def initialize(node)
8
+ @visited = Set.new
9
+ node.accept(self)
10
+ end
11
+
12
+ def mark_visited(node)
13
+ @visited.add(node.class.to_s.split("::").last.to_sym)
14
+ end
15
+
16
+ def visit_operation(node)
17
+ mark_visited(node)
18
+
19
+ node.left.accept(self) if node.left
20
+ node.right.accept(self) if node.right
21
+ end
22
+
23
+ def visit_function(node)
24
+ mark_visited(node)
25
+ node.args.each { |a| a.accept(self) }
26
+ end
27
+
28
+ def visit_array(node)
29
+ mark_visited(node)
30
+ end
31
+
32
+ def visit_case(node)
33
+ mark_visited(node)
34
+ node.switch.accept(self)
35
+ node.conditions.each { |c| c.accept(self) }
36
+ node.else && node.else.accept(self)
37
+ end
38
+
39
+ def visit_switch(node)
40
+ mark_visited(node)
41
+ node.node.accept(self)
42
+ end
43
+
44
+ def visit_case_conditional(node)
45
+ mark_visited(node)
46
+ node.when.accept(self)
47
+ node.then.accept(self)
48
+ end
49
+
50
+ def visit_when(node)
51
+ mark_visited(node)
52
+ node.node.accept(self)
53
+ end
54
+
55
+ def visit_then(node)
56
+ mark_visited(node)
57
+ node.node.accept(self)
58
+ end
59
+
60
+ def visit_else(node)
61
+ mark_visited(node)
62
+ node.node.accept(self)
63
+ end
64
+
65
+ def visit_negation(node)
66
+ mark_visited(node)
67
+ node.node.accept(self)
68
+ end
69
+
70
+ def visit_access(node)
71
+ mark_visited(node)
72
+ node.structure.accept(self)
73
+ node.index.accept(self)
74
+ end
75
+
76
+ def visit_literal(node)
77
+ mark_visited(node)
78
+ end
79
+
80
+ def visit_identifier(node)
81
+ mark_visited(node)
82
+ end
83
+
84
+ def visit_nil(node)
85
+ mark_visited(node)
86
+ end
87
+ end
88
+
89
+ describe TestVisitor do
90
+ def generic_subclasses
91
+ [
92
+ :Arithmetic,
93
+ :Combinator,
94
+ :Comparator,
95
+ :Function,
96
+ :FunctionRegistry,
97
+ :Grouping,
98
+ :Literal,
99
+ :Node,
100
+ :Operation,
101
+ :StringFunctions,
102
+ :RubyMath,
103
+ :Enum,
104
+ ]
105
+ end
106
+
107
+ it 'visits all concrete AST node types' do
108
+ @visited = Set.new
109
+
110
+ visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples')
111
+ visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
112
+ visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
113
+ visit_nodes('case (a % 5) when 0 then a else b end')
114
+ visit_nodes('0xCAFE & 0xDECAF | 0xBEEF')
115
+ visit_nodes('2017-12-24 23:59:59')
116
+ visit_nodes('ALL({1, 2, 3}, "val", val % 2 == 0)')
117
+ visit_nodes('ANY(vals, val, val > 1)')
118
+ visit_nodes('COUNT({1, 2, 3})')
119
+ visit_nodes('PLUCK(users, age)')
120
+ visit_nodes('XOR(false, false)')
121
+ visit_nodes('duration(1, day)')
122
+ visit_nodes('MAP(vals, val, val + 1)')
123
+ visit_nodes('FILTER(vals, val, val > 1)')
124
+
125
+ @expected = Set.new(Dentaku::AST::constants - generic_subclasses)
126
+ expect(@visited.sort).to eq(@expected.sort)
127
+ end
128
+
129
+ private
130
+
131
+ def visit_nodes(string)
132
+ tokens = Dentaku::Tokenizer.new.tokenize(string)
133
+ node = Dentaku::Parser.new(tokens).parse
134
+ visitor = TestVisitor.new(node)
135
+ @visited += visitor.visited
136
+ end
137
+ end