roda 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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