]> git.wincent.com - walrat.git/blob - spec/parslet_combining_spec.rb
Update to RSpec 3.1.0
[walrat.git] / spec / parslet_combining_spec.rb
1 # Copyright 2007-2014 Greg Hurrell. All rights reserved.
2 # Licensed under the terms of the BSD 2-clause license.
3
4 require 'spec_helper'
5
6 describe 'using shorthand operators to combine String, Symbol and Regexp parsers' do
7   it 'should be able to chain a String and a Regexp together' do
8     # try in one order
9     sequence = 'foo' & /\d+/
10     sequence.parse('foo1000').should == ['foo', '1000']
11     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError) # first part alone is not enough
12     lambda { sequence.parse('1000') }.should raise_error(Walrat::ParseError) # neither is second part alone
13     lambda { sequence.parse('1000foo') }.should raise_error(Walrat::ParseError) # order matters
14
15     # same test but in reverse order
16     sequence = /\d+/ & 'foo'
17     sequence.parse('1000foo').should == ['1000', 'foo']
18     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError) # first part alone is not enough
19     lambda { sequence.parse('1000') }.should raise_error(Walrat::ParseError) # neither is second part alone
20     lambda { sequence.parse('foo1000') }.should raise_error(Walrat::ParseError) # order matters
21   end
22
23   it 'should be able to choose between a String and a Regexp' do
24     # try in one order
25     sequence = 'foo' | /\d+/
26     sequence.parse('foo').should == 'foo'
27     sequence.parse('100').should == '100'
28     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
29
30     # same test but in reverse order
31     sequence = /\d+/ | 'foo'
32     sequence.parse('foo').should == 'foo'
33     sequence.parse('100').should == '100'
34     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
35   end
36
37   it 'should be able to freely intermix String and Regexp objects when chaining and choosing' do
38     sequence = 'foo' & /\d+/ | 'bar' & /[XYZ]{3}/
39     sequence.parse('foo123').should == ['foo', '123']
40     sequence.parse('barZYX').should == ['bar', 'ZYX']
41     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
42     lambda { sequence.parse('123') }.should raise_error(Walrat::ParseError)
43     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
44     lambda { sequence.parse('XYZ') }.should raise_error(Walrat::ParseError)
45     lambda { sequence.parse('barXY') }.should raise_error(Walrat::ParseError)
46   end
47
48   it 'should be able to specify minimum and maximum repetition using shorthand methods' do
49     # optional (same as "?" in regular expressions)
50     sequence = 'foo'.optional
51     sequence.parse('foo').should == 'foo'
52     lambda { sequence.parse('bar') }.should throw_symbol(:ZeroWidthParseSuccess)
53
54     # zero_or_one (same as optional; "?" in regular expressions)
55     sequence = 'foo'.zero_or_one
56     sequence.parse('foo').should == 'foo'
57     lambda { sequence.parse('bar') }.should throw_symbol(:ZeroWidthParseSuccess)
58
59     # zero_or_more (same as "*" in regular expressions)
60     sequence = 'foo'.zero_or_more
61     sequence.parse('foo').should == 'foo'
62     sequence.parse('foofoofoobar').should == ['foo', 'foo', 'foo']
63     lambda { sequence.parse('bar') }.should throw_symbol(:ZeroWidthParseSuccess)
64
65     # one_or_more (same as "+" in regular expressions)
66     sequence = 'foo'.one_or_more
67     sequence.parse('foo').should == 'foo'
68     sequence.parse('foofoofoobar').should == ['foo', 'foo', 'foo']
69     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
70
71     # repeat (arbitary limits for min, max; same as {min, max} in regular expressions)
72     sequence = 'foo'.repeat(3, 5)
73     sequence.parse('foofoofoobar').should == ['foo', 'foo', 'foo']
74     sequence.parse('foofoofoofoobar').should == ['foo', 'foo', 'foo', 'foo']
75     sequence.parse('foofoofoofoofoobar').should == ['foo', 'foo', 'foo', 'foo', 'foo']
76     sequence.parse('foofoofoofoofoofoobar').should == ['foo', 'foo', 'foo', 'foo', 'foo']
77     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
78     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
79     lambda { sequence.parse('foofoo') }.should raise_error(Walrat::ParseError)
80   end
81
82   it 'should be able to apply repetitions to other combinations wrapped in parentheses' do
83     sequence = ('foo' & 'bar').one_or_more
84     sequence.parse('foobar').should == ['foo', 'bar']
85     sequence.parse('foobarfoobar').should == [['foo', 'bar'], ['foo', 'bar']] # fails: just returns ['foo', 'bar']
86   end
87
88   it 'should be able to combine use of repetition shorthand methods with other shorthand methods' do
89     # first we test with chaining
90     sequence = 'foo'.optional & 'bar' & 'abc'.one_or_more
91     sequence.parse('foobarabc').should == ['foo', 'bar', 'abc']
92     sequence.parse('foobarabcabc').should == ['foo', 'bar', ['abc', 'abc']]
93     sequence.parse('barabc').should == ['bar', 'abc']
94     lambda { sequence.parse('abc') }.should raise_error(Walrat::ParseError)
95
96     # similar test but with alternation
97     sequence = 'foo' | 'bar' | 'abc'.one_or_more
98     sequence.parse('foobarabc').should == 'foo'
99     sequence.parse('barabc').should == 'bar'
100     sequence.parse('abc').should == 'abc'
101     sequence.parse('abcabc').should == ['abc', 'abc']
102     lambda { sequence.parse('nothing') }.should raise_error(Walrat::ParseError)
103
104     # test with defective sequence (makes no sense to use "optional" with alternation, will always succeed)
105     sequence = 'foo'.optional | 'bar' | 'abc'.one_or_more
106     sequence.parse('foobarabc').should == 'foo'
107     lambda { sequence.parse('nothing') }.should throw_symbol(:ZeroWidthParseSuccess)
108   end
109
110   it 'should be able to chain a "not predicate"' do
111     sequence = 'foo' & 'bar'.not!
112     sequence.parse('foo').should == 'foo' # fails with ['foo'] because that's the way ParserState works...
113     sequence.parse('foo...').should == 'foo' # same
114     lambda { sequence.parse('foobar') }.should raise_error(Walrat::ParseError)
115   end
116
117   it 'an isolated "not predicate" should return a zero-width match' do
118     sequence = 'foo'.not!
119     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
120     lambda { sequence.parse('bar') }.should throw_symbol(:NotPredicateSuccess)
121   end
122
123   it 'two "not predicates" chained together should act like a union' do
124     # this means "not followed by 'foo' and not followed by 'bar'"
125     sequence = 'foo'.not! & 'bar'.not!
126     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
127     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
128     lambda { sequence.parse('abc') }.should throw_symbol(:NotPredicateSuccess)
129   end
130
131   it 'should be able to chain an "and predicate"' do
132     sequence = 'foo' & 'bar'.and?
133     sequence.parse('foobar').should == 'foo' # same problem, returns ['foo']
134     lambda { sequence.parse('foo...') }.should raise_error(Walrat::ParseError)
135     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
136   end
137
138   it 'an isolated "and predicate" should return a zero-width match' do
139     sequence = 'foo'.and?
140     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
141     lambda { sequence.parse('foo') }.should throw_symbol(:AndPredicateSuccess)
142   end
143
144   it 'should be able to follow an "and predicate" with other parslets or combinations' do
145     # this is equivalent to "foo" if followed by "bar", or any three characters
146     sequence = 'foo' & 'bar'.and? | /.../
147     sequence.parse('foobar').should == 'foo' # returns ['foo']
148     sequence.parse('abc').should == 'abc'
149     lambda { sequence.parse('') }.should raise_error(Walrat::ParseError)
150
151     # it makes little sense for the predicate to follows a choice operator so we don't test that
152   end
153
154   it 'should be able to follow a "not predicate" with other parslets or combinations' do
155     # this is equivalent to "foo" followed by any three characters other than "bar"
156     sequence = 'foo' & 'bar'.not! & /.../
157     sequence.parse('fooabc').should == ['foo', 'abc']
158     lambda { sequence.parse('foobar') }.should raise_error(Walrat::ParseError)
159     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
160     lambda { sequence.parse('') }.should raise_error(Walrat::ParseError)
161   end
162
163   it 'should be able to include a "not predicate" when using a repetition operator' do
164     # basic example
165     sequence = ('foo' & 'bar'.not!).one_or_more
166     sequence.parse('foo').should == 'foo'
167     sequence.parse('foofoobar').should == 'foo'
168     sequence.parse('foofoo').should == ['foo', 'foo']
169     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
170     lambda { sequence.parse('foobar') }.should raise_error(Walrat::ParseError)
171
172     # variation: note that greedy matching alters the behaviour
173     sequence = ('foo' & 'bar').one_or_more & 'abc'.not!
174     sequence.parse('foobar').should == ['foo', 'bar']
175     sequence.parse('foobarfoobar').should == [['foo', 'bar'], ['foo', 'bar']]
176     lambda { sequence.parse('foobarabc') }.should raise_error(Walrat::ParseError)
177   end
178
179   it 'should be able to use regular expression shortcuts in conjunction with predicates' do
180     # match "foo" as long as it's not followed by a digit
181     sequence = 'foo' & /\d/.not!
182     sequence.parse('foo').should == 'foo'
183     sequence.parse('foobar').should == 'foo'
184     lambda { sequence.parse('foo1') }.should raise_error(Walrat::ParseError)
185
186     # match "word" characters as long as they're not followed by whitespace
187     sequence = /\w+/ & /\s/.not!
188     sequence.parse('foo').should == 'foo'
189     lambda { sequence.parse('foo ') }.should raise_error(Walrat::ParseError)
190   end
191 end
192
193 describe 'omitting tokens from the output using the "skip" method' do
194   it 'should be able to skip quotation marks delimiting a string' do
195     sequence = '"'.skip & /[^"]+/ & '"'.skip
196     sequence.parse('"hello world"').should == 'hello world' # note this is returning a ParserState object
197   end
198
199   it 'should be able to skip within a repetition expression' do
200     sequence = ('foo'.skip & /\d+/).one_or_more
201     sequence.parse('foo1...').should == '1'
202     sequence.parse('foo1foo2...').should == ['1', '2'] # only returns 1
203     sequence.parse('foo1foo2foo3...').should == ['1', '2', '3'] # only returns 1
204   end
205
206   it 'should be able to skip commas separating a list' do
207     # closer to real-world use: a comma-separated list
208     sequence = /\w+/ & (/\s*,\s*/.skip & /\w+/).zero_or_more
209     sequence.parse('a').should == 'a'
210     sequence.parse('a, b').should == ['a', 'b']
211     sequence.parse('a, b, c').should == ['a', ['b', 'c']]
212     sequence.parse('a, b, c, d').should == ['a', ['b', 'c', 'd']]
213
214     # again, using the ">>" operator
215     sequence = /\w+/ >> (/\s*,\s*/.skip & /\w+/).zero_or_more
216     sequence.parse('a').should == 'a'
217     sequence.parse('a, b').should == ['a', 'b']
218     sequence.parse('a, b, c').should == ['a', 'b', 'c']
219     sequence.parse('a, b, c, d').should == ['a', 'b', 'c', 'd']
220   end
221 end
222
223 describe 'using the shorthand ">>" pseudo-operator' do
224   it 'should be able to chain the operator multiple times' do
225     # comma-separated words followed by comma-separated digits
226     sequence = /[a-zA-Z]+/ >> (/\s*,\s*/.skip & /[a-zA-Z]+/).zero_or_more >> (/\s*,\s*/.skip & /\d+/).one_or_more
227     sequence.parse('a, 1').should == ['a', '1']
228     sequence.parse('a, b, 1').should == ['a', 'b', '1']
229     sequence.parse('a, 1, 2').should == ['a', '1', '2']
230     sequence.parse('a, b, 1, 2').should == ['a', 'b', '1', '2']
231
232     # same, but enclosed in quotes
233     sequence = '"'.skip & /[a-zA-Z]+/ >> (/\s*,\s*/.skip & /[a-zA-Z]+/).zero_or_more >> (/\s*,\s*/.skip & /\d+/).one_or_more & '"'.skip
234     sequence.parse('"a, 1"').should == ['a', '1']
235     sequence.parse('"a, b, 1"').should == ['a', 'b', '1']
236     sequence.parse('"a, 1, 2"').should == ['a', '1', '2']
237     sequence.parse('"a, b, 1, 2"').should == ['a', 'b', '1', '2']
238
239     # alternative construction of same
240     sequence = /[a-zA-Z]+/ >> (/\s*,\s*/.skip & /[a-zA-Z]+/).zero_or_more & /\s*,\s*/.skip & /\d+/ >> (/\s*,\s*/.skip & /\d+/).zero_or_more
241     sequence.parse('a, 1').should == ['a', '1']
242     sequence.parse('a, b, 1').should == ['a', 'b', '1']
243     sequence.parse('a, 1, 2').should == ['a', '1', '2']
244     sequence.parse('a, b, 1, 2').should == ['a', 'b', '1', '2']
245   end
246 end