roda 0.9.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +709 -0
  5. data/Rakefile +124 -0
  6. data/lib/roda.rb +608 -0
  7. data/lib/roda/plugins/all_verbs.rb +48 -0
  8. data/lib/roda/plugins/default_headers.rb +50 -0
  9. data/lib/roda/plugins/error_handler.rb +69 -0
  10. data/lib/roda/plugins/flash.rb +62 -0
  11. data/lib/roda/plugins/h.rb +24 -0
  12. data/lib/roda/plugins/halt.rb +79 -0
  13. data/lib/roda/plugins/header_matchers.rb +57 -0
  14. data/lib/roda/plugins/hooks.rb +106 -0
  15. data/lib/roda/plugins/indifferent_params.rb +47 -0
  16. data/lib/roda/plugins/middleware.rb +88 -0
  17. data/lib/roda/plugins/multi_route.rb +77 -0
  18. data/lib/roda/plugins/not_found.rb +62 -0
  19. data/lib/roda/plugins/pass.rb +34 -0
  20. data/lib/roda/plugins/render.rb +217 -0
  21. data/lib/roda/plugins/streaming.rb +165 -0
  22. data/spec/composition_spec.rb +19 -0
  23. data/spec/env_spec.rb +11 -0
  24. data/spec/integration_spec.rb +63 -0
  25. data/spec/matchers_spec.rb +658 -0
  26. data/spec/module_spec.rb +29 -0
  27. data/spec/opts_spec.rb +42 -0
  28. data/spec/plugin/all_verbs_spec.rb +29 -0
  29. data/spec/plugin/default_headers_spec.rb +63 -0
  30. data/spec/plugin/error_handler_spec.rb +67 -0
  31. data/spec/plugin/flash_spec.rb +59 -0
  32. data/spec/plugin/h_spec.rb +13 -0
  33. data/spec/plugin/halt_spec.rb +62 -0
  34. data/spec/plugin/header_matchers_spec.rb +61 -0
  35. data/spec/plugin/hooks_spec.rb +97 -0
  36. data/spec/plugin/indifferent_params_spec.rb +13 -0
  37. data/spec/plugin/middleware_spec.rb +52 -0
  38. data/spec/plugin/multi_route_spec.rb +98 -0
  39. data/spec/plugin/not_found_spec.rb +99 -0
  40. data/spec/plugin/pass_spec.rb +23 -0
  41. data/spec/plugin/render_spec.rb +148 -0
  42. data/spec/plugin/streaming_spec.rb +52 -0
  43. data/spec/plugin_spec.rb +61 -0
  44. data/spec/redirect_spec.rb +24 -0
  45. data/spec/request_spec.rb +55 -0
  46. data/spec/response_spec.rb +131 -0
  47. data/spec/session_spec.rb +35 -0
  48. data/spec/spec_helper.rb +89 -0
  49. data/spec/version_spec.rb +8 -0
  50. metadata +148 -0
@@ -0,0 +1,165 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The streaming plugin adds support for streaming responses
4
+ # from roda using the +stream+ method:
5
+ #
6
+ # plugin :streaming
7
+ #
8
+ # route do |r|
9
+ # stream do |out|
10
+ # ['a', 'b', 'c'].each{|v| out << v; sleep 1}
11
+ # end
12
+ # end
13
+ #
14
+ # In order for streaming to work, any webservers used in
15
+ # front of the roda app must not buffer responses.
16
+ #
17
+ # The stream method takes the following options:
18
+ #
19
+ # :callback :: A callback proc to call when the connection is
20
+ # closed.
21
+ # :keep_open :: Whether to keep the connection open after the
22
+ # stream block returns, default is false.
23
+ # :loop :: Whether to call the stream block continuously until
24
+ # the connection is closed.
25
+ #
26
+ # The implementation was originally taken from Sinatra,
27
+ # which is also released under the MIT License:
28
+ #
29
+ # Copyright (c) 2007, 2008, 2009 Blake Mizerany
30
+ # Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase
31
+ #
32
+ # Permission is hereby granted, free of charge, to any person
33
+ # obtaining a copy of this software and associated documentation
34
+ # files (the "Software"), to deal in the Software without
35
+ # restriction, including without limitation the rights to use,
36
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ # copies of the Software, and to permit persons to whom the
38
+ # Software is furnished to do so, subject to the following
39
+ # conditions:
40
+ #
41
+ # The above copyright notice and this permission notice shall be
42
+ # included in all copies or substantial portions of the Software.
43
+ #
44
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
45
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
46
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
47
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
48
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
49
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
50
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
51
+ # OTHER DEALINGS IN THE SOFTWARE.
52
+ module Streaming
53
+ # Class of the response body in case you use #stream.
54
+ #
55
+ # Three things really matter: The front and back block (back being the
56
+ # block generating content, front the one sending it to the client) and
57
+ # the scheduler, integrating with whatever concurrency feature the Rack
58
+ # handler is using.
59
+ #
60
+ # Scheduler has to respond to defer and schedule.
61
+ class Stream
62
+ include Enumerable
63
+
64
+ # The default scheduler to used when streaming, useful for code
65
+ # using ruby's default threading support.
66
+ class Scheduler
67
+ # Store the stream to schedule.
68
+ def initialize(stream)
69
+ @stream = stream
70
+ end
71
+
72
+ # Immediately yield.
73
+ def defer(*)
74
+ yield
75
+ end
76
+
77
+ # Close the stream if there is an exception when scheduling,
78
+ # and reraise the exception if so.
79
+ def schedule(*)
80
+ yield
81
+ rescue Exception
82
+ @stream.close
83
+ raise
84
+ end
85
+ end
86
+
87
+ # Handle streaming options, see Streaming for details.
88
+ def initialize(opts={}, &back)
89
+ @scheduler = opts[:scheduler] || Scheduler.new(self)
90
+ @back = back.to_proc
91
+ @keep_open = opts[:keep_open]
92
+ @callbacks = []
93
+ @closed = false
94
+
95
+ if opts[:callback]
96
+ callback(&opts[:callback])
97
+ end
98
+ end
99
+
100
+ # Add output to the streaming response body.
101
+ def <<(data)
102
+ @scheduler.schedule{@front.call(data.to_s)}
103
+ self
104
+ end
105
+
106
+ # Add the given block as a callback to call when the block closes.
107
+ def callback(&block)
108
+ return yield if closed?
109
+ @callbacks << block
110
+ end
111
+
112
+ # Alias to callback for EventMachine compatibility.
113
+ alias errback callback
114
+
115
+ # If not already closed, close the connection, and call
116
+ # any callbacks.
117
+ def close
118
+ return if closed?
119
+ @closed = true
120
+ @scheduler.schedule{@callbacks.each{|c| c.call}}
121
+ end
122
+
123
+ # Whether the connection has already been closed.
124
+ def closed?
125
+ @closed
126
+ end
127
+
128
+ # Yield values to the block as they are passed in via #<<.
129
+ def each(&front)
130
+ @front = front
131
+ @scheduler.defer do
132
+ begin
133
+ @back.call(self)
134
+ rescue Exception => e
135
+ @scheduler.schedule{raise e}
136
+ end
137
+ close unless @keep_open
138
+ end
139
+ end
140
+ end
141
+
142
+ module InstanceMethods
143
+ # Immediately return a streaming response using the current response
144
+ # status and headers, calling the block to get the streaming response.
145
+ # See Streaming for details.
146
+ def stream(opts={}, &block)
147
+ opts = opts.merge(:scheduler=>EventMachine) if !opts.has_key?(:scheduler) && env['async.callback']
148
+
149
+ if opts[:loop]
150
+ block = proc do |out|
151
+ until out.closed?
152
+ yield(out)
153
+ end
154
+ end
155
+ end
156
+
157
+ res = response
158
+ request.halt [res.status || 200, res.headers, Stream.new(opts, &block)]
159
+ end
160
+ end
161
+ end
162
+
163
+ register_plugin(:streaming, Streaming)
164
+ end
165
+ end
@@ -0,0 +1,19 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe "r.run" do
4
+ it "should allow composition of apps" do
5
+ a = app do |r|
6
+ r.on "services/:id" do |id|
7
+ "View #{id}"
8
+ end
9
+ end
10
+
11
+ app(:new) do |r|
12
+ r.on "provider" do
13
+ r.run a
14
+ end
15
+ end
16
+
17
+ body("/provider/services/101").should == 'View 101'
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe "Roda#env" do
4
+ it "should return the environment" do
5
+ app do |r|
6
+ env['PATH_INFO']
7
+ end
8
+
9
+ body("/foo").should == "/foo"
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe "integration" do
4
+ before do
5
+ @c = Class.new do
6
+ def initialize(app, first, second, &block)
7
+ @app, @first, @second, @block = app, first, second, block
8
+ end
9
+
10
+ def call(env)
11
+ env["m.first"] = @first
12
+ env["m.second"] = @second
13
+ env["m.block"] = @block.call
14
+
15
+ @app.call(env)
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ it "should setup middleware using use " do
22
+ c = @c
23
+ app(:bare) do
24
+ use c, "First", "Second" do
25
+ "Block"
26
+ end
27
+
28
+ route do |r|
29
+ r.get "hello" do
30
+ "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
31
+ end
32
+ end
33
+ end
34
+
35
+ body('/hello').should == 'D First Second Block'
36
+ end
37
+
38
+ it "should inherit middleware in subclass " do
39
+ c = @c
40
+ @app = Class.new(app(:bare){use(c, '1', '2'){"3"}})
41
+ @app.route do |r|
42
+ r.get "hello" do
43
+ "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
44
+ end
45
+ end
46
+
47
+ body('/hello').should == 'D 1 2 3'
48
+ end
49
+
50
+ it "should not have future middleware additions to parent class affect subclass " do
51
+ c = @c
52
+ a = app
53
+ @app = Class.new(a)
54
+ @app.route do |r|
55
+ r.get "hello" do
56
+ "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
57
+ end
58
+ end
59
+ a.use(c, '1', '2'){"3"}
60
+
61
+ body('/hello').should == 'D '
62
+ end
63
+ end
@@ -0,0 +1,658 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe "capturing" do
4
+ it "doesn't yield the verb" do
5
+ app do |r|
6
+ r.get do |*args|
7
+ args.size.to_s
8
+ end
9
+ end
10
+
11
+ body.should == '0'
12
+ end
13
+
14
+ it "doesn't yield the path" do
15
+ app do |r|
16
+ r.get "home" do |*args|
17
+ args.size.to_s
18
+ end
19
+ end
20
+
21
+ body('/home').should == '0'
22
+ end
23
+
24
+ it "yields the segment" do
25
+ app do |r|
26
+ r.get "user", :id do |id|
27
+ id
28
+ end
29
+ end
30
+
31
+ body("/user/johndoe").should == 'johndoe'
32
+ end
33
+
34
+ it "yields a number" do
35
+ app do |r|
36
+ r.get "user", :id do |id|
37
+ id
38
+ end
39
+ end
40
+
41
+ body("/user/101").should == '101'
42
+ end
43
+
44
+ it "yields a segment per nested block" do
45
+ app do |r|
46
+ r.on :one do |one|
47
+ r.on :two do |two|
48
+ r.on :three do |three|
49
+ one + two + three
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ body("/one/two/three").should == "onetwothree"
56
+ end
57
+
58
+ it "regex captures in regex format" do
59
+ app do |r|
60
+ r.get %r{posts/(\d+)-(.*)} do |id, slug|
61
+ id + slug
62
+ end
63
+ end
64
+
65
+ body("/posts/123-postal-service").should == "123postal-service"
66
+ end
67
+ end
68
+
69
+ describe "r.is" do
70
+ it "ensures the patch is matched fully" do
71
+ app do |r|
72
+ r.is "" do
73
+ "+1"
74
+ end
75
+ end
76
+
77
+ body.should == '+1'
78
+ status('//').should == 404
79
+ end
80
+
81
+ it "handles no arguments" do
82
+ app do |r|
83
+ r.on "" do
84
+ r.is do
85
+ "+1"
86
+ end
87
+ end
88
+ end
89
+
90
+ body.should == '+1'
91
+ status('//').should == 404
92
+ end
93
+
94
+ it "matches strings" do
95
+ app do |r|
96
+ r.is "123" do
97
+ "+1"
98
+ end
99
+ end
100
+
101
+ body("/123").should == '+1'
102
+ status("/123/").should == 404
103
+ end
104
+
105
+ it "matches regexps" do
106
+ app do |r|
107
+ r.is /(\w+)/ do |id|
108
+ id
109
+ end
110
+ end
111
+
112
+ body("/123").should == '123'
113
+ status("/123/").should == 404
114
+ end
115
+
116
+ it "matches segments" do
117
+ app do |r|
118
+ r.is :id do |id|
119
+ id
120
+ end
121
+ end
122
+
123
+ body("/123").should == '123'
124
+ status("/123/").should == 404
125
+ end
126
+ end
127
+
128
+ describe "matchers" do
129
+ it "should handle string with embedded param" do
130
+ app do |r|
131
+ r.on "posts/:id" do |id|
132
+ id
133
+ end
134
+ end
135
+
136
+ body('/posts/123').should == '123'
137
+ status('/post/123').should == 404
138
+ end
139
+
140
+ it "should handle multiple params in single string" do
141
+ app do |r|
142
+ r.on "u/:uid/posts/:id" do |uid, id|
143
+ uid + id
144
+ end
145
+ end
146
+
147
+ body("/u/jdoe/posts/123").should == 'jdoe123'
148
+ status("/u/jdoe/pots/123").should == 404
149
+ end
150
+
151
+ it "should escape regexp metacharaters in string" do
152
+ app do |r|
153
+ r.on "u/:uid/posts?/:id" do |uid, id|
154
+ uid + id
155
+ end
156
+ end
157
+
158
+ body("/u/jdoe/posts?/123").should == 'jdoe123'
159
+ status("/u/jdoe/post/123").should == 404
160
+ end
161
+
162
+ it "should handle colons by themselves" do
163
+ app do |r|
164
+ r.on "u/:/:uid/posts/::id" do |uid, id|
165
+ uid + id
166
+ end
167
+ end
168
+
169
+ body("/u/:/jdoe/posts/:123").should == 'jdoe123'
170
+ status("/u/a/jdoe/post/b123").should == 404
171
+ end
172
+
173
+ it "should handle regexes and nesting" do
174
+ app do |r|
175
+ r.on(/u\/(\w+)/) do |uid|
176
+ r.on(/posts\/(\d+)/) do |id|
177
+ uid + id
178
+ end
179
+ end
180
+ end
181
+
182
+ body("/u/jdoe/posts/123").should == 'jdoe123'
183
+ status("/u/jdoe/pots/123").should == 404
184
+ end
185
+
186
+ it "should handle regex nesting colon param style" do
187
+ app do |r|
188
+ r.on(/u:(\w+)/) do |uid|
189
+ r.on(/posts:(\d+)/) do |id|
190
+ uid + id
191
+ end
192
+ end
193
+ end
194
+
195
+ body("/u:jdoe/posts:123").should == 'jdoe123'
196
+ status("/u:jdoe/poss:123").should == 404
197
+ end
198
+
199
+ it "symbol matching" do
200
+ app do |r|
201
+ r.on "user", :id do |uid|
202
+ r.on "posts", :pid do |id|
203
+ uid + id
204
+ end
205
+ end
206
+ end
207
+
208
+ body("/user/jdoe/posts/123").should == 'jdoe123'
209
+ status("/user/jdoe/pots/123").should == 404
210
+ end
211
+
212
+ it "paths and numbers" do
213
+ app do |r|
214
+ r.on "about" do
215
+ r.on :one, :two do |one, two|
216
+ one + two
217
+ end
218
+ end
219
+ end
220
+
221
+ body("/about/1/2").should == '12'
222
+ status("/about/1").should == 404
223
+ end
224
+
225
+ it "paths and decimals" do
226
+ app do |r|
227
+ r.on "about" do
228
+ r.on(/(\d+)/) do |one|
229
+ one
230
+ end
231
+ end
232
+ end
233
+
234
+ body("/about/1").should == '1'
235
+ status("/about/1.2").should == 404
236
+ end
237
+
238
+ it "should allow arrays to match any value" do
239
+ app do |r|
240
+ r.on [/(\d+)/, /\d+(bar)?/] do |id|
241
+ id
242
+ end
243
+ end
244
+
245
+ body('/123').should == '123'
246
+ body('/123bar').should == 'bar'
247
+ status('/123bard').should == 404
248
+ end
249
+
250
+ it "should have array capture match string if match" do
251
+ app do |r|
252
+ r.on %w'p q' do |id|
253
+ id
254
+ end
255
+ end
256
+
257
+ body('/p').should == 'p'
258
+ body('/q').should == 'q'
259
+ status('/r').should == 404
260
+ end
261
+ end
262
+
263
+ describe "r.on" do
264
+ it "executes on no arguments" do
265
+ app do |r|
266
+ r.on do
267
+ "+1"
268
+ end
269
+ end
270
+
271
+ body.should == '+1'
272
+ end
273
+
274
+ it "executes on true" do
275
+ app do |r|
276
+ r.on true do
277
+ "+1"
278
+ end
279
+ end
280
+
281
+ body.should == '+1'
282
+ end
283
+
284
+ it "executes on non-false" do
285
+ app do |r|
286
+ r.on "123" do
287
+ "+1"
288
+ end
289
+ end
290
+
291
+ body("/123").should == '+1'
292
+ end
293
+
294
+ it "ensures SCRIPT_NAME and PATH_INFO are reverted" do
295
+ app do |r|
296
+ r.on lambda { r.env["SCRIPT_NAME"] = "/hello"; false } do
297
+ "Unreachable"
298
+ end
299
+
300
+ r.on do
301
+ r.env["SCRIPT_NAME"] + ':' + r.env["PATH_INFO"]
302
+ end
303
+ end
304
+
305
+ body("/hello").should == ':/hello'
306
+ end
307
+
308
+ it "doesn't mutate SCRIPT_NAME or PATH_INFO after request is returned" do
309
+ app do |r|
310
+ r.on 'login', 'foo' do
311
+ "Unreachable"
312
+ end
313
+
314
+ r.on do
315
+ r.env["SCRIPT_NAME"] + ':' + r.env["PATH_INFO"]
316
+ end
317
+ end
318
+
319
+ pi, sn = '/login', ''
320
+ env = {"REQUEST_METHOD" => "GET", "PATH_INFO" => pi, "SCRIPT_NAME" => sn}
321
+ app.call(env)[2].join.should == ":/login"
322
+ env["PATH_INFO"].should equal(pi)
323
+ env["SCRIPT_NAME"].should equal(sn)
324
+ end
325
+
326
+ it "skips consecutive matches" do
327
+ app do |r|
328
+ r.on do
329
+ "foo"
330
+ end
331
+
332
+ r.on do
333
+ "bar"
334
+ end
335
+ end
336
+
337
+ body.should == "foo"
338
+ end
339
+
340
+ it "finds first match available" do
341
+ app do |r|
342
+ r.on false do
343
+ "foo"
344
+ end
345
+
346
+ r.on do
347
+ "bar"
348
+ end
349
+ end
350
+
351
+ body.should == "bar"
352
+ end
353
+
354
+ it "reverts a half-met matcher" do
355
+ app do |r|
356
+ r.on "post", false do
357
+ "Should be unmet"
358
+ end
359
+
360
+ r.on do
361
+ r.env["SCRIPT_NAME"] + ':' + r.env["PATH_INFO"]
362
+ end
363
+ end
364
+
365
+ body("/hello").should == ':/hello'
366
+ end
367
+
368
+ it "doesn't write to body if body already written to" do
369
+ app do |r|
370
+ r.on do
371
+ response.write "a"
372
+ "b"
373
+ end
374
+ end
375
+
376
+ body.should == 'a'
377
+ end
378
+ end
379
+
380
+ describe "param! matcher" do
381
+ it "should yield a param only if given and not empty" do
382
+ app do |r|
383
+ r.get "signup", :param! => "email" do |email|
384
+ email
385
+ end
386
+
387
+ r.on do
388
+ "No email"
389
+ end
390
+ end
391
+
392
+ io = StringIO.new
393
+ body("/signup", "rack.input" => io, "QUERY_STRING" => "email=john@doe.com").should == 'john@doe.com'
394
+ body("/signup", "rack.input" => io, "QUERY_STRING" => "").should == 'No email'
395
+ body("/signup", "rack.input" => io, "QUERY_STRING" => "email=").should == 'No email'
396
+ end
397
+ end
398
+
399
+ describe "param matcher" do
400
+ it "should yield a param only if given" do
401
+ app do |r|
402
+ r.get "signup", :param=>"email" do |email|
403
+ email
404
+ end
405
+
406
+ r.on do
407
+ "No email"
408
+ end
409
+ end
410
+
411
+ io = StringIO.new
412
+ body("/signup", "rack.input" => io, "QUERY_STRING" => "email=john@doe.com").should == 'john@doe.com'
413
+ body("/signup", "rack.input" => io, "QUERY_STRING" => "").should == 'No email'
414
+ body("/signup", "rack.input" => io, "QUERY_STRING" => "email=").should == ''
415
+ end
416
+ end
417
+
418
+ describe "path matchers" do
419
+ it "one level path" do
420
+ app do |r|
421
+ r.on "about" do
422
+ "About"
423
+ end
424
+ end
425
+
426
+ body('/about').should == "About"
427
+ status("/abot").should == 404
428
+ end
429
+
430
+ it "two level nested paths" do
431
+ app do |r|
432
+ r.on "about" do
433
+ r.on "1" do
434
+ "+1"
435
+ end
436
+
437
+ r.on "2" do
438
+ "+2"
439
+ end
440
+ end
441
+ end
442
+
443
+ body('/about/1').should == "+1"
444
+ body('/about/2').should == "+2"
445
+ status('/about/3').should == 404
446
+ end
447
+
448
+ it "two level inlined paths" do
449
+ app do |r|
450
+ r.on "a/b" do
451
+ "ab"
452
+ end
453
+ end
454
+
455
+ body('/a/b').should == "ab"
456
+ status('/a/d').should == 404
457
+ end
458
+
459
+ it "a path with some regex captures" do
460
+ app do |r|
461
+ r.on /user(\d+)/ do |uid|
462
+ uid
463
+ end
464
+ end
465
+
466
+ body('/user123').should == "123"
467
+ status('/useradf').should == 404
468
+ end
469
+
470
+ it "matching the root" do
471
+ app do |r|
472
+ r.on "" do
473
+ "Home"
474
+ end
475
+ end
476
+
477
+ body.should == 'Home'
478
+ status("/foo").should == 404
479
+ end
480
+ end
481
+
482
+ describe "root/empty segment matching" do
483
+ it "matching an empty segment" do
484
+ app do |r|
485
+ r.on "" do
486
+ r.path
487
+ end
488
+ end
489
+
490
+ body.should == '/'
491
+ status("/foo").should == 404
492
+ end
493
+
494
+ it "nested empty segments" do
495
+ app do |r|
496
+ r.on "" do
497
+ r.on "" do
498
+ r.on "1" do
499
+ r.path
500
+ end
501
+ end
502
+ end
503
+ end
504
+
505
+ body("///1").should == '///1'
506
+ status("/1").should == 404
507
+ status("//1").should == 404
508
+ end
509
+
510
+ it "/events/? scenario" do
511
+ a = app do |r|
512
+ r.on "" do
513
+ "Hooray"
514
+ end
515
+
516
+ r.is do
517
+ "Foo"
518
+ end
519
+ end
520
+
521
+ app(:new) do |r|
522
+ r.on "events" do
523
+ r.run a
524
+ end
525
+ end
526
+
527
+ body("/events").should == 'Foo'
528
+ body("/events/").should == 'Hooray'
529
+ status("/events/foo").should == 404
530
+ end
531
+ end
532
+
533
+ describe "segment handling" do
534
+ before do
535
+ app do |r|
536
+ r.on "post" do
537
+ r.on :id do |id|
538
+ id
539
+ end
540
+ end
541
+ end
542
+ end
543
+
544
+ it "matches numeric ids" do
545
+ body('/post/1').should == '1'
546
+ end
547
+
548
+ it "matches decimal numbers" do
549
+ body('/post/1.1').should == '1.1'
550
+ end
551
+
552
+ it "matches slugs" do
553
+ body('/post/my-blog-post-about-cuba').should == 'my-blog-post-about-cuba'
554
+ end
555
+
556
+ it "matches only the first segment available" do
557
+ body('/post/one/two/three').should == 'one'
558
+ end
559
+ end
560
+
561
+ describe "request verb methods" do
562
+ it "executes if verb matches" do
563
+ app do |r|
564
+ r.get do
565
+ "g"
566
+ end
567
+ r.post do
568
+ "p"
569
+ end
570
+ end
571
+
572
+ body.should == 'g'
573
+ body('REQUEST_METHOD'=>'POST').should == 'p'
574
+ end
575
+
576
+ it "requires exact match if given arguments" do
577
+ app do |r|
578
+ r.get "" do
579
+ "g"
580
+ end
581
+ r.post "" do
582
+ "p"
583
+ end
584
+ end
585
+
586
+ body.should == 'g'
587
+ body('REQUEST_METHOD'=>'POST').should == 'p'
588
+ status("/a").should == 404
589
+ status("/a", 'REQUEST_METHOD'=>'POST').should == 404
590
+ end
591
+
592
+ it "does not require exact match if given arguments" do
593
+ app do |r|
594
+ r.get do
595
+ r.is "" do
596
+ "g"
597
+ end
598
+
599
+ "get"
600
+ end
601
+ r.post do
602
+ r.is "" do
603
+ "p"
604
+ end
605
+
606
+ "post"
607
+ end
608
+ end
609
+
610
+ body.should == 'g'
611
+ body('REQUEST_METHOD'=>'POST').should == 'p'
612
+ body("/a").should == 'get'
613
+ body("/a", 'REQUEST_METHOD'=>'POST').should == 'post'
614
+ end
615
+ end
616
+
617
+ describe "extension matcher" do
618
+ it "should match given file extensions" do
619
+ app do |r|
620
+ r.on "styles" do
621
+ r.on :extension=>"css" do |file|
622
+ file
623
+ end
624
+ end
625
+ end
626
+
627
+ body("/styles/reset.css").should == 'reset'
628
+ status("/styles/reset.bar").should == 404
629
+ end
630
+ end
631
+
632
+ describe "method matcher" do
633
+ it "should match given request types" do
634
+ app do |r|
635
+ r.is "", :method=>:get do
636
+ "foo"
637
+ end
638
+ r.is "", :method=>[:patch, :post] do
639
+ "bar"
640
+ end
641
+ end
642
+
643
+ body("REQUEST_METHOD"=>"GET").should == 'foo'
644
+ body("REQUEST_METHOD"=>"PATCH").should == 'bar'
645
+ body("REQUEST_METHOD"=>"POST").should == 'bar'
646
+ status("REQUEST_METHOD"=>"DELETE").should == 404
647
+ end
648
+ end
649
+
650
+ describe "route block that returns string" do
651
+ it "should be treated as if an on block returned string" do
652
+ app do |r|
653
+ "+1"
654
+ end
655
+
656
+ body.should == '+1'
657
+ end
658
+ end