gherkin-ruby 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Rakefile +19 -1
- data/Readme.md +2 -2
- data/gherkin-ruby.gemspec +2 -1
- data/lib/gherkin/ast.rb +18 -17
- data/lib/gherkin/parser/gherkin.rex +44 -0
- data/lib/gherkin/parser/gherkin.y +100 -0
- data/lib/gherkin/parser/lexer.rb +120 -0
- data/lib/gherkin/parser/parser.rb +359 -0
- data/lib/gherkin/parser.rb +1 -43
- data/lib/gherkin/version.rb +1 -1
- data/lib/gherkin.rb +2 -7
- data/test/gherkin/ast_test.rb +19 -32
- data/test/gherkin/parser/lexer_test.rb +43 -0
- data/test/gherkin/parser/parser_test.rb +96 -0
- data/test/gherkin/parser_test.rb +52 -148
- metadata +85 -40
- data/lib/gherkin/transform.rb +0 -50
- data/test/gherkin/transform_test.rb +0 -74
data/.gitignore
CHANGED
data/Rakefile
CHANGED
@@ -8,4 +8,22 @@ Rake::TestTask.new do |t|
|
|
8
8
|
t.test_files = FileList['./test/**/*_test.rb']
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
desc "Regenerate Gherkin-ruby lexer and parser."
|
12
|
+
task :regenerate do
|
13
|
+
has_rex = `which rex`
|
14
|
+
has_racc = `which racc`
|
15
|
+
|
16
|
+
if has_rex && has_racc
|
17
|
+
`rex lib/gherkin/parser/gherkin.rex -o lib/gherkin/parser/lexer.rb`
|
18
|
+
`racc lib/gherkin/parser/gherkin.y -o lib/gherkin/parser/parser.rb`
|
19
|
+
else
|
20
|
+
puts "You need both Rexical and Racc to do that. Install them by doing:"
|
21
|
+
puts
|
22
|
+
puts "\t\tgem install rexical"
|
23
|
+
puts "\t\tgem install racc"
|
24
|
+
puts
|
25
|
+
puts "Or just type `bundle install`."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
task :default => [:regenerate, :test]
|
data/Readme.md
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
# gherkin-ruby
|
2
|
-
Gherkin-ruby is a pure Ruby implementation of a [Gherkin](http://github.com/cucumber/gherkin) parser
|
2
|
+
Gherkin-ruby is a pure Ruby implementation of a [Gherkin](http://github.com/cucumber/gherkin) parser.
|
3
3
|
|
4
4
|
## Why this one over the official, fast, Ragel-based Gherkin parser?
|
5
5
|
|
6
6
|
* Less than 200 LOC.
|
7
|
-
* No C-exts
|
8
7
|
* No Java/.NET crap.
|
9
8
|
* Fast enough for our purposes (using it for the [Spinach](http://github.com/codegram/spinach) project)
|
10
9
|
|
@@ -74,3 +73,4 @@ visitor.visit(ast)
|
|
74
73
|
## Todo
|
75
74
|
|
76
75
|
* Some optimization
|
76
|
+
|
data/gherkin-ruby.gemspec
CHANGED
@@ -13,7 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
|
14
14
|
s.rubyforge_project = "gherkin-ruby"
|
15
15
|
|
16
|
-
s.add_runtime_dependency '
|
16
|
+
s.add_runtime_dependency 'rexical'
|
17
|
+
s.add_runtime_dependency 'racc'
|
17
18
|
s.add_development_dependency 'minitest'
|
18
19
|
|
19
20
|
s.files = `git ls-files`.split("\n")
|
data/lib/gherkin/ast.rb
CHANGED
@@ -1,25 +1,28 @@
|
|
1
1
|
module Gherkin
|
2
2
|
module AST
|
3
3
|
class Node
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :filename, :line
|
5
5
|
|
6
6
|
def accept(visitor)
|
7
7
|
name = self.class.name.split('::').last
|
8
8
|
visitor.send("visit_#{name}".to_sym, self)
|
9
9
|
end
|
10
|
+
|
11
|
+
def pos(filename, line=nil)
|
12
|
+
@filename, @line = filename, line
|
13
|
+
end
|
10
14
|
end
|
11
15
|
|
12
16
|
class Feature < Node
|
13
17
|
attr_reader :name, :background, :scenarios
|
18
|
+
attr_writer :background, :scenarios
|
14
19
|
|
15
20
|
include Enumerable
|
16
21
|
|
17
22
|
def initialize(name, scenarios=[], background=nil)
|
18
|
-
@
|
19
|
-
|
20
|
-
@name = name.to_s
|
23
|
+
@name = name
|
21
24
|
@background = background
|
22
|
-
@scenarios
|
25
|
+
@scenarios = scenarios
|
23
26
|
end
|
24
27
|
|
25
28
|
def each
|
@@ -29,18 +32,18 @@ module Gherkin
|
|
29
32
|
|
30
33
|
class Background < Node
|
31
34
|
attr_reader :steps
|
35
|
+
attr_writer :steps
|
32
36
|
|
33
37
|
include Enumerable
|
34
38
|
|
35
39
|
def initialize(steps=[])
|
36
|
-
if steps.any?
|
37
|
-
@line = steps.first.line - 1
|
38
|
-
@column = 3
|
39
|
-
end
|
40
|
-
|
41
40
|
@steps = steps
|
42
41
|
end
|
43
42
|
|
43
|
+
def line
|
44
|
+
@steps.first.line - 1 if @steps.any?
|
45
|
+
end
|
46
|
+
|
44
47
|
def each
|
45
48
|
@steps.each
|
46
49
|
end
|
@@ -52,13 +55,15 @@ module Gherkin
|
|
52
55
|
include Enumerable
|
53
56
|
|
54
57
|
def initialize(name, steps=[], tags=[])
|
55
|
-
@line, @column = name.line_and_column
|
56
|
-
|
57
58
|
@name = name.to_s
|
58
59
|
@steps = steps
|
59
60
|
@tags = tags
|
60
61
|
end
|
61
62
|
|
63
|
+
def line
|
64
|
+
@steps.first.line - 1 if @steps.any?
|
65
|
+
end
|
66
|
+
|
62
67
|
def each
|
63
68
|
@steps.each
|
64
69
|
end
|
@@ -67,8 +72,6 @@ module Gherkin
|
|
67
72
|
class Step < Node
|
68
73
|
attr_reader :name, :keyword
|
69
74
|
def initialize(name, keyword)
|
70
|
-
@line, @column = name.line_and_column
|
71
|
-
|
72
75
|
@name = name.to_s
|
73
76
|
@keyword = keyword.to_s
|
74
77
|
end
|
@@ -77,9 +80,7 @@ module Gherkin
|
|
77
80
|
class Tag < Node
|
78
81
|
attr_reader :name
|
79
82
|
def initialize(name)
|
80
|
-
@
|
81
|
-
|
82
|
-
@name = name.to_s
|
83
|
+
@name = name
|
83
84
|
end
|
84
85
|
end
|
85
86
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Compile with: rex gherkin.rex -o lexer.rb
|
2
|
+
|
3
|
+
class Gherkin::Parser
|
4
|
+
|
5
|
+
macro
|
6
|
+
BLANK [\ \t]+
|
7
|
+
|
8
|
+
rule
|
9
|
+
# Whitespace
|
10
|
+
{BLANK} # no action
|
11
|
+
\#.*$
|
12
|
+
|
13
|
+
# Literals
|
14
|
+
\n { [:NEWLINE, text] }
|
15
|
+
|
16
|
+
# Keywords
|
17
|
+
Feature: { [:FEATURE, text[0..-2]] }
|
18
|
+
Background: { [:BACKGROUND, text[0..-2]] }
|
19
|
+
Scenario: { [:SCENARIO, text[0..-2]] }
|
20
|
+
|
21
|
+
# Tags
|
22
|
+
@\w+ { [:TAG, text[1..-1]] }
|
23
|
+
|
24
|
+
# Step keywords
|
25
|
+
Given { [:GIVEN, text] }
|
26
|
+
When { [:WHEN, text] }
|
27
|
+
Then { [:THEN, text] }
|
28
|
+
And { [:AND, text] }
|
29
|
+
But { [:BUT, text] }
|
30
|
+
|
31
|
+
# Text
|
32
|
+
[^#\n]* { [:TEXT, text.strip] }
|
33
|
+
|
34
|
+
inner
|
35
|
+
def tokenize(code)
|
36
|
+
scan_setup(code)
|
37
|
+
tokens = []
|
38
|
+
while token = next_token
|
39
|
+
tokens << token
|
40
|
+
end
|
41
|
+
tokens
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# Compile with: racc gherkin.y -o parser.rb
|
2
|
+
|
3
|
+
class Gherkin::Parser
|
4
|
+
|
5
|
+
# Declare tokens produced by the lexer
|
6
|
+
token NEWLINE
|
7
|
+
token FEATURE BACKGROUND SCENARIO
|
8
|
+
token TAG
|
9
|
+
token GIVEN WHEN THEN AND BUT
|
10
|
+
token TEXT
|
11
|
+
|
12
|
+
rule
|
13
|
+
|
14
|
+
Root:
|
15
|
+
Feature { result = val[0]; }
|
16
|
+
|
|
17
|
+
Feature
|
18
|
+
Scenarios { result = val[0]; result.scenarios = val[1] }
|
19
|
+
;
|
20
|
+
|
21
|
+
Newline:
|
22
|
+
NEWLINE
|
23
|
+
| Newline NEWLINE
|
24
|
+
;
|
25
|
+
|
26
|
+
Feature:
|
27
|
+
FeatureHeader { result = val[0] }
|
28
|
+
| FeatureHeader
|
29
|
+
Background { result = val[0]; result.background = val[1] }
|
30
|
+
;
|
31
|
+
|
32
|
+
FeatureHeader:
|
33
|
+
FeatureName { result = val[0] }
|
34
|
+
| FeatureName Newline { result = val[0] }
|
35
|
+
| FeatureName Newline
|
36
|
+
Description { result = val[0] }
|
37
|
+
;
|
38
|
+
|
39
|
+
FeatureName:
|
40
|
+
FEATURE TEXT { result = AST::Feature.new(val[1]); result.pos(filename, lineno) }
|
41
|
+
| Newline FEATURE TEXT { result = AST::Feature.new(val[2]); result.pos(filename, lineno) }
|
42
|
+
;
|
43
|
+
|
44
|
+
Description:
|
45
|
+
TEXT Newline
|
46
|
+
| Description TEXT Newline
|
47
|
+
;
|
48
|
+
|
49
|
+
Background:
|
50
|
+
BackgroundHeader
|
51
|
+
Steps { result = val[0]; result.steps = val[1] }
|
52
|
+
;
|
53
|
+
|
54
|
+
BackgroundHeader:
|
55
|
+
BACKGROUND Newline { result = AST::Background.new; result.pos(filename, lineno) }
|
56
|
+
;
|
57
|
+
|
58
|
+
Steps:
|
59
|
+
Step Newline { result = [val[0]] }
|
60
|
+
| Steps Step Newline { result = val[0] << val[1] }
|
61
|
+
;
|
62
|
+
|
63
|
+
Step:
|
64
|
+
Keyword TEXT { result = AST::Step.new(val[1], val[0]); result.pos(filename, lineno) }
|
65
|
+
;
|
66
|
+
|
67
|
+
Keyword:
|
68
|
+
GIVEN | WHEN | THEN | AND | BUT
|
69
|
+
;
|
70
|
+
|
71
|
+
Scenarios:
|
72
|
+
Scenario { result = [val[0]] }
|
73
|
+
| Scenarios Scenario { result = val[0] << val[1] }
|
74
|
+
| Scenarios Newline Scenario { result = val[0] << val[2] }
|
75
|
+
;
|
76
|
+
|
77
|
+
Scenario:
|
78
|
+
SCENARIO TEXT Newline
|
79
|
+
Steps { result = AST::Scenario.new(val[1], val[3]); result.pos(filename, lineno - 1) }
|
80
|
+
| Tags Newline
|
81
|
+
SCENARIO TEXT Newline
|
82
|
+
Steps { result = AST::Scenario.new(val[3], val[5], val[0]); result.pos(filename, lineno - 2) }
|
83
|
+
;
|
84
|
+
|
85
|
+
Tags:
|
86
|
+
TAG { result = [AST::Tag.new(val[0])] }
|
87
|
+
| Tags TAG { result = val[0] << AST::Tag.new(val[1]) }
|
88
|
+
;
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
---- header
|
93
|
+
require_relative "lexer"
|
94
|
+
require_relative "../ast"
|
95
|
+
|
96
|
+
---- inner
|
97
|
+
|
98
|
+
def parse(input)
|
99
|
+
scan_str(input)
|
100
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
#--
|
2
|
+
# DO NOT MODIFY!!!!
|
3
|
+
# This file is automatically generated by rex 1.0.5
|
4
|
+
# from lexical definition file "lib/gherkin/parser/gherkin.rex".
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'racc/parser'
|
8
|
+
# Compile with: rex gherkin.rex -o lexer.rb
|
9
|
+
|
10
|
+
class Gherkin::Parser < Racc::Parser
|
11
|
+
require 'strscan'
|
12
|
+
|
13
|
+
class ScanError < StandardError ; end
|
14
|
+
|
15
|
+
attr_reader :lineno
|
16
|
+
attr_reader :filename
|
17
|
+
attr_accessor :state
|
18
|
+
|
19
|
+
def scan_setup(str)
|
20
|
+
@ss = StringScanner.new(str)
|
21
|
+
@lineno = 1
|
22
|
+
@state = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def action
|
26
|
+
yield
|
27
|
+
end
|
28
|
+
|
29
|
+
def scan_str(str)
|
30
|
+
scan_setup(str)
|
31
|
+
do_parse
|
32
|
+
end
|
33
|
+
alias :scan :scan_str
|
34
|
+
|
35
|
+
def load_file( filename )
|
36
|
+
@filename = filename
|
37
|
+
open(filename, "r") do |f|
|
38
|
+
scan_setup(f.read)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def scan_file( filename )
|
43
|
+
load_file(filename)
|
44
|
+
do_parse
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def next_token
|
49
|
+
return if @ss.eos?
|
50
|
+
|
51
|
+
# skips empty actions
|
52
|
+
until token = _next_token or @ss.eos?; end
|
53
|
+
token
|
54
|
+
end
|
55
|
+
|
56
|
+
def _next_token
|
57
|
+
text = @ss.peek(1)
|
58
|
+
@lineno += 1 if text == "\n"
|
59
|
+
token = case @state
|
60
|
+
when nil
|
61
|
+
case
|
62
|
+
when (text = @ss.scan(/[ \t]+/))
|
63
|
+
;
|
64
|
+
|
65
|
+
when (text = @ss.scan(/\#.*$/))
|
66
|
+
;
|
67
|
+
|
68
|
+
when (text = @ss.scan(/\n/))
|
69
|
+
action { [:NEWLINE, text] }
|
70
|
+
|
71
|
+
when (text = @ss.scan(/Feature:/))
|
72
|
+
action { [:FEATURE, text[0..-2]] }
|
73
|
+
|
74
|
+
when (text = @ss.scan(/Background:/))
|
75
|
+
action { [:BACKGROUND, text[0..-2]] }
|
76
|
+
|
77
|
+
when (text = @ss.scan(/Scenario:/))
|
78
|
+
action { [:SCENARIO, text[0..-2]] }
|
79
|
+
|
80
|
+
when (text = @ss.scan(/@\w+/))
|
81
|
+
action { [:TAG, text[1..-1]] }
|
82
|
+
|
83
|
+
when (text = @ss.scan(/Given/))
|
84
|
+
action { [:GIVEN, text] }
|
85
|
+
|
86
|
+
when (text = @ss.scan(/When/))
|
87
|
+
action { [:WHEN, text] }
|
88
|
+
|
89
|
+
when (text = @ss.scan(/Then/))
|
90
|
+
action { [:THEN, text] }
|
91
|
+
|
92
|
+
when (text = @ss.scan(/And/))
|
93
|
+
action { [:AND, text] }
|
94
|
+
|
95
|
+
when (text = @ss.scan(/But/))
|
96
|
+
action { [:BUT, text] }
|
97
|
+
|
98
|
+
when (text = @ss.scan(/[^#\n]*/))
|
99
|
+
action { [:TEXT, text.strip] }
|
100
|
+
|
101
|
+
else
|
102
|
+
text = @ss.string[@ss.pos .. -1]
|
103
|
+
raise ScanError, "can not match: '" + text + "'"
|
104
|
+
end # if
|
105
|
+
|
106
|
+
else
|
107
|
+
raise ScanError, "undefined state: '" + state.to_s + "'"
|
108
|
+
end # case state
|
109
|
+
token
|
110
|
+
end # def _next_token
|
111
|
+
|
112
|
+
def tokenize(code)
|
113
|
+
scan_setup(code)
|
114
|
+
tokens = []
|
115
|
+
while token = next_token
|
116
|
+
tokens << token
|
117
|
+
end
|
118
|
+
tokens
|
119
|
+
end
|
120
|
+
end # class
|