mustermann-rails 0.4.0

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.
@@ -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: