ghostwheel 0.0.1

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