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,48 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
6
+ require File.join(File.dirname(__FILE__), %w[.. helpers json_tests])
7
+
8
+ class TestJSONGhostWheel < Test::Unit::TestCase
9
+ include ParseHelpers
10
+ include JSONTests
11
+
12
+ def setup
13
+ @parser = GhostWheel.build_parser( %q{
14
+ keyword = 'true' { true } | 'false' { false } | 'null' { nil }
15
+
16
+ number = /-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?/
17
+ { ast.include?(".") ? Float(ast) : Integer(ast) }
18
+
19
+ string_content = /\\\\["\\\\\/]/ { ast[-1, 1] }
20
+ | /\\\\[bfnrt]/
21
+ { Hash[*%W[b \n f \f n \n r \r t \t]][ast[-1, 1]] }
22
+ | /\\\\u[0-9a-fA-F]{4}/
23
+ { [Integer("0x#{ast[2..-1]}")].pack("U") }
24
+ | /[^\\\\"]+/
25
+ string = '"' string_content* '"' { ast.flatten[1..-2].join }
26
+
27
+ array_content = value_with_array_sep+ value
28
+ { ast[0].inject([]) { |a, v| a.push(*v) } + ast[-1..-1] }
29
+ | value? { ast.is_a?(EmptyParseResult) ? [] : [ast] }
30
+ array = /\[\s*/ array_content /\s*\]/ { ast[1] }
31
+
32
+ object_pair = string /\s*:\s*/ value { {ast[0] => ast[-1]} }
33
+ object_pair_and_sep = object_pair /\s*;\s*/ { ast[0] }
34
+ object_content = object_pair_and_sep+ object_pair { ast.flatten }
35
+ | object_pair?
36
+ { ast.is_a?(EmptyParseResult) ? [] : [ast] }
37
+ object = /\\\{\s*/ object_content /\\\}\s*/
38
+ { ast[1].inject({}) { |h, p| h.merge(p) } }
39
+
40
+ value_space = /\s*/
41
+ value_content = keyword | number | string | array | object
42
+ value = value_space value_content value_space { ast[1] }
43
+ value_with_array_sep = value /\s*,\s*/ { ast[0] }
44
+
45
+ json := value EOF { ast[0] }
46
+ } )[:json]
47
+ end
48
+ end
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "test/unit"
4
+
5
+ require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
6
+ require File.join(File.dirname(__FILE__), %w[.. helpers json_tests])
7
+
8
+ class TestJSONRuby < Test::Unit::TestCase
9
+ include ParseHelpers
10
+ include JSONTests
11
+
12
+ def setup
13
+ @parser = GhostWheel.build_parser do
14
+ rule( :keyword,
15
+ alt("true", "false", "null") { |k|
16
+ {"true" => true, "false" => false, "null" => nil}[k]
17
+ } )
18
+
19
+ rule( :number,
20
+ /-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?/ ) { |n|
21
+ n.include?(".") ? Float(n) : Integer(n)
22
+ }
23
+
24
+ rule( :string,
25
+ seq(
26
+ skip('"'),
27
+ zplus(
28
+ alt(
29
+ lit(%r{\\["\\/]}) { |e| e[-1, 1] },
30
+ lit(/\\[bfnrt]/) { |e|
31
+ Hash[*%W[b \n f \f n \n r \r t \t]][e[-1, 1]]
32
+ },
33
+ lit(/\\u[0-9a-fA-F]{4}/) { |e|
34
+ [Integer("0x#{e[2..-1]}")].pack("U")
35
+ },
36
+ /[^\\"]+/
37
+ )
38
+ ),
39
+ skip('"')
40
+ ) { |s| s.flatten.join } )
41
+
42
+ rule( :array,
43
+ seq(
44
+ skip(/\[\s*/),
45
+ alt(
46
+ seq(
47
+ oplus(seq(:value, skip(/\s*,\s*/))),
48
+ :value
49
+ ) { |a| a[0].inject([]) { |vs, v| vs.push(*v) } + a[-1..-1] },
50
+ seq(opt(:value))
51
+ ),
52
+ skip(/\s*\]/)
53
+ ) { |a| a.first } )
54
+
55
+ rule( :object_pair,
56
+ seq(:string, /\s*:\s*/, :value) { |kv| {kv[0] => kv[-1]} } )
57
+ rule( :object,
58
+ seq(
59
+ skip(/\{\s*/),
60
+ alt(
61
+ seq(
62
+ oplus(seq(:object_pair, skip(/\s*;\s*/))),
63
+ :object_pair
64
+ ) { |ps| ps[0][0] + ps[-1..-1] },
65
+ seq(opt(:object_pair))
66
+ ),
67
+ skip(/\}\s*/)
68
+ ) { |ps| ps[0].inject({}) { |h, p| h.merge(p) } } )
69
+
70
+ rule(:value_space, opt(skip(/\s+/)))
71
+ rule( :value,
72
+ seq(
73
+ :value_space,
74
+ alt(:object, :array, :string, :number, :keyword),
75
+ :value_space
76
+ ) { |v| v[0] } )
77
+
78
+ parser(:json, seq(:value, eof) { |j| j[0] })
79
+ end[:json]
80
+ end
81
+ end
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "ghost_wheel"
4
+
5
+ #
6
+ # Including this module in a test case embeds the GhostWheel namespace for those
7
+ # tests. Constants both in the root GhostWheel module and in the
8
+ # GhostWheel::Expression class may be used without qualification.
9
+ #
10
+ module GhostWheelNamespace
11
+ #
12
+ # Includes the GhostWheel module in +test_case+ and builds a
13
+ # self.const_missing() method to handle lookups in the GhostWheel::Expression
14
+ # class.
15
+ #
16
+ def self.included(test_case)
17
+ test_case.instance_eval do
18
+ include GhostWheel
19
+ def const_missing(const)
20
+ GhostWheel::Expression.const_get(const) rescue super
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ module JSONTests
4
+ def test_keyword_parsing
5
+ assert_parses(true, parse("true"))
6
+ assert_parses(false, parse("false"))
7
+ assert_parses(nil, parse("null"))
8
+ end
9
+
10
+ def test_number_parsing
11
+ assert_parses(42, parse("42"))
12
+ assert_parses(-13, parse("-13"))
13
+ assert_parses(3.1415, parse("3.1415"))
14
+ assert_parses(-0.01, parse("-0.01"))
15
+
16
+ assert_parses(0.2e1, parse("0.2e1"))
17
+ assert_parses(0.2e+1, parse("0.2e+1"))
18
+ assert_parses(0.2e-1, parse("0.2e-1"))
19
+ assert_parses(0.2E1, parse("0.2e1"))
20
+ end
21
+
22
+ def test_string_parsing
23
+ assert_parses(String.new, parse(%Q{""}))
24
+ assert_parses("James", parse(%Q{"James"}))
25
+
26
+ assert_parses(%Q{nested "quotes"}, parse('"nested \"quotes\""'))
27
+ assert_parses("\n", parse(%Q{"\\n"}))
28
+ assert_parses("a", parse(%Q{"\\u#{"%04X" % ?a}"}))
29
+ end
30
+
31
+ def test_array_parsing
32
+ assert_parses(Array.new, parse(%Q{[]}))
33
+ assert_parses(["James", 3.1415, true], parse(%Q{["James", 3.1415, true]}))
34
+ assert_parses([1, [2, [3]]], parse(%Q{[1, [2, [3]]]}))
35
+ end
36
+
37
+ def test_object_parsing
38
+ assert_parses(Hash.new, parse(%Q{{}}))
39
+ assert_parses( {"James" => 3.1415, "Gray" => true},
40
+ parse(%Q{{"James": 3.1415; "Gray": true}}) )
41
+ assert_parses( {"Array" => [1, 2, 3], "Object" => {"nested" => "objects"}},
42
+ parse(<<-END_OBJECT) )
43
+ {"Array": [1, 2, 3]; "Object": {"nested": "objects"}}
44
+ END_OBJECT
45
+ end
46
+
47
+ def test_error_parsing
48
+ assert_doesnt_parse(parse("unknown"))
49
+
50
+ assert_doesnt_parse(parse("$1,000"))
51
+ assert_doesnt_parse(parse("1_000"))
52
+ assert_doesnt_parse(parse("1K"))
53
+
54
+ assert_doesnt_parse(parse(%Q{"}))
55
+ assert_doesnt_parse(parse(%Q{"\\i"}))
56
+
57
+ assert_doesnt_parse(parse("["))
58
+ assert_doesnt_parse(parse("[1,,2]"))
59
+
60
+ assert_doesnt_parse(parse("{"))
61
+ assert_doesnt_parse(parse(%q{{"key": true false}}))
62
+ end
63
+ end
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require "ghost_wheel"
4
+
5
+ require File.join(File.dirname(__FILE__), "ghost_wheel_namespace")
6
+
7
+ module ParseHelpers
8
+ include GhostWheelNamespace
9
+
10
+ private
11
+
12
+ ##################
13
+ ### Assertions ###
14
+ ##################
15
+
16
+ def assert_parses(expected_ast, actual_ast, message = nil)
17
+ if message.nil?
18
+ parsed = actual_ast.is_a?(ParseResult) ? actual_ast.value.inspect :
19
+ actual_ast.class
20
+ message = "Parsed #{parsed} while expecting #{expected_ast.inspect}."
21
+ end
22
+
23
+ assert_block(message) do
24
+ actual_ast.is_a?(ParseResult) and actual_ast.value == expected_ast
25
+ end
26
+ end
27
+
28
+ def assert_doesnt_parse(actual_ast, message = "Parse succeeded.")
29
+ assert_block(message) { actual_ast.is_a?(FailedParseResult) }
30
+ end
31
+
32
+ def assert_parses_empty(actual_ast, message = "Parse was not empty.")
33
+ assert_block(message) { actual_ast.is_a?(EmptyParseResult) }
34
+ end
35
+
36
+ ########################
37
+ ### Parser Shortcuts ###
38
+ ########################
39
+
40
+ def parse(source)
41
+ @parser.parse(Scanner.new(source))
42
+ end
43
+
44
+ def parse_alternation(*expressions)
45
+ Alternation.new(*wrap_literals(expressions)).parse(@source)
46
+ end
47
+
48
+ def parse_eof
49
+ EndOfFile.instance.parse(@source)
50
+ end
51
+
52
+ def parse_literal(literal, *args)
53
+ Literal.new(literal, *args).parse(@source)
54
+ end
55
+
56
+ def parse_look_ahead(literal, *args)
57
+ LookAhead.new(Literal.new(literal), *args).parse(@source)
58
+ end
59
+
60
+ def parse_optional(literal)
61
+ Optional.new(Literal.new(literal)).parse(@source)
62
+ end
63
+
64
+ def parse_repetition(literal, *args)
65
+ Repetition.new(Literal.new(literal), *args).parse(@source)
66
+ end
67
+
68
+ def parse_sequence(*expressions)
69
+ Sequence.new(*wrap_literals(expressions)).parse(@source)
70
+ end
71
+
72
+ ########################
73
+ ### Literal Wrappers ###
74
+ ########################
75
+
76
+ def wrap_literal(literal)
77
+ literal.is_a?(Expression) ? literal : Literal.new(literal)
78
+ end
79
+
80
+ def wrap_literals(literals)
81
+ literals.map { |literal| wrap_literal(literal) }
82
+ end
83
+ end
@@ -0,0 +1,27 @@
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 TestAlternationExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def setup
13
+ @source = Scanner.new("null")
14
+ end
15
+
16
+ def test_match
17
+ assert_parses("null", parse_alternation(*%w[true false null]))
18
+ end
19
+
20
+ def test_failed_match
21
+ assert_doesnt_parse(parse_alternation(*%w[true false nil]))
22
+ end
23
+
24
+ def test_empty_match
25
+ assert_parses_empty(parse_alternation(Empty.instance))
26
+ end
27
+ end
@@ -0,0 +1,15 @@
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 TestEmptyExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def test_empty_expression
13
+ assert_parses_empty(Empty.instance.parse(Scanner.new("not_used")))
14
+ end
15
+ end
@@ -0,0 +1,27 @@
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 TestEndOfFileExpression < 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
+ test_failed_match
18
+ assert_parses_empty(parse_eof)
19
+ end
20
+
21
+ def test_failed_match
22
+ 4.times do
23
+ assert_doesnt_parse(parse_eof)
24
+ @source.scan(/./)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
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 TestLiteralExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def setup
13
+ @source = Scanner.new("some words to parse")
14
+ end
15
+
16
+ def test_string_match
17
+ assert_parses("some", parse_literal("some"))
18
+ end
19
+
20
+ def test_failed_string_match
21
+ assert_doesnt_parse(parse_literal("doesn't match"))
22
+ end
23
+
24
+ def test_character_match
25
+ assert_parses("s", parse_literal(?s))
26
+ end
27
+
28
+ def test_failed_character_match
29
+ assert_doesnt_parse(parse_literal(?S))
30
+ end
31
+
32
+ def test_regexp_match
33
+ assert_parses("some words ", parse_literal(/(?:\w+\s+){2}/))
34
+ end
35
+
36
+ def test_failed_regexp_match
37
+ assert_doesnt_parse(parse_literal(/(?=missing)/))
38
+ end
39
+
40
+ def test_skip_literal_match
41
+ assert_parses_empty(parse_literal("some", true))
42
+ end
43
+
44
+ def test_failed_skip_literal_match
45
+ assert_doesnt_parse(parse_literal("doesn't match", true))
46
+ end
47
+
48
+ def test_equality
49
+ assert_not_equal(Literal.new("keyword"), Literal.new("tag"))
50
+ assert_not_equal(Literal.new("keyword"), Literal.new("keyword", true))
51
+
52
+ assert_equal(Literal.new("keyword"), Literal.new("keyword"))
53
+ assert_equal(Literal.new("keyword"), Literal.new(/keyword/))
54
+ end
55
+ end
@@ -0,0 +1,41 @@
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 TestLookAheadExpression < Test::Unit::TestCase
9
+ include GhostWheelNamespace
10
+ include ParseHelpers
11
+
12
+ def setup
13
+ @source = Scanner.new("ahead")
14
+ end
15
+
16
+ def test_look_ahead_match
17
+ assert_parses_empty(parse_look_ahead("ahead"))
18
+ end
19
+
20
+ def test_failed_look_ahead_match
21
+ assert_doesnt_parse(parse_look_ahead("missing"))
22
+ end
23
+
24
+ def test_negative_look_ahead_match
25
+ assert_parses_empty(parse_look_ahead("missing", true))
26
+ end
27
+
28
+ def test_failed_negative_look_ahead_match
29
+ assert_doesnt_parse(parse_look_ahead("ahead", true))
30
+ end
31
+
32
+ def test_equality
33
+ assert_not_equal( LookAhead.new(Literal.new("one")),
34
+ LookAhead.new(Literal.new("two")) )
35
+ assert_not_equal( LookAhead.new(Literal.new("one"), true),
36
+ LookAhead.new(Literal.new("one")) )
37
+
38
+ assert_equal( LookAhead.new(Literal.new("one")),
39
+ LookAhead.new(Literal.new("one")) )
40
+ end
41
+ end