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.
- checksums.yaml +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +709 -0
- data/Rakefile +124 -0
- data/lib/roda.rb +608 -0
- data/lib/roda/plugins/all_verbs.rb +48 -0
- data/lib/roda/plugins/default_headers.rb +50 -0
- data/lib/roda/plugins/error_handler.rb +69 -0
- data/lib/roda/plugins/flash.rb +62 -0
- data/lib/roda/plugins/h.rb +24 -0
- data/lib/roda/plugins/halt.rb +79 -0
- data/lib/roda/plugins/header_matchers.rb +57 -0
- data/lib/roda/plugins/hooks.rb +106 -0
- data/lib/roda/plugins/indifferent_params.rb +47 -0
- data/lib/roda/plugins/middleware.rb +88 -0
- data/lib/roda/plugins/multi_route.rb +77 -0
- data/lib/roda/plugins/not_found.rb +62 -0
- data/lib/roda/plugins/pass.rb +34 -0
- data/lib/roda/plugins/render.rb +217 -0
- data/lib/roda/plugins/streaming.rb +165 -0
- data/spec/composition_spec.rb +19 -0
- data/spec/env_spec.rb +11 -0
- data/spec/integration_spec.rb +63 -0
- data/spec/matchers_spec.rb +658 -0
- data/spec/module_spec.rb +29 -0
- data/spec/opts_spec.rb +42 -0
- data/spec/plugin/all_verbs_spec.rb +29 -0
- data/spec/plugin/default_headers_spec.rb +63 -0
- data/spec/plugin/error_handler_spec.rb +67 -0
- data/spec/plugin/flash_spec.rb +59 -0
- data/spec/plugin/h_spec.rb +13 -0
- data/spec/plugin/halt_spec.rb +62 -0
- data/spec/plugin/header_matchers_spec.rb +61 -0
- data/spec/plugin/hooks_spec.rb +97 -0
- data/spec/plugin/indifferent_params_spec.rb +13 -0
- data/spec/plugin/middleware_spec.rb +52 -0
- data/spec/plugin/multi_route_spec.rb +98 -0
- data/spec/plugin/not_found_spec.rb +99 -0
- data/spec/plugin/pass_spec.rb +23 -0
- data/spec/plugin/render_spec.rb +148 -0
- data/spec/plugin/streaming_spec.rb +52 -0
- data/spec/plugin_spec.rb +61 -0
- data/spec/redirect_spec.rb +24 -0
- data/spec/request_spec.rb +55 -0
- data/spec/response_spec.rb +131 -0
- data/spec/session_spec.rb +35 -0
- data/spec/spec_helper.rb +89 -0
- data/spec/version_spec.rb +8 -0
- 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
|
data/spec/env_spec.rb
ADDED
@@ -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
|