]> git.wincent.com - walrat.git/blob - spec/walrat/parslet_combining_spec.rb
Initial import (extraction from Walrus repo, commit 0c9d44c)
[walrat.git] / spec / walrat / parslet_combining_spec.rb
1 # Copyright 2007-2010 Wincent Colaiuta. All rights reserved.
2 # Redistribution and use in source and binary forms, with or without
3 # modification, are permitted provided that the following conditions are met:
4 #
5 # 1. Redistributions of source code must retain the above copyright notice,
6 #    this list of conditions and the following disclaimer.
7 # 2. Redistributions in binary form must reproduce the above copyright notice,
8 #    this list of conditions and the following disclaimer in the documentation
9 #    and/or other materials provided with the distribution.
10 #
11 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
12 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
13 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
14 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
15 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
16 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
17 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
18 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
19 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
20 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
21 # POSSIBILITY OF SUCH DAMAGE.
22
23 require File.expand_path('../spec_helper', File.dirname(__FILE__))
24
25 describe 'using shorthand operators to combine String, Symbol and Regexp parsers' do
26   it 'should be able to chain a String and a Regexp together' do
27     # try in one order
28     sequence = 'foo' & /\d+/
29     sequence.parse('foo1000').should == ['foo', '1000']
30     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError) # first part alone is not enough
31     lambda { sequence.parse('1000') }.should raise_error(Walrat::ParseError) # neither is second part alone
32     lambda { sequence.parse('1000foo') }.should raise_error(Walrat::ParseError) # order matters
33
34     # same test but in reverse order
35     sequence = /\d+/ & 'foo'
36     sequence.parse('1000foo').should == ['1000', 'foo']
37     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError) # first part alone is not enough
38     lambda { sequence.parse('1000') }.should raise_error(Walrat::ParseError) # neither is second part alone
39     lambda { sequence.parse('foo1000') }.should raise_error(Walrat::ParseError) # order matters
40   end
41
42   it 'should be able to choose between a String and a Regexp' do
43     # try in one order
44     sequence = 'foo' | /\d+/
45     sequence.parse('foo').should == 'foo'
46     sequence.parse('100').should == '100'
47     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
48
49     # same test but in reverse order
50     sequence = /\d+/ | 'foo'
51     sequence.parse('foo').should == 'foo'
52     sequence.parse('100').should == '100'
53     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
54   end
55
56   it 'should be able to freely intermix String and Regexp objects when chaining and choosing' do
57     sequence = 'foo' & /\d+/ | 'bar' & /[XYZ]{3}/
58     sequence.parse('foo123').should == ['foo', '123']
59     sequence.parse('barZYX').should == ['bar', 'ZYX']
60     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
61     lambda { sequence.parse('123') }.should raise_error(Walrat::ParseError)
62     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
63     lambda { sequence.parse('XYZ') }.should raise_error(Walrat::ParseError)
64     lambda { sequence.parse('barXY') }.should raise_error(Walrat::ParseError)
65   end
66
67   it 'should be able to specify minimum and maximum repetition using shorthand methods' do
68     # optional (same as "?" in regular expressions)
69     sequence = 'foo'.optional
70     sequence.parse('foo').should == 'foo'
71     lambda { sequence.parse('bar') }.should throw_symbol(:ZeroWidthParseSuccess)
72
73     # zero_or_one (same as optional; "?" in regular expressions)
74     sequence = 'foo'.zero_or_one
75     sequence.parse('foo').should == 'foo'
76     lambda { sequence.parse('bar') }.should throw_symbol(:ZeroWidthParseSuccess)
77
78     # zero_or_more (same as "*" in regular expressions)
79     sequence = 'foo'.zero_or_more
80     sequence.parse('foo').should == 'foo'
81     sequence.parse('foofoofoobar').should == ['foo', 'foo', 'foo']
82     lambda { sequence.parse('bar') }.should throw_symbol(:ZeroWidthParseSuccess)
83
84     # one_or_more (same as "+" in regular expressions)
85     sequence = 'foo'.one_or_more
86     sequence.parse('foo').should == 'foo'
87     sequence.parse('foofoofoobar').should == ['foo', 'foo', 'foo']
88     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
89
90     # repeat (arbitary limits for min, max; same as {min, max} in regular expressions)
91     sequence = 'foo'.repeat(3, 5)
92     sequence.parse('foofoofoobar').should == ['foo', 'foo', 'foo']
93     sequence.parse('foofoofoofoobar').should == ['foo', 'foo', 'foo', 'foo']
94     sequence.parse('foofoofoofoofoobar').should == ['foo', 'foo', 'foo', 'foo', 'foo']
95     sequence.parse('foofoofoofoofoofoobar').should == ['foo', 'foo', 'foo', 'foo', 'foo']
96     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
97     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
98     lambda { sequence.parse('foofoo') }.should raise_error(Walrat::ParseError)
99   end
100
101   it 'should be able to apply repetitions to other combinations wrapped in parentheses' do
102     sequence = ('foo' & 'bar').one_or_more
103     sequence.parse('foobar').should == ['foo', 'bar']
104     sequence.parse('foobarfoobar').should == [['foo', 'bar'], ['foo', 'bar']] # fails: just returns ['foo', 'bar']
105   end
106
107   it 'should be able to combine use of repetition shorthand methods with other shorthand methods' do
108     # first we test with chaining
109     sequence = 'foo'.optional & 'bar' & 'abc'.one_or_more
110     sequence.parse('foobarabc').should == ['foo', 'bar', 'abc']
111     sequence.parse('foobarabcabc').should == ['foo', 'bar', ['abc', 'abc']]
112     sequence.parse('barabc').should == ['bar', 'abc']
113     lambda { sequence.parse('abc') }.should raise_error(Walrat::ParseError)
114
115     # similar test but with alternation
116     sequence = 'foo' | 'bar' | 'abc'.one_or_more
117     sequence.parse('foobarabc').should == 'foo'
118     sequence.parse('barabc').should == 'bar'
119     sequence.parse('abc').should == 'abc'
120     sequence.parse('abcabc').should == ['abc', 'abc']
121     lambda { sequence.parse('nothing') }.should raise_error(Walrat::ParseError)
122
123     # test with defective sequence (makes no sense to use "optional" with alternation, will always succeed)
124     sequence = 'foo'.optional | 'bar' | 'abc'.one_or_more
125     sequence.parse('foobarabc').should == 'foo'
126     lambda { sequence.parse('nothing') }.should throw_symbol(:ZeroWidthParseSuccess)
127   end
128
129   it 'should be able to chain a "not predicate"' do
130     sequence = 'foo' & 'bar'.not!
131     sequence.parse('foo').should == 'foo' # fails with ['foo'] because that's the way ParserState works...
132     sequence.parse('foo...').should == 'foo' # same
133     lambda { sequence.parse('foobar') }.should raise_error(Walrat::ParseError)
134   end
135
136   it 'an isolated "not predicate" should return a zero-width match' do
137     sequence = 'foo'.not!
138     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
139     lambda { sequence.parse('bar') }.should throw_symbol(:NotPredicateSuccess)
140   end
141
142   it 'two "not predicates" chained together should act like a union' do
143     # this means "not followed by 'foo' and not followed by 'bar'"
144     sequence = 'foo'.not! & 'bar'.not!
145     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
146     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
147     lambda { sequence.parse('abc') }.should throw_symbol(:NotPredicateSuccess)
148   end
149
150   it 'should be able to chain an "and predicate"' do
151     sequence = 'foo' & 'bar'.and?
152     sequence.parse('foobar').should == 'foo' # same problem, returns ['foo']
153     lambda { sequence.parse('foo...') }.should raise_error(Walrat::ParseError)
154     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
155   end
156
157   it 'an isolated "and predicate" should return a zero-width match' do
158     sequence = 'foo'.and?
159     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
160     lambda { sequence.parse('foo') }.should throw_symbol(:AndPredicateSuccess)
161   end
162
163   it 'should be able to follow an "and predicate" with other parslets or combinations' do
164     # this is equivalent to "foo" if followed by "bar", or any three characters
165     sequence = 'foo' & 'bar'.and? | /.../
166     sequence.parse('foobar').should == 'foo' # returns ['foo']
167     sequence.parse('abc').should == 'abc'
168     lambda { sequence.parse('') }.should raise_error(Walrat::ParseError)
169
170     # it makes little sense for the predicate to follows a choice operator so we don't test that
171   end
172
173   it 'should be able to follow a "not predicate" with other parslets or combinations' do
174     # this is equivalent to "foo" followed by any three characters other than "bar"
175     sequence = 'foo' & 'bar'.not! & /.../
176     sequence.parse('fooabc').should == ['foo', 'abc']
177     lambda { sequence.parse('foobar') }.should raise_error(Walrat::ParseError)
178     lambda { sequence.parse('foo') }.should raise_error(Walrat::ParseError)
179     lambda { sequence.parse('') }.should raise_error(Walrat::ParseError)
180   end
181
182   it 'should be able to include a "not predicate" when using a repetition operator' do
183     # basic example
184     sequence = ('foo' & 'bar'.not!).one_or_more
185     sequence.parse('foo').should == 'foo'
186     sequence.parse('foofoobar').should == 'foo'
187     sequence.parse('foofoo').should == ['foo', 'foo']
188     lambda { sequence.parse('bar') }.should raise_error(Walrat::ParseError)
189     lambda { sequence.parse('foobar') }.should raise_error(Walrat::ParseError)
190
191     # variation: note that greedy matching alters the behaviour
192     sequence = ('foo' & 'bar').one_or_more & 'abc'.not!
193     sequence.parse('foobar').should == ['foo', 'bar']
194     sequence.parse('foobarfoobar').should == [['foo', 'bar'], ['foo', 'bar']]
195     lambda { sequence.parse('foobarabc') }.should raise_error(Walrat::ParseError)
196   end
197
198   it 'should be able to use regular expression shortcuts in conjunction with predicates' do
199     # match "foo" as long as it's not followed by a digit
200     sequence = 'foo' & /\d/.not!
201     sequence.parse('foo').should == 'foo'
202     sequence.parse('foobar').should == 'foo'
203     lambda { sequence.parse('foo1') }.should raise_error(Walrat::ParseError)
204
205     # match "word" characters as long as they're not followed by whitespace
206     sequence = /\w+/ & /\s/.not!
207     sequence.parse('foo').should == 'foo'
208     lambda { sequence.parse('foo ') }.should raise_error(Walrat::ParseError)
209   end
210 end
211
212 describe 'omitting tokens from the output using the "skip" method' do
213   it 'should be able to skip quotation marks delimiting a string' do
214     sequence = '"'.skip & /[^"]+/ & '"'.skip
215     sequence.parse('"hello world"').should == 'hello world' # note this is returning a ParserState object
216   end
217
218   it 'should be able to skip within a repetition expression' do
219     sequence = ('foo'.skip & /\d+/).one_or_more
220     sequence.parse('foo1...').should == '1'
221     sequence.parse('foo1foo2...').should == ['1', '2'] # only returns 1
222     sequence.parse('foo1foo2foo3...').should == ['1', '2', '3'] # only returns 1
223   end
224
225   it 'should be able to skip commas separating a list' do
226     # closer to real-world use: a comma-separated list
227     sequence = /\w+/ & (/\s*,\s*/.skip & /\w+/).zero_or_more
228     sequence.parse('a').should == 'a'
229     sequence.parse('a, b').should == ['a', 'b']
230     sequence.parse('a, b, c').should == ['a', ['b', 'c']]
231     sequence.parse('a, b, c, d').should == ['a', ['b', 'c', 'd']]
232
233     # again, using the ">>" operator
234     sequence = /\w+/ >> (/\s*,\s*/.skip & /\w+/).zero_or_more
235     sequence.parse('a').should == 'a'
236     sequence.parse('a, b').should == ['a', 'b']
237     sequence.parse('a, b, c').should == ['a', 'b', 'c']
238     sequence.parse('a, b, c, d').should == ['a', 'b', 'c', 'd']
239   end
240 end
241
242 describe 'using the shorthand ">>" pseudo-operator' do
243   it 'should be able to chain the operator multiple times' do
244     # comma-separated words followed by comma-separated digits
245     sequence = /[a-zA-Z]+/ >> (/\s*,\s*/.skip & /[a-zA-Z]+/).zero_or_more >> (/\s*,\s*/.skip & /\d+/).one_or_more
246     sequence.parse('a, 1').should == ['a', '1']
247     sequence.parse('a, b, 1').should == ['a', 'b', '1']
248     sequence.parse('a, 1, 2').should == ['a', '1', '2']
249     sequence.parse('a, b, 1, 2').should == ['a', 'b', '1', '2']
250
251     # same, but enclosed in quotes
252     sequence = '"'.skip & /[a-zA-Z]+/ >> (/\s*,\s*/.skip & /[a-zA-Z]+/).zero_or_more >> (/\s*,\s*/.skip & /\d+/).one_or_more & '"'.skip
253     sequence.parse('"a, 1"').should == ['a', '1']
254     sequence.parse('"a, b, 1"').should == ['a', 'b', '1']
255     sequence.parse('"a, 1, 2"').should == ['a', '1', '2']
256     sequence.parse('"a, b, 1, 2"').should == ['a', 'b', '1', '2']
257
258     # alternative construction of same
259     sequence = /[a-zA-Z]+/ >> (/\s*,\s*/.skip & /[a-zA-Z]+/).zero_or_more & /\s*,\s*/.skip & /\d+/ >> (/\s*,\s*/.skip & /\d+/).zero_or_more
260     sequence.parse('a, 1').should == ['a', '1']
261     sequence.parse('a, b, 1').should == ['a', 'b', '1']
262     sequence.parse('a, 1, 2').should == ['a', '1', '2']
263     sequence.parse('a, b, 1, 2').should == ['a', 'b', '1', '2']
264   end
265 end