mustermann-rails 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8a27c18c01c4a2db8f22534ae77959dead448a8d
4
+ data.tar.gz: fa2947e67a353ffec312ab8f8524c036b00558dd
5
+ SHA512:
6
+ metadata.gz: 84a6f98b7795648592d926b759a92516e383778c479672f4754bcc6a37e3dae73e45435c4bbad08276cc4688cf87abd268c6e9621b48070f783ab58add2e4a68
7
+ data.tar.gz: 659bf9f1296199b8f152599228ceec43ce9a28a08177af7a720816acb5e2be8cdd5f8aaa2257d8dda1f41ff6c602bc6599f9541bd25f77d69bf0e814c5445eba
@@ -0,0 +1,103 @@
1
+ # Rails Syntax for Mustermann
2
+
3
+ This gem implements the `rails` pattern type for Mustermann. It is compatible with [Ruby on Rails](http://rubyonrails.org/), [Journey](https://github.com/rails/journey), the [http_router gem](https://github.com/joshbuddy/http_router), [Lotus](http://lotusrb.org/) and [Scalatra](http://www.scalatra.org/) (if [configured](http://www.scalatra.org/2.3/guides/http/routes.html#toc_248))</td>
4
+
5
+ ## Overview
6
+
7
+ **Supported options:**
8
+ `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, `version`, and `ignore_unknown_options`.
9
+
10
+ **External documentation:**
11
+ [Ruby on Rails Guides: Routing](http://guides.rubyonrails.org/routing.html).
12
+
13
+ ``` ruby
14
+ require 'mustermann'
15
+
16
+ pattern = Mustermann.new('/:example', type: :rails)
17
+ pattern === "/foo.bar" # => true
18
+ pattern === "/foo/bar" # => false
19
+ pattern.params("/foo.bar") # => { "example" => "foo.bar" }
20
+ pattern.params("/foo/bar") # => nil
21
+
22
+ pattern = Mustermann.new('/:example(/:optional)', type: :rails)
23
+ pattern === "/foo.bar" # => true
24
+ pattern === "/foo/bar" # => true
25
+ pattern.params("/foo.bar") # => { "example" => "foo.bar", "optional" => nil }
26
+ pattern.params("/foo/bar") # => { "example" => "foo", "optional" => "bar" }
27
+
28
+ pattern = Mustermann.new('/*example', type: :rails)
29
+ pattern === "/foo.bar" # => true
30
+ pattern === "/foo/bar" # => true
31
+ pattern.params("/foo.bar") # => { "example" => "foo.bar" }
32
+ pattern.params("/foo/bar") # => { "example" => "foo/bar" }
33
+ ```
34
+
35
+ ## Rails Compatibility
36
+
37
+ Rails syntax changed over time. You can target different Ruby on Rails versions by setting the `version` option to the desired Rails version.
38
+
39
+ The default is `4.2`. Versions prior to `2.3` are not supported.
40
+
41
+ ``` ruby
42
+ require 'mustermann'
43
+ Mustermann.new('/', type: :rails, version: "2.3")
44
+ Mustermann.new('/', type: :rails, version: "3.0.0")
45
+
46
+ require 'rails'
47
+ Mustermann.new('/', type: :rails, version: Rails::VERSION::STRING)
48
+ ```
49
+
50
+ ## Syntax
51
+
52
+ <table>
53
+ <thead>
54
+ <tr>
55
+ <th>Syntax Element</th>
56
+ <th>Description</th>
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ <tr>
61
+ <td><b>:</b><i>name</i></td>
62
+ <td>
63
+ Captures anything but a forward slash in a semi-greedy fashion. Capture is named <i>name</i>.
64
+ Capture behavior can be modified with tt>capture</tt> and <tt>greedy</tt> option.
65
+ </td>
66
+ </tr>
67
+ <tr>
68
+ <td><b>*</b><i>name</i></td>
69
+ <td>
70
+ Captures anything in a non-greedy fashion. Capture is named <i>name</i>.
71
+ </td>
72
+ </tr>
73
+ <tr>
74
+ <td><b>(</b><i>expression</i><b>)</b></td>
75
+ <td>Enclosed <i>expression</i> is optional. Not available in 2.3 compatibility mode.</td>
76
+ </tr>
77
+ <tr>
78
+ <td><b>/</b></td>
79
+ <td>
80
+ Matches forward slash. Does not match URI encoded version of forward slash.
81
+ </td>
82
+ </tr>
83
+ <tr>
84
+ <td><b>\</b><i>x</i></td>
85
+ <td>
86
+ In 3.x compatibility mode and starting with 4.2:
87
+ Matches <i>x</i> or URI encoded version of <i>x</i>. For instance <tt>\*</tt> matches <tt>*</tt>.<br>
88
+ In 4.0 or 4.1 compatibility mode:
89
+ <b>\</b> is ignored, <i>x</i> is parsed normally.<br>
90
+ </td>
91
+ </tr>
92
+ <tr>
93
+ <td><b>|</b></td>
94
+ <td>
95
+ Starting with 3.2 compatibility mode, this will raise a `Mustermann::ParseError`. While Ruby on Rails happily parses this character, it will result in broken routes due to a buggy implementation.
96
+ </td>
97
+ </tr>
98
+ <tr>
99
+ <td><i>any other character</i></td>
100
+ <td>Matches exactly that character or a URI encoded version of it.</td>
101
+ </tr>
102
+ </tbody>
103
+ </table>
@@ -0,0 +1,43 @@
1
+ require 'mustermann'
2
+ require 'mustermann/ast/pattern'
3
+ require 'mustermann/versions'
4
+
5
+ module Mustermann
6
+ # Rails style pattern implementation.
7
+ #
8
+ # @example
9
+ # Mustermann.new('/:foo', type: :rails) === '/bar' # => true
10
+ #
11
+ # @see Mustermann::Pattern
12
+ # @see file:README.md#rails Syntax description in the README
13
+ class Rails < AST::Pattern
14
+ extend Versions
15
+ register :rails
16
+
17
+ # first parser, no optional parts
18
+ version('2.3') do
19
+ on(nil) { |c| unexpected(c) }
20
+ on(?*) { |c| node(:named_splat) { scan(/\w+/) } }
21
+ on(?:) { |c| node(:capture) { scan(/\w+/) } }
22
+ end
23
+
24
+ # rack-mount
25
+ version('3.0', '3.1') do
26
+ on(?)) { |c| unexpected(c) }
27
+ on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
28
+ on(?\\) { |c| node(:char, expect(/./)) }
29
+ end
30
+
31
+ # stand-alone journey
32
+ version('3.2') do
33
+ on(?|) { |c| raise ParseError, "the implementation of | is broken in ActionDispatch, cannot compile compatible pattern" }
34
+ on(?\\) { |c| node(:char, c) }
35
+ end
36
+
37
+ # embedded journey, broken (ignored) escapes
38
+ version('4.0', '4.1') { on(?\\) { |c| read } }
39
+
40
+ # escapes got fixed in 4.2
41
+ version('4.2') { on(?\\) { |c| node(:char, expect(/./)) } }
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ module Mustermann
2
+ # Mixin that adds support for multiple versions of the same type.
3
+ # @see Mustermann::Rails
4
+ # @!visibility private
5
+ module Versions
6
+ # Checks if class has mulitple versions available and picks one that matches the version option.
7
+ # @!visibility private
8
+ def new(*args, version: nil, **options)
9
+ return super(*args, **options) unless versions.any?
10
+ self[version].new(*args, **options)
11
+ end
12
+
13
+ # @return [Hash] version to subclass mapping.
14
+ # @!visibility private
15
+ def versions
16
+ @versions ||= {}
17
+ end
18
+
19
+ # Defines a new version.
20
+ # @!visibility private
21
+ def version(*list, inherit_from: nil, &block)
22
+ superclass = self[inherit_from] || self
23
+ subclass = Class.new(superclass, &block)
24
+ list.each { |v| versions[v] = subclass }
25
+ end
26
+
27
+ # Resolve a subclass for a given version string.
28
+ # @!visibility private
29
+ def [](version)
30
+ return versions.values.last unless version
31
+ detected = versions.detect { |v,_| version.start_with?(v) }
32
+ raise ArgumentError, 'unsupported version %p' % version unless detected
33
+ detected.last
34
+ end
35
+
36
+ # @!visibility private
37
+ def name
38
+ super || superclass.name
39
+ end
40
+
41
+ # @!visibility private
42
+ def inspect
43
+ name
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ $:.unshift File.expand_path("../../mustermann/lib", __FILE__)
2
+ require "mustermann/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "mustermann-rails"
6
+ s.version = Mustermann::VERSION
7
+ s.author = "Konstantin Haase"
8
+ s.email = "konstantin.mailinglists@googlemail.com"
9
+ s.homepage = "https://github.com/rkh/mustermann"
10
+ s.summary = %q{Rails syntax for Mustermann}
11
+ s.description = %q{Adds Rails style patterns to Mustermman}
12
+ s.license = 'MIT'
13
+ s.required_ruby_version = '>= 2.1.0'
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.add_dependency 'mustermann', Mustermann::VERSION
18
+ end
@@ -0,0 +1,640 @@
1
+ require 'support'
2
+ require 'mustermann/rails'
3
+
4
+ describe Mustermann::Rails do
5
+ extend Support::Pattern
6
+
7
+ pattern '' do
8
+ it { should match('') }
9
+ it { should_not match('/') }
10
+
11
+ it { should expand.to('') }
12
+ it { should_not expand(a: 1) }
13
+
14
+ it { should generate_template('') }
15
+
16
+ it { should respond_to(:expand) }
17
+ it { should respond_to(:to_templates) }
18
+ end
19
+
20
+ pattern '/' do
21
+ it { should match('/') }
22
+ it { should_not match('/foo') }
23
+
24
+ it { should expand.to('/') }
25
+ it { should_not expand(a: 1) }
26
+ end
27
+
28
+ pattern '/foo' do
29
+ it { should match('/foo') }
30
+ it { should_not match('/bar') }
31
+ it { should_not match('/foo.bar') }
32
+
33
+ it { should expand.to('/foo') }
34
+ it { should_not expand(a: 1) }
35
+ end
36
+
37
+ pattern '/foo/bar' do
38
+ it { should match('/foo/bar') }
39
+ it { should_not match('/foo%2Fbar') }
40
+ it { should_not match('/foo%2fbar') }
41
+
42
+ it { should expand.to('/foo/bar') }
43
+ it { should_not expand(a: 1) }
44
+ end
45
+
46
+ pattern '/:foo' do
47
+ it { should match('/foo') .capturing foo: 'foo' }
48
+ it { should match('/bar') .capturing foo: 'bar' }
49
+ it { should match('/foo.bar') .capturing foo: 'foo.bar' }
50
+ it { should match('/%0Afoo') .capturing foo: '%0Afoo' }
51
+ it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' }
52
+
53
+ it { should_not match('/foo?') }
54
+ it { should_not match('/foo/bar') }
55
+ it { should_not match('/') }
56
+ it { should_not match('/foo/') }
57
+
58
+ example { pattern.params('/foo') .should be == {"foo" => "foo"} }
59
+ example { pattern.params('/f%20o') .should be == {"foo" => "f o"} }
60
+ example { pattern.params('').should be_nil }
61
+
62
+ it { should expand(foo: 'bar') .to('/bar') }
63
+ it { should expand(foo: 'b r') .to('/b%20r') }
64
+ it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') }
65
+
66
+ it { should_not expand(foo: 'foo', bar: 'bar') }
67
+ it { should_not expand(bar: 'bar') }
68
+ it { should_not expand }
69
+
70
+ it { should generate_template('/{foo}') }
71
+ end
72
+
73
+ pattern '/föö' do
74
+ it { should match("/f%C3%B6%C3%B6") }
75
+ it { should expand.to("/f%C3%B6%C3%B6") }
76
+ it { should_not expand(a: 1) }
77
+ end
78
+
79
+ pattern "/:foo/:bar" do
80
+ it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' }
81
+ it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' }
82
+ it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' }
83
+ it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' }
84
+ it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' }
85
+
86
+ it { should_not match('/foo%2Fbar') }
87
+ it { should_not match('/foo%2fbar') }
88
+
89
+ example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} }
90
+ example { pattern.params('').should be_nil }
91
+
92
+ it { should expand(foo: 'foo', bar: 'bar').to('/foo/bar') }
93
+ it { should_not expand(foo: 'foo') }
94
+ it { should_not expand(bar: 'bar') }
95
+
96
+ it { should generate_template('/{foo}/{bar}') }
97
+ end
98
+
99
+ pattern '/hello/:person' do
100
+ it { should match('/hello/Frank').capturing person: 'Frank' }
101
+ it { should expand(person: 'Frank') .to '/hello/Frank' }
102
+ it { should expand(person: 'Frank?') .to '/hello/Frank%3F' }
103
+
104
+ it { should generate_template('/hello/{person}') }
105
+ end
106
+
107
+ pattern '/?:foo?/?:bar?' do
108
+ it { should match('/?hello?/?world?').capturing foo: 'hello', bar: 'world' }
109
+ it { should_not match('/hello/world/') }
110
+ it { should expand(foo: 'hello', bar: 'world').to('/%3Fhello%3F/%3Fworld%3F') }
111
+ it { should generate_template('/?{foo}?/?{bar}?') }
112
+ end
113
+
114
+ pattern '/:foo_bar' do
115
+ it { should match('/hello').capturing foo_bar: 'hello' }
116
+ it { should expand(foo_bar: 'hello').to('/hello') }
117
+ it { should generate_template('/{foo_bar}') }
118
+ end
119
+
120
+ pattern '/*foo' do
121
+ it { should match('/') .capturing foo: '' }
122
+ it { should match('/foo') .capturing foo: 'foo' }
123
+ it { should match('/foo/bar') .capturing foo: 'foo/bar' }
124
+
125
+ it { should expand .to('/') }
126
+ it { should expand(foo: nil) .to('/') }
127
+ it { should expand(foo: '') .to('/') }
128
+ it { should expand(foo: 'foo') .to('/foo') }
129
+ it { should expand(foo: 'foo/bar') .to('/foo/bar') }
130
+ it { should expand(foo: 'foo.bar') .to('/foo.bar') }
131
+
132
+ it { should generate_template('/{+foo}') }
133
+ end
134
+
135
+ pattern '/*splat' do
136
+ it { should match('/') .capturing splat: '' }
137
+ it { should match('/foo') .capturing splat: 'foo' }
138
+ it { should match('/foo/bar') .capturing splat: 'foo/bar' }
139
+ it { should generate_template('/{+splat}') }
140
+ end
141
+
142
+ pattern '/:foo/*bar' do
143
+ it { should match("/foo/bar/baz") .capturing foo: 'foo', bar: 'bar/baz' }
144
+ it { should match("/foo%2Fbar/baz") .capturing foo: 'foo%2Fbar', bar: 'baz' }
145
+ it { should match("/foo/") .capturing foo: 'foo', bar: '' }
146
+ it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', bar: 'h%20a%20y' }
147
+ it { should_not match('/foo') }
148
+
149
+ it { should expand(foo: 'foo') .to('/foo/') }
150
+ it { should expand(foo: 'foo', bar: 'bar') .to('/foo/bar') }
151
+ it { should expand(foo: 'foo', bar: 'foo/bar') .to('/foo/foo/bar') }
152
+ it { should expand(foo: 'foo/bar', bar: 'bar') .to('/foo%2Fbar/bar') }
153
+
154
+ it { should generate_template('/{foo}/{+bar}') }
155
+ end
156
+
157
+ pattern '/test$/' do
158
+ it { should match('/test$/') }
159
+ it { should expand.to('/test$/') }
160
+ end
161
+
162
+ pattern '/te+st/' do
163
+ it { should match('/te+st/') }
164
+ it { should_not match('/test/') }
165
+ it { should_not match('/teest/') }
166
+ it { should expand.to('/te+st/') }
167
+ end
168
+
169
+ pattern "/path with spaces" do
170
+ it { should match('/path%20with%20spaces') }
171
+ it { should match('/path%2Bwith%2Bspaces') }
172
+ it { should match('/path+with+spaces') }
173
+ it { should expand.to('/path%20with%20spaces') }
174
+
175
+ it { should generate_template('/path%20with%20spaces') }
176
+ end
177
+
178
+ pattern '/foo&bar' do
179
+ it { should match('/foo&bar') }
180
+ end
181
+
182
+ pattern '/*a/:foo/*b/*c' do
183
+ it { should match('/bar/foo/bling/baz/boom').capturing a: 'bar', foo: 'foo', b: 'bling', c: 'baz/boom' }
184
+ example { pattern.params('/bar/foo/bling/baz/boom').should be == { "a" => 'bar', "foo" => 'foo', "b" => 'bling', "c" => 'baz/boom' } }
185
+ it { should expand(a: 'bar', foo: 'foo', b: 'bling', c: 'baz/boom').to('/bar/foo/bling/baz/boom') }
186
+ it { should generate_template('/{+a}/{foo}/{+b}/{+c}') }
187
+ end
188
+
189
+ pattern '/test.bar' do
190
+ it { should match('/test.bar') }
191
+ it { should_not match('/test0bar') }
192
+ end
193
+
194
+ pattern '/:file.:ext' do
195
+ it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' }
196
+ it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' }
197
+ it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' }
198
+
199
+ it { should match('/pony%E6%AD%A3%2Ejpg') .capturing file: 'pony%E6%AD%A3', ext: 'jpg' }
200
+ it { should match('/pony%e6%ad%a3%2ejpg') .capturing file: 'pony%e6%ad%a3', ext: 'jpg' }
201
+ it { should match('/pony正%2Ejpg') .capturing file: 'pony正', ext: 'jpg' }
202
+ it { should match('/pony正%2ejpg') .capturing file: 'pony正', ext: 'jpg' }
203
+ it { should match('/pony正..jpg') .capturing file: 'pony正.', ext: 'jpg' }
204
+
205
+ it { should_not match('/.jpg') }
206
+ it { should expand(file: 'pony', ext: 'jpg').to('/pony.jpg') }
207
+ end
208
+
209
+ pattern '/:a(x)' do
210
+ it { should match('/a') .capturing a: 'a' }
211
+ it { should match('/xa') .capturing a: 'xa' }
212
+ it { should match('/axa') .capturing a: 'axa' }
213
+ it { should match('/ax') .capturing a: 'a' }
214
+ it { should match('/axax') .capturing a: 'axa' }
215
+ it { should match('/axaxx') .capturing a: 'axax' }
216
+ it { should expand(a: 'x').to('/xx') }
217
+ it { should expand(a: 'a').to('/ax') }
218
+
219
+ it { should generate_template('/{a}x') }
220
+ it { should generate_template('/{a}') }
221
+ end
222
+
223
+ pattern '/:user(@:host)' do
224
+ it { should match('/foo@bar') .capturing user: 'foo', host: 'bar' }
225
+ it { should match('/foo.foo@bar') .capturing user: 'foo.foo', host: 'bar' }
226
+ it { should match('/foo@bar.bar') .capturing user: 'foo', host: 'bar.bar' }
227
+
228
+ it { should expand(user: 'foo') .to('/foo') }
229
+ it { should expand(user: 'foo', host: 'bar') .to('/foo@bar') }
230
+
231
+ it { should generate_template('/{user}') }
232
+ it { should generate_template('/{user}@{host}') }
233
+ end
234
+
235
+ pattern '/:file(.:ext)' do
236
+ it { should match('/pony') .capturing file: 'pony', ext: nil }
237
+ it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' }
238
+ it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' }
239
+ it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' }
240
+ it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' }
241
+ it { should match('/pony.') .capturing file: 'pony.' }
242
+ it { should_not match('/.jpg') }
243
+
244
+ it { should expand(file: 'pony') .to('/pony') }
245
+ it { should expand(file: 'pony', ext: 'jpg') .to('/pony.jpg') }
246
+
247
+ it { should generate_template('/{file}') }
248
+ it { should generate_template('/{file}.{ext}') }
249
+ end
250
+
251
+ pattern '/:id/test.bar' do
252
+ it { should match('/3/test.bar') .capturing id: '3' }
253
+ it { should match('/2/test.bar') .capturing id: '2' }
254
+ it { should match('/2E/test.bar') .capturing id: '2E' }
255
+ it { should match('/2e/test.bar') .capturing id: '2e' }
256
+ it { should match('/%2E/test.bar') .capturing id: '%2E' }
257
+ end
258
+
259
+ pattern '/10/:id' do
260
+ it { should match('/10/test') .capturing id: 'test' }
261
+ it { should match('/10/te.st') .capturing id: 'te.st' }
262
+ end
263
+
264
+ pattern '/10.1/:id' do
265
+ it { should match('/10.1/test') .capturing id: 'test' }
266
+ it { should match('/10.1/te.st') .capturing id: 'te.st' }
267
+ end
268
+
269
+ pattern '/:foo.:bar/:id' do
270
+ it { should match('/10.1/te.st') .capturing foo: "10", bar: "1", id: "te.st" }
271
+ it { should match('/10.1.2/te.st') .capturing foo: "10.1", bar: "2", id: "te.st" }
272
+ end
273
+
274
+ pattern '/:a/:b(.)(:c)' do
275
+ it { should match('/a/b') .capturing a: 'a', b: 'b', c: nil }
276
+ it { should match('/a/b.c') .capturing a: 'a', b: 'b', c: 'c' }
277
+ it { should match('/a.b/c') .capturing a: 'a.b', b: 'c', c: nil }
278
+ it { should match('/a.b/c.d') .capturing a: 'a.b', b: 'c', c: 'd' }
279
+ it { should_not match('/a.b/c.d/e') }
280
+
281
+ it { should expand(a: ?a, b: ?b) .to('/a/b.') }
282
+ it { should expand(a: ?a, b: ?b, c: ?c) .to('/a/b.c') }
283
+
284
+ it { should generate_template('/{a}/{b}') }
285
+ it { should generate_template('/{a}/{b}.') }
286
+ it { should generate_template('/{a}/{b}.{c}') }
287
+ end
288
+
289
+ pattern '/:a(foo:b)' do
290
+ it { should match('/barfoobar') .capturing a: 'bar', b: 'bar' }
291
+ it { should match('/barfoobarfoobar') .capturing a: 'barfoobar', b: 'bar' }
292
+ it { should match('/bar') .capturing a: 'bar', b: nil }
293
+ it { should_not match('/') }
294
+
295
+ it { should expand(a: ?a) .to('/a') }
296
+ it { should expand(a: ?a, b: ?b) .to('/afoob') }
297
+
298
+ it { should generate_template('/{a}foo{b}') }
299
+ it { should generate_template('/{a}') }
300
+ it { should_not generate_template('/{a}foo') }
301
+ end
302
+
303
+ pattern '/fo(o)' do
304
+ it { should match('/fo') }
305
+ it { should match('/foo') }
306
+ it { should_not match('') }
307
+ it { should_not match('/') }
308
+ it { should_not match('/f') }
309
+ it { should_not match('/fooo') }
310
+
311
+ it { should expand.to('/foo') }
312
+ end
313
+
314
+ pattern '/foo?' do
315
+ it { should match('/foo?') }
316
+ it { should_not match('/foo\?') }
317
+ it { should_not match('/fo') }
318
+ it { should_not match('/foo') }
319
+ it { should_not match('') }
320
+ it { should_not match('/') }
321
+ it { should_not match('/f') }
322
+ it { should_not match('/fooo') }
323
+
324
+ it { should expand.to('/foo%3F') }
325
+ end
326
+
327
+ pattern '/:fOO' do
328
+ it { should match('/a').capturing fOO: 'a' }
329
+ end
330
+
331
+ pattern '/:_X' do
332
+ it { should match('/a').capturing _X: 'a' }
333
+ end
334
+
335
+ pattern '/:f00' do
336
+ it { should match('/a').capturing f00: 'a' }
337
+ end
338
+
339
+ pattern '/:foo(/:bar)/:baz' do
340
+ it { should match('/foo/bar/baz').capturing foo: 'foo', bar: 'bar', baz: 'baz' }
341
+ it { should expand(foo: ?a, baz: ?b) .to('/a/b') }
342
+ it { should expand(foo: ?a, baz: ?b, bar: ?x) .to('/a/x/b') }
343
+ end
344
+
345
+ pattern '/:foo', capture: /\d+/ do
346
+ it { should match('/1') .capturing foo: '1' }
347
+ it { should match('/123') .capturing foo: '123' }
348
+
349
+ it { should_not match('/') }
350
+ it { should_not match('/foo') }
351
+ end
352
+
353
+ pattern '/:foo', capture: /\d+/ do
354
+ it { should match('/1') .capturing foo: '1' }
355
+ it { should match('/123') .capturing foo: '123' }
356
+
357
+ it { should_not match('/') }
358
+ it { should_not match('/foo') }
359
+ end
360
+
361
+ pattern '/:foo', capture: '1' do
362
+ it { should match('/1').capturing foo: '1' }
363
+
364
+ it { should_not match('/') }
365
+ it { should_not match('/foo') }
366
+ it { should_not match('/123') }
367
+ end
368
+
369
+ pattern '/:foo', capture: 'a.b' do
370
+ it { should match('/a.b') .capturing foo: 'a.b' }
371
+ it { should match('/a%2Eb') .capturing foo: 'a%2Eb' }
372
+ it { should match('/a%2eb') .capturing foo: 'a%2eb' }
373
+
374
+ it { should_not match('/ab') }
375
+ it { should_not match('/afb') }
376
+ it { should_not match('/a1b') }
377
+ it { should_not match('/a.bc') }
378
+ end
379
+
380
+ pattern '/:foo(/:bar)', capture: :alpha do
381
+ it { should match('/abc') .capturing foo: 'abc', bar: nil }
382
+ it { should match('/a/b') .capturing foo: 'a', bar: 'b' }
383
+ it { should match('/a') .capturing foo: 'a', bar: nil }
384
+
385
+ it { should_not match('/1/2') }
386
+ it { should_not match('/a/2') }
387
+ it { should_not match('/1/b') }
388
+ it { should_not match('/1') }
389
+ it { should_not match('/1/') }
390
+ it { should_not match('/a/') }
391
+ it { should_not match('//a') }
392
+ end
393
+
394
+ pattern '/:foo', capture: ['foo', 'bar', /\d+/] do
395
+ it { should match('/1') .capturing foo: '1' }
396
+ it { should match('/123') .capturing foo: '123' }
397
+ it { should match('/foo') .capturing foo: 'foo' }
398
+ it { should match('/bar') .capturing foo: 'bar' }
399
+
400
+ it { should_not match('/') }
401
+ it { should_not match('/baz') }
402
+ it { should_not match('/foo1') }
403
+ end
404
+
405
+ pattern '/:foo:bar:baz', capture: { foo: :alpha, bar: /\d+/ } do
406
+ it { should match('/ab123xy-1') .capturing foo: 'ab', bar: '123', baz: 'xy-1' }
407
+ it { should match('/ab123') .capturing foo: 'ab', bar: '12', baz: '3' }
408
+ it { should_not match('/123abcxy-1') }
409
+ it { should_not match('/abcxy-1') }
410
+ it { should_not match('/abc1') }
411
+ end
412
+
413
+ pattern '/:foo', capture: { foo: ['foo', 'bar', /\d+/] } do
414
+ it { should match('/1') .capturing foo: '1' }
415
+ it { should match('/123') .capturing foo: '123' }
416
+ it { should match('/foo') .capturing foo: 'foo' }
417
+ it { should match('/bar') .capturing foo: 'bar' }
418
+
419
+ it { should_not match('/') }
420
+ it { should_not match('/baz') }
421
+ it { should_not match('/foo1') }
422
+ end
423
+
424
+ pattern '/:file(.:ext)', capture: { ext: ['jpg', 'png'] } do
425
+ it { should match('/pony') .capturing file: 'pony', ext: nil }
426
+ it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' }
427
+ it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' }
428
+ it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' }
429
+ it { should match('/pony.png') .capturing file: 'pony', ext: 'png' }
430
+ it { should match('/pony%2Epng') .capturing file: 'pony', ext: 'png' }
431
+ it { should match('/pony%2epng') .capturing file: 'pony', ext: 'png' }
432
+ it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' }
433
+ it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: 'png' }
434
+ it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil }
435
+ it { should match('/pony.') .capturing file: 'pony.', ext: nil }
436
+ it { should_not match('.jpg') }
437
+ end
438
+
439
+ pattern '/:file(:ext)', capture: { ext: ['.jpg', '.png', '.tar.gz'] } do
440
+ it { should match('/pony') .capturing file: 'pony', ext: nil }
441
+ it { should match('/pony.jpg') .capturing file: 'pony', ext: '.jpg' }
442
+ it { should match('/pony.png') .capturing file: 'pony', ext: '.png' }
443
+ it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: '.jpg' }
444
+ it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: '.png' }
445
+ it { should match('/pony.tar.gz') .capturing file: 'pony', ext: '.tar.gz' }
446
+ it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil }
447
+ it { should match('/pony.') .capturing file: 'pony.', ext: nil }
448
+ it { should_not match('/.jpg') }
449
+ end
450
+
451
+ pattern '/:a(@:b)', capture: { b: /\d+/ } do
452
+ it { should match('/a') .capturing a: 'a', b: nil }
453
+ it { should match('/a@1') .capturing a: 'a', b: '1' }
454
+ it { should match('/a@b') .capturing a: 'a@b', b: nil }
455
+ it { should match('/a@1@2') .capturing a: 'a@1', b: '2' }
456
+ end
457
+
458
+ pattern '/:a(b)', greedy: false do
459
+ it { should match('/ab').capturing a: 'a' }
460
+ end
461
+
462
+ pattern '/:file(.:ext)', greedy: false do
463
+ it { should match('/pony') .capturing file: 'pony', ext: nil }
464
+ it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' }
465
+ it { should match('/pony.png.jpg') .capturing file: 'pony', ext: 'png.jpg' }
466
+ end
467
+
468
+ pattern '/:controller(/:action(/:id(.:format)))' do
469
+ it { should match('/content').capturing controller: 'content' }
470
+ end
471
+
472
+ pattern '/fo(o)', uri_decode: false do
473
+ it { should match('/foo') }
474
+ it { should match('/fo') }
475
+ it { should_not match('/fo(o)') }
476
+ end
477
+
478
+ pattern '/foo/bar', uri_decode: false do
479
+ it { should match('/foo/bar') }
480
+ it { should_not match('/foo%2Fbar') }
481
+ it { should_not match('/foo%2fbar') }
482
+ end
483
+
484
+ pattern "/path with spaces", uri_decode: false do
485
+ it { should match('/path with spaces') }
486
+ it { should_not match('/path%20with%20spaces') }
487
+ it { should_not match('/path%2Bwith%2Bspaces') }
488
+ it { should_not match('/path+with+spaces') }
489
+ end
490
+
491
+ pattern "/path with spaces", space_matches_plus: false do
492
+ it { should match('/path%20with%20spaces') }
493
+ it { should_not match('/path%2Bwith%2Bspaces') }
494
+ it { should_not match('/path+with+spaces') }
495
+ end
496
+
497
+ context 'invalid syntax' do
498
+ example 'unexpected closing parenthesis' do
499
+ expect { Mustermann::Rails.new('foo)bar') }.
500
+ to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"')
501
+ end
502
+
503
+ example 'missing closing parenthesis' do
504
+ expect { Mustermann::Rails.new('foo(bar') }.
505
+ to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"')
506
+ end
507
+ end
508
+
509
+ context 'invalid capture names' do
510
+ example 'empty name' do
511
+ expect { Mustermann::Rails.new('/:/') }.
512
+ to raise_error(Mustermann::CompileError, "capture name can't be empty: \"/:/\"")
513
+ end
514
+
515
+ example 'named splat' do
516
+ expect { Mustermann::Rails.new('/:splat/') }.
517
+ to raise_error(Mustermann::CompileError, "capture name can't be splat: \"/:splat/\"")
518
+ end
519
+
520
+ example 'named captures' do
521
+ expect { Mustermann::Rails.new('/:captures/') }.
522
+ to raise_error(Mustermann::CompileError, "capture name can't be captures: \"/:captures/\"")
523
+ end
524
+
525
+ example 'with capital letter' do
526
+ expect { Mustermann::Rails.new('/:Foo/') }.
527
+ to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:Foo/\"")
528
+ end
529
+
530
+ example 'with integer' do
531
+ expect { Mustermann::Rails.new('/:1a/') }.
532
+ to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:1a/\"")
533
+ end
534
+
535
+ example 'same name twice' do
536
+ expect { Mustermann::Rails.new('/:foo(/:bar)/:bar') }.
537
+ to raise_error(Mustermann::CompileError, "can't use the same capture name twice: \"/:foo(/:bar)/:bar\"")
538
+ end
539
+ end
540
+
541
+ context 'Regexp compatibility' do
542
+ describe :=== do
543
+ example('non-matching') { Mustermann::Rails.new("/") .should_not be === '/foo' }
544
+ example('matching') { Mustermann::Rails.new("/:foo") .should be === '/foo' }
545
+ end
546
+
547
+ describe :=~ do
548
+ example('non-matching') { Mustermann::Rails.new("/") .should_not be =~ '/foo' }
549
+ example('matching') { Mustermann::Rails.new("/:foo") .should be =~ '/foo' }
550
+
551
+ context 'String#=~' do
552
+ example('non-matching') { "/foo".should_not be =~ Mustermann::Rails.new("/") }
553
+ example('matching') { "/foo".should be =~ Mustermann::Rails.new("/:foo") }
554
+ end
555
+ end
556
+
557
+ describe :to_regexp do
558
+ example('empty pattern') { Mustermann::Rails.new('').to_regexp.should be == /\A(?-mix:)\Z/ }
559
+
560
+ context 'Regexp.try_convert' do
561
+ example('empty pattern') { Regexp.try_convert(Mustermann::Rails.new('')).should be == /\A(?-mix:)\Z/ }
562
+ end
563
+ end
564
+ end
565
+
566
+ context 'Proc compatibility' do
567
+ describe :to_proc do
568
+ example { Mustermann::Rails.new("/").to_proc.should be_a(Proc) }
569
+ example('non-matching') { Mustermann::Rails.new("/") .to_proc.call('/foo').should be == false }
570
+ example('matching') { Mustermann::Rails.new("/:foo") .to_proc.call('/foo').should be == true }
571
+ end
572
+ end
573
+
574
+ context "peeking" do
575
+ subject(:pattern) { Mustermann::Rails.new(":name") }
576
+
577
+ describe :peek_size do
578
+ example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size }
579
+ example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size }
580
+ example { pattern.peek_size("/foo bar") .should be_nil }
581
+ end
582
+
583
+ describe :peek_match do
584
+ example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" }
585
+ example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" }
586
+ example { pattern.peek_match("/foo bar") .should be_nil }
587
+ end
588
+
589
+ describe :peek_params do
590
+ example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] }
591
+ example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] }
592
+ example { pattern.peek_params("/foo bar") .should be_nil }
593
+ end
594
+ end
595
+
596
+ context 'version compatibility' do
597
+ context '2.3' do
598
+ pattern '(foo)', version: '2.3' do
599
+ it { should_not match("") }
600
+ it { should_not match("foo") }
601
+ it { should match("(foo)") }
602
+ end
603
+
604
+ pattern '\\:name', version: '2.3' do
605
+ it { should match('%5cfoo').capturing(name: 'foo') }
606
+ end
607
+ end
608
+
609
+ context '3.0' do
610
+ pattern '(foo)', version: '3.0' do
611
+ it { should match("") }
612
+ it { should match("foo") }
613
+ end
614
+
615
+ pattern '\\:name', version: '3.0' do
616
+ it { should match(':name') }
617
+ it { should_not match(':foo') }
618
+ end
619
+ end
620
+
621
+ context '3.2' do
622
+ pattern '\\:name', version: '3.2' do
623
+ it { should match('%5cfoo').capturing(name: 'foo') }
624
+ end
625
+ end
626
+
627
+ context '4.0' do
628
+ pattern '\\:name', version: '4.0' do
629
+ it { should match('foo').capturing(name: 'foo') }
630
+ end
631
+ end
632
+
633
+ context '4.2' do
634
+ pattern '\\:name', version: '4.2' do
635
+ it { should match(':name') }
636
+ it { should_not match(':foo') }
637
+ end
638
+ end
639
+ end
640
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mustermann-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Haase
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mustermann
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ description: Adds Rails style patterns to Mustermman
28
+ email: konstantin.mailinglists@googlemail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/mustermann/rails.rb
35
+ - lib/mustermann/versions.rb
36
+ - mustermann-rails.gemspec
37
+ - spec/rails_spec.rb
38
+ homepage: https://github.com/rkh/mustermann
39
+ licenses:
40
+ - MIT
41
+ metadata: {}
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 2.1.0
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubyforge_project:
58
+ rubygems_version: 2.4.3
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Rails syntax for Mustermann
62
+ test_files:
63
+ - spec/rails_spec.rb
64
+ has_rdoc: