mustermann19 0.3.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 (69) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +10 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +22 -0
  7. data/README.md +1081 -0
  8. data/Rakefile +6 -0
  9. data/bench/capturing.rb +57 -0
  10. data/bench/regexp.rb +21 -0
  11. data/bench/simple_vs_sinatra.rb +23 -0
  12. data/bench/template_vs_addressable.rb +26 -0
  13. data/internals.md +64 -0
  14. data/lib/mustermann.rb +61 -0
  15. data/lib/mustermann/ast/compiler.rb +168 -0
  16. data/lib/mustermann/ast/expander.rb +134 -0
  17. data/lib/mustermann/ast/node.rb +160 -0
  18. data/lib/mustermann/ast/parser.rb +137 -0
  19. data/lib/mustermann/ast/pattern.rb +84 -0
  20. data/lib/mustermann/ast/transformer.rb +129 -0
  21. data/lib/mustermann/ast/translator.rb +108 -0
  22. data/lib/mustermann/ast/tree_renderer.rb +29 -0
  23. data/lib/mustermann/ast/validation.rb +43 -0
  24. data/lib/mustermann/caster.rb +117 -0
  25. data/lib/mustermann/equality_map.rb +48 -0
  26. data/lib/mustermann/error.rb +6 -0
  27. data/lib/mustermann/expander.rb +206 -0
  28. data/lib/mustermann/extension.rb +52 -0
  29. data/lib/mustermann/identity.rb +19 -0
  30. data/lib/mustermann/mapper.rb +98 -0
  31. data/lib/mustermann/pattern.rb +182 -0
  32. data/lib/mustermann/rails.rb +17 -0
  33. data/lib/mustermann/regexp_based.rb +30 -0
  34. data/lib/mustermann/regular.rb +26 -0
  35. data/lib/mustermann/router.rb +9 -0
  36. data/lib/mustermann/router/rack.rb +50 -0
  37. data/lib/mustermann/router/simple.rb +144 -0
  38. data/lib/mustermann/shell.rb +29 -0
  39. data/lib/mustermann/simple.rb +38 -0
  40. data/lib/mustermann/simple_match.rb +30 -0
  41. data/lib/mustermann/sinatra.rb +22 -0
  42. data/lib/mustermann/template.rb +48 -0
  43. data/lib/mustermann/to_pattern.rb +45 -0
  44. data/lib/mustermann/version.rb +3 -0
  45. data/mustermann.gemspec +31 -0
  46. data/spec/expander_spec.rb +105 -0
  47. data/spec/extension_spec.rb +296 -0
  48. data/spec/identity_spec.rb +83 -0
  49. data/spec/mapper_spec.rb +83 -0
  50. data/spec/mustermann_spec.rb +65 -0
  51. data/spec/pattern_spec.rb +49 -0
  52. data/spec/rails_spec.rb +522 -0
  53. data/spec/regexp_based_spec.rb +8 -0
  54. data/spec/regular_spec.rb +36 -0
  55. data/spec/router/rack_spec.rb +39 -0
  56. data/spec/router/simple_spec.rb +32 -0
  57. data/spec/shell_spec.rb +109 -0
  58. data/spec/simple_match_spec.rb +10 -0
  59. data/spec/simple_spec.rb +237 -0
  60. data/spec/sinatra_spec.rb +574 -0
  61. data/spec/support.rb +5 -0
  62. data/spec/support/coverage.rb +16 -0
  63. data/spec/support/env.rb +15 -0
  64. data/spec/support/expand_matcher.rb +27 -0
  65. data/spec/support/match_matcher.rb +39 -0
  66. data/spec/support/pattern.rb +39 -0
  67. data/spec/template_spec.rb +815 -0
  68. data/spec/to_pattern_spec.rb +20 -0
  69. metadata +301 -0
@@ -0,0 +1,5 @@
1
+ require 'support/env'
2
+ require 'support/coverage'
3
+ require 'support/expand_matcher'
4
+ require 'support/match_matcher'
5
+ require 'support/pattern'
@@ -0,0 +1,16 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+
4
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
5
+ SimpleCov::Formatter::HTMLFormatter,
6
+ Coveralls::SimpleCov::Formatter
7
+ ]
8
+
9
+ SimpleCov.start do
10
+ project_name 'mustermann'
11
+ minimum_coverage 100
12
+ coverage_dir '.coverage'
13
+
14
+ add_filter "/spec/"
15
+ add_group 'Library', 'lib'
16
+ end
@@ -0,0 +1,15 @@
1
+ #if RUBY_VERSION < '2.0.0'
2
+ # $stderr.puts "needs Ruby 2.0.0, you're running #{RUBY_VERSION}"
3
+ # exit 1
4
+ #end
5
+ require 'rspec'
6
+ require 'rspec/its'
7
+
8
+ ENV['RACK_ENV'] = 'test'
9
+
10
+ RSpec.configure do |config|
11
+ config.filter_run_excluding :skip => true
12
+ config.expect_with :rspec do |c|
13
+ c.syntax = [:should, :expect]
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ RSpec::Matchers.define :expand do |values = {}|
2
+ match do |pattern|
3
+ @string ||= nil
4
+ begin
5
+ expanded = pattern.expand(values)
6
+ rescue Exception
7
+ false
8
+ else
9
+ @string ? @string == expanded : !!expanded
10
+ end
11
+ end
12
+
13
+ chain :to do |string|
14
+ @string = string
15
+ end
16
+
17
+ failure_message do |pattern|
18
+ message = "expected %p to be expandable with %p" % [pattern, values]
19
+ expanded = pattern.expand(values)
20
+ message << " and result in %p, but got %p" % [@string, expanded] if @string
21
+ message
22
+ end
23
+
24
+ failure_message_when_negated do |pattern|
25
+ "expected %p not to be expandable with %p" % [pattern, values]
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ RSpec::Matchers.define :match do |expected|
2
+ match do |actual|
3
+ @captures ||= false
4
+ match = actual.match(expected)
5
+ match &&= @captures.all? { |k, v| match[k] == v } if @captures
6
+ match
7
+ end
8
+
9
+ chain :capturing do |captures|
10
+ @captures = captures
11
+ end
12
+
13
+ failure_message do |actual|
14
+ require 'pp'
15
+ match = actual.match(expected)
16
+ if match
17
+ key, value = @captures.detect { |k, v| match[k] != v }
18
+ message = "expected %p to capture %p as %p when matching %p, but got %p\n\nRegular Expression:\n%p" % [
19
+ actual.to_s, key, value, expected, match[key], actual.regexp
20
+ ]
21
+
22
+ if ast = actual.instance_variable_get(:@ast)
23
+ require 'mustermann/ast/tree_renderer'
24
+ tree = Mustermann::AST::TreeRenderer.render(ast)
25
+ message << "\n\nAST:\n#{tree}"
26
+ end
27
+
28
+ message
29
+ else
30
+ "expected %p to match %p" % [ actual, expected ]
31
+ end
32
+ end
33
+
34
+ failure_message_when_negated do |actual|
35
+ "expected %p not to match %p" % [
36
+ actual.to_s, expected
37
+ ]
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+ require 'timeout'
2
+
3
+ module Support
4
+ module Pattern
5
+ def pattern(pattern, options = nil, &block)
6
+ description = "pattern %p" % pattern
7
+
8
+ if options
9
+ description << " with options %p" % [options]
10
+ instance = subject_for(pattern, options)
11
+ else
12
+ instance = subject_for(pattern)
13
+ end
14
+
15
+ context description do
16
+ subject(:pattern, &instance)
17
+ its(:to_s) { should be == pattern }
18
+ its(:inspect) { should be == "#<#{described_class}:#{pattern.inspect}>" }
19
+ its(:names) { should be_an(Array) }
20
+
21
+ example 'string should be immune to external change' do
22
+ subject.to_s.replace "NOT THE PATTERN"
23
+ subject.to_s.should be == pattern
24
+ end
25
+
26
+ instance_eval(&block)
27
+ end
28
+ end
29
+
30
+ def subject_for(pattern, *args)
31
+ instance = Timeout.timeout(1) { described_class.new(pattern, *args) }
32
+ proc { instance }
33
+ rescue Timeout::Error => error
34
+ proc { raise Timeout::Error, "could not compile #{pattern.inspect} in time", error.backtrace }
35
+ rescue Exception => error
36
+ proc { raise error }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,815 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'support'
3
+ require 'mustermann/template'
4
+
5
+ describe Mustermann::Template do
6
+ extend Support::Pattern
7
+
8
+ pattern '' do
9
+ it { should match('') }
10
+ it { should_not match('/') }
11
+ end
12
+
13
+ pattern '/' do
14
+ it { should match('/') }
15
+ it { should_not match('/foo') }
16
+ end
17
+
18
+ pattern '/foo' do
19
+ it { should match('/foo') }
20
+ it { should_not match('/bar') }
21
+ it { should_not match('/foo.bar') }
22
+ end
23
+
24
+ pattern '/foo/bar' do
25
+ it { should match('/foo/bar') }
26
+ it { should_not match('/foo%2Fbar') }
27
+ it { should_not match('/foo%2fbar') }
28
+ end
29
+
30
+ pattern '/:foo' do
31
+ it { should match('/:foo') }
32
+ it { should match('/%3Afoo') }
33
+ it { should_not match('/foo') }
34
+ it { should_not match('/foo?') }
35
+ it { should_not match('/foo/bar') }
36
+ it { should_not match('/') }
37
+ it { should_not match('/foo/') }
38
+ end
39
+
40
+ pattern '/föö' do
41
+ it { should match("/f%C3%B6%C3%B6") }
42
+ end
43
+
44
+ pattern '/test$/' do
45
+ it { should match('/test$/') }
46
+ end
47
+
48
+ pattern '/te+st/' do
49
+ it { should match('/te+st/') }
50
+ it { should_not match('/test/') }
51
+ it { should_not match('/teest/') }
52
+ end
53
+
54
+ pattern "/path with spaces" do
55
+ it { should match('/path%20with%20spaces') }
56
+ it { should match('/path%2Bwith%2Bspaces') }
57
+ it { should match('/path+with+spaces') }
58
+ end
59
+
60
+ pattern '/foo&bar' do
61
+ it { should match('/foo&bar') }
62
+ end
63
+
64
+ pattern '/test.bar' do
65
+ it { should match('/test.bar') }
66
+ it { should_not match('/test0bar') }
67
+ end
68
+
69
+ pattern "/path with spaces", space_matches_plus: false do
70
+ it { should match('/path%20with%20spaces') }
71
+ it { should_not match('/path%2Bwith%2Bspaces') }
72
+ it { should_not match('/path+with+spaces') }
73
+ end
74
+
75
+ pattern "/path with spaces", uri_decode: false do
76
+ it { should_not match('/path%20with%20spaces') }
77
+ it { should_not match('/path%2Bwith%2Bspaces') }
78
+ it { should_not match('/path+with+spaces') }
79
+ end
80
+
81
+ context 'level 1' do
82
+ context 'without operator' do
83
+ pattern '/hello/{person}' do
84
+ it { should match('/hello/Frank').capturing person: 'Frank' }
85
+ it { should match('/hello/a_b~c').capturing person: 'a_b~c' }
86
+ it { should match('/hello/a.%20').capturing person: 'a.%20' }
87
+
88
+ it { should_not match('/hello/:') }
89
+ it { should_not match('/hello//') }
90
+ it { should_not match('/hello/?') }
91
+ it { should_not match('/hello/#') }
92
+ it { should_not match('/hello/[') }
93
+ it { should_not match('/hello/]') }
94
+ it { should_not match('/hello/@') }
95
+ it { should_not match('/hello/!') }
96
+ it { should_not match('/hello/*') }
97
+ it { should_not match('/hello/+') }
98
+ it { should_not match('/hello/,') }
99
+ it { should_not match('/hello/;') }
100
+ it { should_not match('/hello/=') }
101
+
102
+ example { pattern.params('/hello/Frank').should be == {'person' => 'Frank'} }
103
+ end
104
+
105
+ pattern "/{foo}/{bar}" do
106
+ it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' }
107
+ it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' }
108
+ it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' }
109
+ it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' }
110
+
111
+ it { should_not match('/foo%2Fbar') }
112
+ it { should_not match('/foo%2fbar') }
113
+ end
114
+ end
115
+ end
116
+
117
+ context 'level 2' do
118
+ context 'operator +' do
119
+ pattern '/hello/{+person}' do
120
+ it { should match('/hello/Frank') .capturing person: 'Frank' }
121
+ it { should match('/hello/a_b~c') .capturing person: 'a_b~c' }
122
+ it { should match('/hello/a.%20') .capturing person: 'a.%20' }
123
+ it { should match('/hello/a/%20') .capturing person: 'a/%20' }
124
+ it { should match('/hello/:') .capturing person: ?: }
125
+ it { should match('/hello//') .capturing person: ?/ }
126
+ it { should match('/hello/?') .capturing person: ?? }
127
+ it { should match('/hello/#') .capturing person: ?# }
128
+ it { should match('/hello/[') .capturing person: ?[ }
129
+ it { should match('/hello/]') .capturing person: ?] }
130
+ it { should match('/hello/@') .capturing person: ?@ }
131
+ it { should match('/hello/!') .capturing person: ?! }
132
+ it { should match('/hello/*') .capturing person: ?* }
133
+ it { should match('/hello/+') .capturing person: ?+ }
134
+ it { should match('/hello/,') .capturing person: ?, }
135
+ it { should match('/hello/;') .capturing person: ?; }
136
+ it { should match('/hello/=') .capturing person: ?= }
137
+ end
138
+
139
+ pattern "/{+foo}/{bar}" do
140
+ it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' }
141
+ it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' }
142
+ it { should match('/foo/bar/bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' }
143
+ it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' }
144
+ it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' }
145
+
146
+ it { should_not match('/foo%2Fbar') }
147
+ it { should_not match('/foo%2fbar') }
148
+ end
149
+ end
150
+
151
+ context 'operator #' do
152
+ pattern '/hello/{#person}' do
153
+ it { should match('/hello/#Frank') .capturing person: 'Frank' }
154
+ it { should match('/hello/#a_b~c') .capturing person: 'a_b~c' }
155
+ it { should match('/hello/#a.%20') .capturing person: 'a.%20' }
156
+ it { should match('/hello/#a/%20') .capturing person: 'a/%20' }
157
+ it { should match('/hello/#:') .capturing person: ?: }
158
+ it { should match('/hello/#/') .capturing person: ?/ }
159
+ it { should match('/hello/#?') .capturing person: ?? }
160
+ it { should match('/hello/##') .capturing person: ?# }
161
+ it { should match('/hello/#[') .capturing person: ?[ }
162
+ it { should match('/hello/#]') .capturing person: ?] }
163
+ it { should match('/hello/#@') .capturing person: ?@ }
164
+ it { should match('/hello/#!') .capturing person: ?! }
165
+ it { should match('/hello/#*') .capturing person: ?* }
166
+ it { should match('/hello/#+') .capturing person: ?+ }
167
+ it { should match('/hello/#,') .capturing person: ?, }
168
+ it { should match('/hello/#;') .capturing person: ?; }
169
+ it { should match('/hello/#=') .capturing person: ?= }
170
+
171
+
172
+ it { should_not match('/hello/Frank') }
173
+ it { should_not match('/hello/a_b~c') }
174
+ it { should_not match('/hello/a.%20') }
175
+
176
+ it { should_not match('/hello/:') }
177
+ it { should_not match('/hello//') }
178
+ it { should_not match('/hello/?') }
179
+ it { should_not match('/hello/#') }
180
+ it { should_not match('/hello/[') }
181
+ it { should_not match('/hello/]') }
182
+ it { should_not match('/hello/@') }
183
+ it { should_not match('/hello/!') }
184
+ it { should_not match('/hello/*') }
185
+ it { should_not match('/hello/+') }
186
+ it { should_not match('/hello/,') }
187
+ it { should_not match('/hello/;') }
188
+ it { should_not match('/hello/=') }
189
+
190
+
191
+ example { pattern.params('/hello/#Frank').should be == {'person' => 'Frank'} }
192
+ end
193
+
194
+ pattern "/{+foo}/{#bar}" do
195
+ it { should match('/foo/#bar') .capturing foo: 'foo', bar: 'bar' }
196
+ it { should match('/foo.bar/#bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' }
197
+ it { should match('/foo/bar/#bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' }
198
+ it { should match('/10.1/#te.st') .capturing foo: '10.1', bar: 'te.st' }
199
+ it { should match('/10.1.2/#te.st') .capturing foo: '10.1.2', bar: 'te.st' }
200
+
201
+ it { should_not match('/foo%2F#bar') }
202
+ it { should_not match('/foo%2f#bar') }
203
+
204
+ example { pattern.params('/hello/#Frank').should be == {'foo' => 'hello', 'bar' => 'Frank'} }
205
+ end
206
+ end
207
+ end
208
+
209
+ context 'level 3' do
210
+ context 'without operator' do
211
+ pattern "{a,b,c}" do
212
+ it { should match("~x,42,_").capturing a: '~x', b: '42', c: '_' }
213
+ it { should_not match("~x,42") }
214
+ it { should_not match("~x/42") }
215
+ it { should_not match("~x#42") }
216
+ it { should_not match("~x,42,_#42") }
217
+
218
+ example { pattern.params('d,f,g').should be == {'a' => 'd', 'b' => 'f', 'c' => 'g'} }
219
+ end
220
+ end
221
+
222
+ context 'operator +' do
223
+ pattern "{+a,b,c}" do
224
+ it { should match("~x,42,_") .capturing a: '~x', b: '42', c: '_' }
225
+ it { should match("~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' }
226
+ it { should match("~/x,42,_/42") .capturing a: '~/x', b: '42', c: '_/42' }
227
+
228
+ it { should_not match("~x,42") }
229
+ it { should_not match("~x/42") }
230
+ it { should_not match("~x#42") }
231
+ end
232
+ end
233
+
234
+ context 'operator #' do
235
+ pattern "{#a,b,c}" do
236
+ it { should match("#~x,42,_") .capturing a: '~x', b: '42', c: '_' }
237
+ it { should match("#~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' }
238
+ it { should match("#~/x,42,_#42") .capturing a: '~/x', b: '42', c: '_#42' }
239
+
240
+ it { should_not match("~x,42,_") }
241
+ it { should_not match("~x,42,_#42") }
242
+ it { should_not match("~/x,42,_#42") }
243
+
244
+ it { should_not match("~x,42") }
245
+ it { should_not match("~x/42") }
246
+ it { should_not match("~x#42") }
247
+ end
248
+ end
249
+
250
+ context 'operator .' do
251
+ pattern '/hello/{.person}' do
252
+ it { should match('/hello/.Frank') .capturing person: 'Frank' }
253
+ it { should match('/hello/.a_b~c') .capturing person: 'a_b~c' }
254
+
255
+ it { should_not match('/hello/.:') }
256
+ it { should_not match('/hello/./') }
257
+ it { should_not match('/hello/.?') }
258
+ it { should_not match('/hello/.#') }
259
+ it { should_not match('/hello/.[') }
260
+ it { should_not match('/hello/.]') }
261
+ it { should_not match('/hello/.@') }
262
+ it { should_not match('/hello/.!') }
263
+ it { should_not match('/hello/.*') }
264
+ it { should_not match('/hello/.+') }
265
+ it { should_not match('/hello/.,') }
266
+ it { should_not match('/hello/.;') }
267
+ it { should_not match('/hello/.=') }
268
+
269
+ it { should_not match('/hello/Frank') }
270
+ it { should_not match('/hello/a_b~c') }
271
+ it { should_not match('/hello/a.%20') }
272
+
273
+ it { should_not match('/hello/:') }
274
+ it { should_not match('/hello//') }
275
+ it { should_not match('/hello/?') }
276
+ it { should_not match('/hello/#') }
277
+ it { should_not match('/hello/[') }
278
+ it { should_not match('/hello/]') }
279
+ it { should_not match('/hello/@') }
280
+ it { should_not match('/hello/!') }
281
+ it { should_not match('/hello/*') }
282
+ it { should_not match('/hello/+') }
283
+ it { should_not match('/hello/,') }
284
+ it { should_not match('/hello/;') }
285
+ it { should_not match('/hello/=') }
286
+ end
287
+
288
+ pattern "{.a,b,c}" do
289
+ it { should match(".~x.42._").capturing a: '~x', b: '42', c: '_' }
290
+ it { should_not match(".~x,42") }
291
+ it { should_not match(".~x/42") }
292
+ it { should_not match(".~x#42") }
293
+ it { should_not match(".~x,42,_") }
294
+ it { should_not match("~x.42._") }
295
+ end
296
+ end
297
+
298
+ context 'operator /' do
299
+ pattern '/hello{/person}' do
300
+ it { should match('/hello/Frank') .capturing person: 'Frank' }
301
+ it { should match('/hello/a_b~c') .capturing person: 'a_b~c' }
302
+
303
+ it { should_not match('/hello//:') }
304
+ it { should_not match('/hello///') }
305
+ it { should_not match('/hello//?') }
306
+ it { should_not match('/hello//#') }
307
+ it { should_not match('/hello//[') }
308
+ it { should_not match('/hello//]') }
309
+ it { should_not match('/hello//@') }
310
+ it { should_not match('/hello//!') }
311
+ it { should_not match('/hello//*') }
312
+ it { should_not match('/hello//+') }
313
+ it { should_not match('/hello//,') }
314
+ it { should_not match('/hello//;') }
315
+ it { should_not match('/hello//=') }
316
+
317
+ it { should_not match('/hello/:') }
318
+ it { should_not match('/hello//') }
319
+ it { should_not match('/hello/?') }
320
+ it { should_not match('/hello/#') }
321
+ it { should_not match('/hello/[') }
322
+ it { should_not match('/hello/]') }
323
+ it { should_not match('/hello/@') }
324
+ it { should_not match('/hello/!') }
325
+ it { should_not match('/hello/*') }
326
+ it { should_not match('/hello/+') }
327
+ it { should_not match('/hello/,') }
328
+ it { should_not match('/hello/;') }
329
+ it { should_not match('/hello/=') }
330
+ end
331
+
332
+ pattern "{/a,b,c}" do
333
+ it { should match("/~x/42/_").capturing a: '~x', b: '42', c: '_' }
334
+ it { should_not match("/~x,42") }
335
+ it { should_not match("/~x.42") }
336
+ it { should_not match("/~x#42") }
337
+ it { should_not match("/~x,42,_") }
338
+ it { should_not match("~x/42/_") }
339
+ end
340
+ end
341
+
342
+ context 'operator ;' do
343
+ pattern '/hello/{;person}' do
344
+ it { should match('/hello/;person=Frank') .capturing person: 'Frank' }
345
+ it { should match('/hello/;person=a_b~c') .capturing person: 'a_b~c' }
346
+ it { should match('/hello/;person') .capturing person: nil }
347
+
348
+ it { should_not match('/hello/;persona=Frank') }
349
+ it { should_not match('/hello/;persona=a_b~c') }
350
+
351
+ it { should_not match('/hello/;person=:') }
352
+ it { should_not match('/hello/;person=/') }
353
+ it { should_not match('/hello/;person=?') }
354
+ it { should_not match('/hello/;person=#') }
355
+ it { should_not match('/hello/;person=[') }
356
+ it { should_not match('/hello/;person=]') }
357
+ it { should_not match('/hello/;person=@') }
358
+ it { should_not match('/hello/;person=!') }
359
+ it { should_not match('/hello/;person=*') }
360
+ it { should_not match('/hello/;person=+') }
361
+ it { should_not match('/hello/;person=,') }
362
+ it { should_not match('/hello/;person=;') }
363
+ it { should_not match('/hello/;person==') }
364
+
365
+ it { should_not match('/hello/;Frank') }
366
+ it { should_not match('/hello/;a_b~c') }
367
+ it { should_not match('/hello/;a.%20') }
368
+
369
+ it { should_not match('/hello/:') }
370
+ it { should_not match('/hello//') }
371
+ it { should_not match('/hello/?') }
372
+ it { should_not match('/hello/#') }
373
+ it { should_not match('/hello/[') }
374
+ it { should_not match('/hello/]') }
375
+ it { should_not match('/hello/@') }
376
+ it { should_not match('/hello/!') }
377
+ it { should_not match('/hello/*') }
378
+ it { should_not match('/hello/+') }
379
+ it { should_not match('/hello/,') }
380
+ it { should_not match('/hello/;') }
381
+ it { should_not match('/hello/=') }
382
+ end
383
+
384
+ pattern "{;a,b,c}" do
385
+ it { should match(";a=~x;b=42;c=_") .capturing a: '~x', b: '42', c: '_' }
386
+ it { should match(";a=~x;b;c=_") .capturing a: '~x', b: nil, c: '_' }
387
+
388
+ it { should_not match(";a=~x;c=_;b=42").capturing a: '~x', b: '42', c: '_' }
389
+
390
+ it { should_not match(";a=~x;b=42") }
391
+ it { should_not match("a=~x;b=42") }
392
+ it { should_not match(";a=~x;b=#42;c") }
393
+ it { should_not match(";a=~x,b=42,c=_") }
394
+ it { should_not match("~x;b=42;c=_") }
395
+ end
396
+ end
397
+
398
+ context 'operator ?' do
399
+ pattern '/hello/{?person}' do
400
+ it { should match('/hello/?person=Frank') .capturing person: 'Frank' }
401
+ it { should match('/hello/?person=a_b~c') .capturing person: 'a_b~c' }
402
+ it { should match('/hello/?person') .capturing person: nil }
403
+
404
+ it { should_not match('/hello/?persona=Frank') }
405
+ it { should_not match('/hello/?persona=a_b~c') }
406
+
407
+ it { should_not match('/hello/?person=:') }
408
+ it { should_not match('/hello/?person=/') }
409
+ it { should_not match('/hello/?person=?') }
410
+ it { should_not match('/hello/?person=#') }
411
+ it { should_not match('/hello/?person=[') }
412
+ it { should_not match('/hello/?person=]') }
413
+ it { should_not match('/hello/?person=@') }
414
+ it { should_not match('/hello/?person=!') }
415
+ it { should_not match('/hello/?person=*') }
416
+ it { should_not match('/hello/?person=+') }
417
+ it { should_not match('/hello/?person=,') }
418
+ it { should_not match('/hello/?person=;') }
419
+ it { should_not match('/hello/?person==') }
420
+
421
+ it { should_not match('/hello/?Frank') }
422
+ it { should_not match('/hello/?a_b~c') }
423
+ it { should_not match('/hello/?a.%20') }
424
+
425
+ it { should_not match('/hello/:') }
426
+ it { should_not match('/hello//') }
427
+ it { should_not match('/hello/?') }
428
+ it { should_not match('/hello/#') }
429
+ it { should_not match('/hello/[') }
430
+ it { should_not match('/hello/]') }
431
+ it { should_not match('/hello/@') }
432
+ it { should_not match('/hello/!') }
433
+ it { should_not match('/hello/*') }
434
+ it { should_not match('/hello/+') }
435
+ it { should_not match('/hello/,') }
436
+ it { should_not match('/hello/;') }
437
+ it { should_not match('/hello/=') }
438
+ end
439
+
440
+ pattern "{?a,b,c}" do
441
+ it { should match("?a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' }
442
+ it { should match("?a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' }
443
+
444
+ it { should_not match("?a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' }
445
+
446
+ it { should_not match("?a=~x&b=42") }
447
+ it { should_not match("a=~x&b=42") }
448
+ it { should_not match("?a=~x&b=#42&c") }
449
+ it { should_not match("?a=~x,b=42,c=_") }
450
+ it { should_not match("~x&b=42&c=_") }
451
+ end
452
+ end
453
+
454
+ context 'operator &' do
455
+ pattern '/hello/{&person}' do
456
+ it { should match('/hello/&person=Frank') .capturing person: 'Frank' }
457
+ it { should match('/hello/&person=a_b~c') .capturing person: 'a_b~c' }
458
+ it { should match('/hello/&person') .capturing person: nil }
459
+
460
+ it { should_not match('/hello/&persona=Frank') }
461
+ it { should_not match('/hello/&persona=a_b~c') }
462
+
463
+ it { should_not match('/hello/&person=:') }
464
+ it { should_not match('/hello/&person=/') }
465
+ it { should_not match('/hello/&person=?') }
466
+ it { should_not match('/hello/&person=#') }
467
+ it { should_not match('/hello/&person=[') }
468
+ it { should_not match('/hello/&person=]') }
469
+ it { should_not match('/hello/&person=@') }
470
+ it { should_not match('/hello/&person=!') }
471
+ it { should_not match('/hello/&person=*') }
472
+ it { should_not match('/hello/&person=+') }
473
+ it { should_not match('/hello/&person=,') }
474
+ it { should_not match('/hello/&person=;') }
475
+ it { should_not match('/hello/&person==') }
476
+
477
+ it { should_not match('/hello/&Frank') }
478
+ it { should_not match('/hello/&a_b~c') }
479
+ it { should_not match('/hello/&a.%20') }
480
+
481
+ it { should_not match('/hello/:') }
482
+ it { should_not match('/hello//') }
483
+ it { should_not match('/hello/?') }
484
+ it { should_not match('/hello/#') }
485
+ it { should_not match('/hello/[') }
486
+ it { should_not match('/hello/]') }
487
+ it { should_not match('/hello/@') }
488
+ it { should_not match('/hello/!') }
489
+ it { should_not match('/hello/*') }
490
+ it { should_not match('/hello/+') }
491
+ it { should_not match('/hello/,') }
492
+ it { should_not match('/hello/;') }
493
+ it { should_not match('/hello/=') }
494
+ end
495
+
496
+ pattern "{&a,b,c}" do
497
+ it { should match("&a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' }
498
+ it { should match("&a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' }
499
+
500
+ it { should_not match("&a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' }
501
+
502
+ it { should_not match("&a=~x&b=42") }
503
+ it { should_not match("a=~x&b=42") }
504
+ it { should_not match("&a=~x&b=#42&c") }
505
+ it { should_not match("&a=~x,b=42,c=_") }
506
+ it { should_not match("~x&b=42&c=_") }
507
+ end
508
+ end
509
+ end
510
+
511
+ context 'level 4' do
512
+ context 'without operator' do
513
+ context 'prefix' do
514
+ pattern '{a:3}/bar' do
515
+ it { should match('foo/bar') .capturing a: 'foo' }
516
+ it { should match('fo/bar') .capturing a: 'fo' }
517
+ it { should match('f/bar') .capturing a: 'f' }
518
+ it { should_not match('fooo/bar') }
519
+ end
520
+
521
+ pattern '{a:3}{b}' do
522
+ it { should match('foobar') .capturing a: 'foo', b: 'bar' }
523
+ end
524
+ end
525
+
526
+ context 'expand' do
527
+ pattern '{a*}' do
528
+ it { should match('a') .capturing a: 'a' }
529
+ it { should match('a,b') .capturing a: 'a,b' }
530
+ it { should match('a,b,c') .capturing a: 'a,b,c' }
531
+ it { should_not match('a,b/c') }
532
+ it { should_not match('a,') }
533
+
534
+ example { pattern.params('a').should be == { 'a' => ['a'] }}
535
+ example { pattern.params('a,b').should be == { 'a' => ['a', 'b'] }}
536
+ end
537
+
538
+ pattern '{a*},{b}' do
539
+ it { should match('a,b') .capturing a: 'a', b: 'b' }
540
+ it { should match('a,b,c') .capturing a: 'a,b', b: 'c' }
541
+ it { should_not match('a,b/c') }
542
+ it { should_not match('a,') }
543
+
544
+ example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
545
+ example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
546
+ end
547
+
548
+ pattern '{a*,b}' do
549
+ it { should match('a,b') .capturing a: 'a', b: 'b' }
550
+ it { should match('a,b,c') .capturing a: 'a,b', b: 'c' }
551
+ it { should_not match('a,b/c') }
552
+ it { should_not match('a,') }
553
+
554
+ example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
555
+ example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
556
+ end
557
+ end
558
+ end
559
+
560
+ context 'operator +' do
561
+ context 'prefix' do
562
+ pattern '{+a:3}/bar' do
563
+ it { should match('foo/bar') .capturing a: 'foo' }
564
+ it { should match('fo/bar') .capturing a: 'fo' }
565
+ it { should match('f/bar') .capturing a: 'f' }
566
+ it { should_not match('fooo/bar') }
567
+ end
568
+
569
+ pattern '{+a:3}{b}' do
570
+ it { should match('foobar') .capturing a: 'foo', b: 'bar' }
571
+ end
572
+ end
573
+
574
+ context 'expand' do
575
+ pattern '{+a*}' do
576
+ it { should match('a') .capturing a: 'a' }
577
+ it { should match('a,b') .capturing a: 'a,b' }
578
+ it { should match('a,b,c') .capturing a: 'a,b,c' }
579
+ it { should match('a,b/c') .capturing a: 'a,b/c' }
580
+ end
581
+
582
+ pattern '{+a*},{b}' do
583
+ it { should match('a,b') .capturing a: 'a', b: 'b' }
584
+ it { should match('a,b,c') .capturing a: 'a,b', b: 'c' }
585
+ it { should_not match('a,b/c') }
586
+ it { should_not match('a,') }
587
+
588
+ example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
589
+ example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
590
+ end
591
+ end
592
+ end
593
+
594
+ context 'operator #' do
595
+ context 'prefix' do
596
+ pattern '{#a:3}/bar' do
597
+ it { should match('#foo/bar') .capturing a: 'foo' }
598
+ it { should match('#fo/bar') .capturing a: 'fo' }
599
+ it { should match('#f/bar') .capturing a: 'f' }
600
+ it { should_not match('#fooo/bar') }
601
+ end
602
+
603
+ pattern '{#a:3}{b}' do
604
+ it { should match('#foobar') .capturing a: 'foo', b: 'bar' }
605
+ end
606
+ end
607
+
608
+ context 'expand' do
609
+ pattern '{#a*}' do
610
+ it { should match('#a') .capturing a: 'a' }
611
+ it { should match('#a,b') .capturing a: 'a,b' }
612
+ it { should match('#a,b,c') .capturing a: 'a,b,c' }
613
+ it { should match('#a,b/c') .capturing a: 'a,b/c' }
614
+
615
+ example { pattern.params('#a,b').should be == { 'a' => ['a', 'b'] }}
616
+ example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b', 'c'] }}
617
+ end
618
+
619
+ pattern '{#a*,b}' do
620
+ it { should match('#a,b') .capturing a: 'a', b: 'b' }
621
+ it { should match('#a,b,c') .capturing a: 'a,b', b: 'c' }
622
+ it { should_not match('#a,') }
623
+
624
+ example { pattern.params('#a,b').should be == { 'a' => ['a'], 'b' => 'b' }}
625
+ example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
626
+ end
627
+ end
628
+ end
629
+
630
+ context 'operator .' do
631
+ context 'prefix' do
632
+ pattern '{.a:3}/bar' do
633
+ it { should match('.foo/bar') .capturing a: 'foo' }
634
+ it { should match('.fo/bar') .capturing a: 'fo' }
635
+ it { should match('.f/bar') .capturing a: 'f' }
636
+ it { should_not match('.fooo/bar') }
637
+ end
638
+
639
+ pattern '{.a:3}{b}' do
640
+ it { should match('.foobar') .capturing a: 'foo', b: 'bar' }
641
+ end
642
+ end
643
+
644
+ context 'expand' do
645
+ pattern '{.a*}' do
646
+ it { should match('.a') .capturing a: 'a' }
647
+ it { should match('.a.b') .capturing a: 'a.b' }
648
+ it { should match('.a.b.c') .capturing a: 'a.b.c' }
649
+ it { should_not match('.a.b,c') }
650
+ it { should_not match('.a,') }
651
+ end
652
+
653
+ pattern '{.a*,b}' do
654
+ it { should match('.a.b') .capturing a: 'a', b: 'b' }
655
+ it { should match('.a.b.c') .capturing a: 'a.b', b: 'c' }
656
+ it { should_not match('.a.b/c') }
657
+ it { should_not match('.a.') }
658
+
659
+ example { pattern.params('.a.b').should be == { 'a' => ['a'], 'b' => 'b' }}
660
+ example { pattern.params('.a.b.c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
661
+ end
662
+ end
663
+ end
664
+
665
+ context 'operator /' do
666
+ context 'prefix' do
667
+ pattern '{/a:3}/bar' do
668
+ it { should match('/foo/bar') .capturing a: 'foo' }
669
+ it { should match('/fo/bar') .capturing a: 'fo' }
670
+ it { should match('/f/bar') .capturing a: 'f' }
671
+ it { should_not match('/fooo/bar') }
672
+ end
673
+
674
+ pattern '{/a:3}{b}' do
675
+ it { should match('/foobar') .capturing a: 'foo', b: 'bar' }
676
+ end
677
+ end
678
+
679
+ context 'expand' do
680
+ pattern '{/a*}' do
681
+ it { should match('/a') .capturing a: 'a' }
682
+ it { should match('/a/b') .capturing a: 'a/b' }
683
+ it { should match('/a/b/c') .capturing a: 'a/b/c' }
684
+ it { should_not match('/a/b,c') }
685
+ it { should_not match('/a,') }
686
+ end
687
+
688
+ pattern '{/a*,b}' do
689
+ it { should match('/a/b') .capturing a: 'a', b: 'b' }
690
+ it { should match('/a/b/c') .capturing a: 'a/b', b: 'c' }
691
+ it { should_not match('/a/b,c') }
692
+ it { should_not match('/a/') }
693
+
694
+ example { pattern.params('/a/b').should be == { 'a' => ['a'], 'b' => 'b' }}
695
+ example { pattern.params('/a/b/c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }}
696
+ end
697
+ end
698
+ end
699
+
700
+ context 'operator ;' do
701
+ context 'prefix' do
702
+ pattern '{;a:3}/bar' do
703
+ it { should match(';a=foo/bar') .capturing a: 'foo' }
704
+ it { should match(';a=fo/bar') .capturing a: 'fo' }
705
+ it { should match(';a=f/bar') .capturing a: 'f' }
706
+ it { should_not match(';a=fooo/bar') }
707
+ end
708
+
709
+ pattern '{;a:3}{b}' do
710
+ it { should match(';a=foobar') .capturing a: 'foo', b: 'bar' }
711
+ end
712
+ end
713
+
714
+ context 'expand' do
715
+ pattern '{;a*}' do
716
+ it { should match(';a=1') .capturing a: 'a=1' }
717
+ it { should match(';a=1;a=2') .capturing a: 'a=1;a=2' }
718
+ it { should match(';a=1;a=2;a=3') .capturing a: 'a=1;a=2;a=3' }
719
+ it { should_not match(';a=1;a=2;b=3') }
720
+ it { should_not match(';a=1;a=2;a=3,') }
721
+ end
722
+
723
+ pattern '{;a*,b}' do
724
+ it { should match(';a=1;b') .capturing a: 'a=1', b: nil }
725
+ it { should match(';a=2;a=2;b=1') .capturing a: 'a=2;a=2', b: '1' }
726
+ it { should_not match(';a;b;c') }
727
+ it { should_not match(';a;') }
728
+
729
+ example { pattern.params(';a=2;a=2;b').should be == { 'a' => ['2', '2'], 'b' => nil }}
730
+ end
731
+ end
732
+ end
733
+
734
+ context 'operator ?' do
735
+ context 'prefix' do
736
+ pattern '{?a:3}/bar' do
737
+ it { should match('?a=foo/bar') .capturing a: 'foo' }
738
+ it { should match('?a=fo/bar') .capturing a: 'fo' }
739
+ it { should match('?a=f/bar') .capturing a: 'f' }
740
+ it { should_not match('?a=fooo/bar') }
741
+ end
742
+
743
+ pattern '{?a:3}{b}' do
744
+ it { should match('?a=foobar') .capturing a: 'foo', b: 'bar' }
745
+ end
746
+ end
747
+
748
+ context 'expand' do
749
+ pattern '{?a*}' do
750
+ it { should match('?a=1') .capturing a: 'a=1' }
751
+ it { should match('?a=1&a=2') .capturing a: 'a=1&a=2' }
752
+ it { should match('?a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' }
753
+ it { should_not match('?a=1&a=2&b=3') }
754
+ it { should_not match('?a=1&a=2&a=3,') }
755
+ end
756
+
757
+ pattern '{?a*,b}' do
758
+ it { should match('?a=1&b') .capturing a: 'a=1', b: nil }
759
+ it { should match('?a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' }
760
+ it { should_not match('?a&b&c') }
761
+ it { should_not match('?a&') }
762
+
763
+ example { pattern.params('?a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }}
764
+ end
765
+ end
766
+ end
767
+
768
+ context 'operator &' do
769
+ context 'prefix' do
770
+ pattern '{&a:3}/bar' do
771
+ it { should match('&a=foo/bar') .capturing a: 'foo' }
772
+ it { should match('&a=fo/bar') .capturing a: 'fo' }
773
+ it { should match('&a=f/bar') .capturing a: 'f' }
774
+ it { should_not match('&a=fooo/bar') }
775
+ end
776
+
777
+ pattern '{&a:3}{b}' do
778
+ it { should match('&a=foobar') .capturing a: 'foo', b: 'bar' }
779
+ end
780
+ end
781
+
782
+ context 'expand' do
783
+ pattern '{&a*}' do
784
+ it { should match('&a=1') .capturing a: 'a=1' }
785
+ it { should match('&a=1&a=2') .capturing a: 'a=1&a=2' }
786
+ it { should match('&a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' }
787
+ it { should_not match('&a=1&a=2&b=3') }
788
+ it { should_not match('&a=1&a=2&a=3,') }
789
+ end
790
+
791
+ pattern '{&a*,b}' do
792
+ it { should match('&a=1&b') .capturing a: 'a=1', b: nil }
793
+ it { should match('&a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' }
794
+ it { should_not match('&a&b&c') }
795
+ it { should_not match('&a&') }
796
+
797
+ example { pattern.params('&a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }}
798
+ example { pattern.params('&a=2&a=%20&b').should be == { 'a' => ['2', ' '], 'b' => nil }}
799
+ end
800
+ end
801
+ end
802
+ end
803
+
804
+ context 'invalid syntax' do
805
+ example 'unexpected closing bracket' do
806
+ expect { Mustermann::Template.new('foo}bar') }.
807
+ to raise_error(Mustermann::ParseError, 'unexpected } while parsing "foo}bar"')
808
+ end
809
+
810
+ example 'missing closing bracket' do
811
+ expect { Mustermann::Template.new('foo{bar') }.
812
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo{bar"')
813
+ end
814
+ end
815
+ end