ghostwheel 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.
Files changed (50) hide show
  1. data/Rakefile +46 -0
  2. data/lib/ghost_wheel.rb +28 -0
  3. data/lib/ghost_wheel/build_parser.rb +11 -0
  4. data/lib/ghost_wheel/errors.rb +9 -0
  5. data/lib/ghost_wheel/expression.rb +22 -0
  6. data/lib/ghost_wheel/expression/alternation.rb +25 -0
  7. data/lib/ghost_wheel/expression/empty.rb +15 -0
  8. data/lib/ghost_wheel/expression/end_of_file.rb +19 -0
  9. data/lib/ghost_wheel/expression/literal.rb +31 -0
  10. data/lib/ghost_wheel/expression/look_ahead.rb +33 -0
  11. data/lib/ghost_wheel/expression/optional.rb +26 -0
  12. data/lib/ghost_wheel/expression/query.rb +32 -0
  13. data/lib/ghost_wheel/expression/repetition.rb +44 -0
  14. data/lib/ghost_wheel/expression/rule.rb +24 -0
  15. data/lib/ghost_wheel/expression/sequence.rb +30 -0
  16. data/lib/ghost_wheel/expression/transform.rb +30 -0
  17. data/lib/ghost_wheel/parse_results.rb +9 -0
  18. data/lib/ghost_wheel/parser.rb +71 -0
  19. data/lib/ghost_wheel/parser_builder/ghost_wheel.rb +100 -0
  20. data/lib/ghost_wheel/parser_builder/ruby.rb +175 -0
  21. data/lib/ghost_wheel/scanner.rb +42 -0
  22. data/setup.rb +1360 -0
  23. data/test/dsl/tc_build_parser.rb +29 -0
  24. data/test/dsl/tc_ghost_wheel_dsl.rb +143 -0
  25. data/test/dsl/tc_ruby_dsl.rb +227 -0
  26. data/test/example/tc_json_core.rb +95 -0
  27. data/test/example/tc_json_ghost_wheel.rb +48 -0
  28. data/test/example/tc_json_ruby.rb +81 -0
  29. data/test/helpers/ghost_wheel_namespace.rb +24 -0
  30. data/test/helpers/json_tests.rb +63 -0
  31. data/test/helpers/parse_helpers.rb +83 -0
  32. data/test/parser/tc_alternation_expression.rb +27 -0
  33. data/test/parser/tc_empty_expression.rb +15 -0
  34. data/test/parser/tc_end_of_file_expression.rb +27 -0
  35. data/test/parser/tc_literal_expression.rb +55 -0
  36. data/test/parser/tc_look_ahead_expression.rb +41 -0
  37. data/test/parser/tc_memoization.rb +31 -0
  38. data/test/parser/tc_optional_expression.rb +31 -0
  39. data/test/parser/tc_parser.rb +78 -0
  40. data/test/parser/tc_query_expression.rb +40 -0
  41. data/test/parser/tc_repetition_expression.rb +76 -0
  42. data/test/parser/tc_rule_expression.rb +32 -0
  43. data/test/parser/tc_scanning.rb +192 -0
  44. data/test/parser/tc_sequence_expression.rb +42 -0
  45. data/test/parser/tc_transform_expression.rb +77 -0
  46. data/test/ts_all.rb +7 -0
  47. data/test/ts_dsl.rb +8 -0
  48. data/test/ts_example.rb +7 -0
  49. data/test/ts_parser.rb +19 -0
  50. metadata +94 -0
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
6
+ require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
7
+
8
+ class TestMemoization < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def setup
13
+ @source = Scanner.new("42")
14
+ end
15
+
16
+ def test_memoization
17
+ calls = 0
18
+ parser = Transform.new(Literal.new(/\d+/)) { |ast| calls += 1; ast }
19
+ cache = Hash.new
20
+
21
+ assert_equal(0, cache.size)
22
+
23
+ parsed = parser.parse(@source, cache)
24
+ @source.pos = 0
25
+ cached = parser.parse(@source, cache)
26
+
27
+ assert_not_equal(0, cache.size)
28
+ assert_same(parsed, cached)
29
+ assert_equal(1, calls)
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
6
+ require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
7
+
8
+ class TestOptionalExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def setup
13
+ @source = Scanner.new("data")
14
+ end
15
+
16
+ def test_match
17
+ assert_parses("data", parse_optional("data"))
18
+ end
19
+
20
+ def test_failed_match
21
+ assert_parses_empty(parse_optional("missing"))
22
+ end
23
+
24
+ def test_equality
25
+ assert_not_equal( Optional.new(Literal.new("one")),
26
+ Optional.new(Literal.new("two")) )
27
+
28
+ assert_equal( Optional.new(Literal.new("one")),
29
+ Optional.new(Literal.new("one")) )
30
+ end
31
+ end
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
6
+
7
+ class TestScanning < Test::Unit::TestCase
8
+ include GhostWheelNamespace
9
+
10
+ def setup
11
+ @parser = Parser.new
12
+ end
13
+
14
+ def test_read_and_write_rules
15
+ assert_nil(@parser[:empty])
16
+
17
+ @parser[:empty] = Empty.instance
18
+ @parser[:space] = Literal.new(/\s+/)
19
+ assert_equal(Empty.instance, @parser[:empty])
20
+ assert_equal(Literal.new(/\s+/), @parser[:space])
21
+ end
22
+
23
+ def test_no_parsers_for_rules
24
+ @parser[:empty] = Empty.instance
25
+ assert( !@parser.respond_to?(:parse_empty),
26
+ "A parse_empty() method was defined, but not expected." )
27
+ assert_raise(NoMethodError) { @parser.parse_empty("not_used") }
28
+ assert( !@parser.respond_to?(:parse),
29
+ "A parse() method was defined, but not expected." )
30
+ assert_raise(NoMethodError) { @parser.parse("not_used") }
31
+ end
32
+
33
+ def test_named_parsers
34
+ add_parsers
35
+ assert_respond_to(@parser, :parse_empty)
36
+ assert_raise(GhostWheel::EmptyParseError) do
37
+ @parser.parse_empty("not_used")
38
+ end
39
+ assert_respond_to(@parser, :parse_space)
40
+ assert_equal(" ", @parser.parse_space(" word"))
41
+ end
42
+
43
+ def test_parse
44
+ # parse() works with just one rule
45
+ add_empty_parser
46
+ assert_respond_to(@parser, :parse)
47
+ assert_raise(GhostWheel::EmptyParseError) { @parser.parse("not_used") }
48
+
49
+ # but not with more than one
50
+ add_space_parser
51
+ assert( !@parser.respond_to?(:parse),
52
+ "A parse() method was defined, but not expected." )
53
+ assert_raise(NoMethodError) { @parser.parse("not_used") }
54
+ end
55
+
56
+ def test_pass_extra_arg
57
+ add_empty_parser
58
+ assert_equal(42, @parser.parse_empty("not_used", 42))
59
+ assert_equal(42, @parser.parse("not_used", 42))
60
+ end
61
+
62
+ private
63
+
64
+ def add_empty_parser
65
+ @parser[:empty] = Empty.instance
66
+ @parser.build_parser(:empty)
67
+ end
68
+
69
+ def add_space_parser
70
+ @parser[:space] = Literal.new(/\s+/)
71
+ @parser.build_parser(:space)
72
+ end
73
+
74
+ def add_parsers
75
+ add_empty_parser
76
+ add_space_parser
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
6
+ require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
7
+
8
+ class TestQueryExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def setup
13
+ @parser = Query.new(Literal.new(/\d+/)) { |i| i.to_i % 2 == 0 }
14
+ end
15
+
16
+ def test_query_match
17
+ assert_parses("12", parse("12"))
18
+ end
19
+
20
+ def test_failed_query_match
21
+ assert_doesnt_parse(parse("13"))
22
+ end
23
+
24
+ def test_yields_empty
25
+ Query.new(Empty.instance) do |empty|
26
+ assert_instance_of(EmptyParseResult, empty)
27
+ end.parse(Scanner.new("not_used"))
28
+ end
29
+
30
+ def test_equality
31
+ assert_not_equal( Query.new(Literal.new("one")) { },
32
+ Query.new(Literal.new("two")) { } )
33
+ assert_not_equal( Query.new(Literal.new("one")) { true },
34
+ Query.new(Literal.new("one")) { } )
35
+
36
+ block = lambda { true } # must be *exact* same block
37
+ assert_equal( Query.new(Literal.new("one"), &block),
38
+ Query.new(Literal.new("one"), &block) )
39
+ end
40
+ end
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
6
+ require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
7
+
8
+ class TestRepetitionExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def setup
13
+ @source = Scanner.new("X" * 10 + "YZ")
14
+ end
15
+
16
+ def test_zero_or_more_match
17
+ assert_parses(["X"] * 10, parse_repetition("X"))
18
+ end
19
+
20
+ def test_failed_zero_or_more_match
21
+ assert_parses_empty(parse_repetition("Z"))
22
+ end
23
+
24
+ def test_one_or_more_match
25
+ assert_parses(["X"] * 10, parse_repetition("X", 1))
26
+ end
27
+
28
+ def test_failed_one_or_more_match
29
+ assert_doesnt_parse(parse_repetition("Z", 1))
30
+ end
31
+
32
+ def test_with_minimum_match
33
+ assert_parses(["X"] * 10, parse_repetition("X", 5))
34
+ end
35
+
36
+ def test_failed_with_minimum_match
37
+ assert_doesnt_parse(parse_repetition("Z", 15))
38
+ end
39
+
40
+ def test_with_maximum_match
41
+ assert_parses(["X"] * 3, parse_repetition("X", 0, 3))
42
+ assert_parses(["X"] * 7, parse_repetition("X", 0, 20))
43
+ end
44
+
45
+ def test_failed_with_maximum_match
46
+ assert_parses_empty(parse_repetition("X", 0, 0))
47
+ end
48
+
49
+ def test_with_minimum_and_maximum_match
50
+ assert_parses(["X"] * 3, parse_repetition("X", 1, 3))
51
+ end
52
+
53
+ def test_failed_with_minimum_and_maximum_match
54
+ assert_doesnt_parse(parse_repetition("X", 20, 40))
55
+ end
56
+
57
+ def test_exact_count_match
58
+ assert_parses(["X"] * 3, parse_repetition("X", 3, 3))
59
+ end
60
+
61
+ def test_failed_exact_count_match
62
+ assert_doesnt_parse(parse_repetition("X", 11, 11))
63
+ end
64
+
65
+ def test_equality
66
+ assert_not_equal( Repetition.new(Literal.new("one")),
67
+ Repetition.new(Literal.new("two")) )
68
+ assert_not_equal( Repetition.new(Literal.new("one"), 1),
69
+ Repetition.new(Literal.new("one")) )
70
+ assert_not_equal( Repetition.new(Literal.new("one"), 1, 5),
71
+ Repetition.new(Literal.new("one"), 1) )
72
+
73
+ assert_equal( Repetition.new(Literal.new("one")),
74
+ Repetition.new(Literal.new("one")) )
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
6
+ require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
7
+
8
+ class TestRuleExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def test_simple_wrapping
13
+ assert_parses_empty(Rule.new(Empty.instance).parse(Scanner.new("not_used")))
14
+ end
15
+
16
+ def test_forward_declaration
17
+ rule = Rule.new
18
+ assert_raise(EmptyRuleError) { rule.parse(Scanner.new("not_used")) }
19
+ rule.expression = Empty.instance
20
+ assert_parses_empty(rule.parse(Scanner.new("not_used")))
21
+ end
22
+
23
+ def test_equality
24
+ assert_not_equal(Rule.new(Literal.new("one")), Rule.new)
25
+ assert_not_equal( Rule.new(Literal.new("one")),
26
+ Rule.new(Literal.new("two")) )
27
+
28
+ assert_equal(Rule.new, Rule.new)
29
+ assert_equal( Rule.new(Literal.new("one")),
30
+ Rule.new(Literal.new("one")) )
31
+ end
32
+ end
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
6
+
7
+ class TestScanning < Test::Unit::TestCase
8
+ include GhostWheelNamespace
9
+
10
+ def setup
11
+ @scanner = Scanner.new("three little words")
12
+ end
13
+
14
+ def test_scanner_advances_on_match
15
+ assert_equal("three little words", @scanner.rest)
16
+ assert_scan_to("little words", /\S+\s+/)
17
+ assert_scan_to("words", /\S+\s+/)
18
+ end
19
+
20
+ def test_failed_match_does_not_advance
21
+ assert_equal("three little words", @scanner.rest)
22
+ assert_doesnt_scan(/fails/)
23
+ assert_scan_to("little words", /\S+\s+/)
24
+ end
25
+
26
+ def test_successful_transaction
27
+ assert_equal("three little words", @scanner.rest)
28
+
29
+ @scanner.transaction do
30
+ assert_doesnt_scan(/fails/)
31
+ assert_scan_to("little words", /\S+\s+/)
32
+ end
33
+
34
+ assert_equal("little words", @scanner.rest)
35
+ end
36
+
37
+ def test_aborted_transaction
38
+ assert_equal("three little words", @scanner.rest)
39
+
40
+ assert_nothing_raised(Exception) do
41
+ @scanner.transaction do
42
+ assert_scan_to("little words", /\S+\s+/)
43
+
44
+ @scanner.abort
45
+
46
+ flunk("Should never reach here.")
47
+ end
48
+ end
49
+
50
+ assert_equal("three little words", @scanner.rest)
51
+ end
52
+
53
+ def test_nested_transactions
54
+ assert_equal("three little words", @scanner.rest)
55
+
56
+ # nested aborts...
57
+ @scanner.transaction do
58
+ assert_scan_to("little words", /\S+\s+/)
59
+
60
+ @scanner.transaction do
61
+ assert_scan_to("words", /\S+\s+/)
62
+
63
+ @scanner.abort
64
+
65
+ flunk("Should never reach here.")
66
+ end
67
+
68
+ assert_equal("little words", @scanner.rest)
69
+
70
+ @scanner.abort
71
+
72
+ flunk("Should never reach here.")
73
+ end
74
+
75
+ assert_equal("three little words", @scanner.rest)
76
+
77
+ # nested success...
78
+ @scanner.transaction do
79
+ assert_scan_to("little words", /\S+\s+/)
80
+
81
+ @scanner.transaction do
82
+ assert_scan_to("words", /\S+\s+/)
83
+ end
84
+
85
+ assert_equal("words", @scanner.rest)
86
+ end
87
+
88
+ assert_equal("words", @scanner.rest)
89
+ end
90
+
91
+ def test_successful_transactions_auto_commit_changes
92
+ assert_equal("three little words", @scanner.rest)
93
+
94
+ @scanner.transaction do
95
+ assert_scan_to("little words", /\S+\s+/)
96
+
97
+ @scanner.transaction do
98
+ assert_scan_to("words", /\S+\s+/)
99
+ end
100
+
101
+ assert_equal("words", @scanner.rest)
102
+
103
+ @scanner.abort
104
+
105
+ flunk("Should never reach here.")
106
+ end
107
+
108
+ assert_equal("three little words", @scanner.rest)
109
+ end
110
+
111
+ def test_exception_aborts_transaction
112
+ assert_equal("three little words", @scanner.rest)
113
+
114
+ @scanner.transaction do
115
+ assert_scan_to("little words", /\S+\s+/)
116
+
117
+ assert_raise(NoMethodError) do
118
+ @scanner.transaction do
119
+ assert_scan_to("words", /\S+\s+/)
120
+
121
+ Object.new.no_such_method # raise an exception
122
+
123
+ flunk("Should never reach here.")
124
+ end
125
+ end
126
+
127
+ assert_equal("little words", @scanner.rest)
128
+ end
129
+
130
+ assert_equal("little words", @scanner.rest)
131
+ end
132
+
133
+ def test_stack_underflow_error
134
+ # abort() without opening a transaction
135
+ assert_raise(StackUnderflowError) { @scanner.abort }
136
+ end
137
+
138
+ def test_eos_detection
139
+ assert(!@scanner.eos?, "Detected eos?() with content left.")
140
+ assert_scan_to(String.new, /.*/m)
141
+ assert(@scanner.eos?, "Failed to detect eos?().")
142
+ end
143
+
144
+ def test_transaction_return_value
145
+ assert_equal("success", @scanner.transaction { "success" })
146
+ assert_equal(
147
+ "failure",
148
+ @scanner.transaction { @scanner.abort("failure"); "success" }
149
+ )
150
+ end
151
+
152
+ def test_pos
153
+ assert_equal("three little words", @scanner.rest)
154
+
155
+ saved_pos = nil
156
+ @scanner.transaction do
157
+ assert_scan_to("little words", /\S+\s+/)
158
+
159
+ @scanner.transaction do
160
+ assert_scan_to("words", /\S+\s+/)
161
+
162
+ saved_pos = @scanner.pos
163
+
164
+ @scanner.abort
165
+
166
+ flunk("Should never reach here.")
167
+ end
168
+
169
+ assert_equal("little words", @scanner.rest)
170
+
171
+ @scanner.abort
172
+
173
+ flunk("Should never reach here.")
174
+ end
175
+
176
+ assert_equal("three little words", @scanner.rest)
177
+
178
+ @scanner.pos = saved_pos
179
+ assert_equal("words", @scanner.rest)
180
+ end
181
+
182
+ private
183
+
184
+ def assert_scan_to(rest, regexp, message = "Scan failed.")
185
+ assert_block(message) { @scanner.scan(regexp) and @scanner.rest == rest }
186
+ end
187
+
188
+ def assert_doesnt_scan(regexp, message = "Scan matched unexpectedly.")
189
+ rest = @scanner.rest
190
+ assert_block(message) { !@scanner.scan(regexp) and @scanner.rest == rest }
191
+ end
192
+ end