sinatra-resources 0.1.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.
- data/README.rdoc +116 -0
- data/lib/sinatra/resources.rb +46 -0
- data/test/contest.rb +64 -0
- data/test/helper.rb +82 -0
- data/test/routing_test.rb +973 -0
- data/test/test_app.rb +44 -0
- metadata +70 -0
data/README.rdoc
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
= Sinatra::Resources - Simple nested resources for Sinatra
|
2
|
+
|
3
|
+
Ever wished you could do this in Sinatra?
|
4
|
+
|
5
|
+
resource 'posts' do
|
6
|
+
get do
|
7
|
+
# show all posts
|
8
|
+
end
|
9
|
+
|
10
|
+
post do
|
11
|
+
# create new post
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
resource 'posts/:id' do
|
16
|
+
get do
|
17
|
+
# show post params[:id]
|
18
|
+
end
|
19
|
+
|
20
|
+
delete do
|
21
|
+
# destroy post params[:id]
|
22
|
+
end
|
23
|
+
|
24
|
+
get 'comments' do
|
25
|
+
# show this post's comments
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Now you can.
|
30
|
+
|
31
|
+
This was inspired by {Sinatra ticket #31}[https://sinatra.lighthouseapp.com/projects/9779/tickets/31-nested-resources],
|
32
|
+
which many people want but hasn't gotten traction to make it into Sinatra core. If you want it in Sinatra,
|
33
|
+
pipe up on the ticket!
|
34
|
+
|
35
|
+
== Installation
|
36
|
+
|
37
|
+
Install +gemcutter+ if you don't have it:
|
38
|
+
|
39
|
+
sudo gem install gemcutter
|
40
|
+
sudo gem tumble
|
41
|
+
|
42
|
+
Then just install this gem:
|
43
|
+
|
44
|
+
sudo gem install sinatra-resources
|
45
|
+
|
46
|
+
If you are using a classic (one-file) Sinatra app, just add:
|
47
|
+
|
48
|
+
require 'sinatra/resources'
|
49
|
+
|
50
|
+
If you are using a modular Sinatra::Base app, you must also add:
|
51
|
+
|
52
|
+
register Sinatra::Resources
|
53
|
+
|
54
|
+
To the top of your application class.
|
55
|
+
|
56
|
+
== Examples
|
57
|
+
|
58
|
+
Resources can be arbitrarily nested, and can be either string paths or symbols. There is also the
|
59
|
+
shortcut +member+ which just maps to "resource ':id'". So you could also write the above example as:
|
60
|
+
|
61
|
+
resource :posts do
|
62
|
+
get do
|
63
|
+
# show all posts
|
64
|
+
end
|
65
|
+
|
66
|
+
post do
|
67
|
+
# create new post
|
68
|
+
end
|
69
|
+
|
70
|
+
member do
|
71
|
+
get do
|
72
|
+
# show post params[:id]
|
73
|
+
end
|
74
|
+
|
75
|
+
delete do
|
76
|
+
# destroy post params[:id]
|
77
|
+
end
|
78
|
+
|
79
|
+
get :comments do
|
80
|
+
# show this post's comments
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Or you can extract the "id" parameter as well:
|
86
|
+
|
87
|
+
resource :posts do
|
88
|
+
get do
|
89
|
+
# show all posts
|
90
|
+
end
|
91
|
+
|
92
|
+
post do
|
93
|
+
# create new post
|
94
|
+
end
|
95
|
+
|
96
|
+
member do
|
97
|
+
get do |id|
|
98
|
+
# show post ID=id
|
99
|
+
end
|
100
|
+
|
101
|
+
delete do |id|
|
102
|
+
# destroy post ID=id
|
103
|
+
end
|
104
|
+
|
105
|
+
get :comments do |id|
|
106
|
+
# show this post's comments
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
Whatever blows your hair back.
|
112
|
+
|
113
|
+
== Author
|
114
|
+
|
115
|
+
Copyright (c) 2010 {Nate Wiger}[http://nate.wiger.org]. All Rights Reserved.
|
116
|
+
Released under the {Artistic License}[http://www.opensource.org/licenses/artistic-license-2.0.php].
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Resources
|
3
|
+
def self.registered(app)
|
4
|
+
[:get, :post, :put, :delete].each do |meth|
|
5
|
+
# http://whynotwiki.com/Ruby_/_Method_aliasing_and_chaining#Can_you_alias_class_methods.3F
|
6
|
+
app.class_eval <<-EndAlias
|
7
|
+
class << self
|
8
|
+
alias_method :#{meth}_without_resource, :#{meth}
|
9
|
+
alias_method :#{meth}, :#{meth}_with_resource
|
10
|
+
end
|
11
|
+
EndAlias
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
[:get, :post, :put, :delete].each do |meth|
|
16
|
+
class_eval <<-EndMeth
|
17
|
+
def #{meth}_with_resource(path=nil, options={}, &block)
|
18
|
+
#{meth}_without_resource(make_path(path), options, &block)
|
19
|
+
end
|
20
|
+
EndMeth
|
21
|
+
end
|
22
|
+
|
23
|
+
# Define a new resource block. Resources can be nested of arbitrary depth.
|
24
|
+
def resource(path, &block)
|
25
|
+
raise "Resource path cannot be nil" if path.nil?
|
26
|
+
(@path_parts ||= []) << path
|
27
|
+
block.call
|
28
|
+
@path_parts.pop
|
29
|
+
end
|
30
|
+
|
31
|
+
# Shortcut for "resource ':id'".
|
32
|
+
def member(&block)
|
33
|
+
raise "Nested member do..end must be within resource do..end" if @path_parts.nil? || @path_parts.empty?
|
34
|
+
resource(':id', &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def make_path(path)
|
38
|
+
return path if path.is_a?(Regexp) || @path_parts.nil? || @path_parts.empty?
|
39
|
+
route = @path_parts.join('/')
|
40
|
+
route += '/' + path if path
|
41
|
+
'/' + route.squeeze('/')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
register Resources
|
46
|
+
end
|
data/test/contest.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
|
3
|
+
# Test::Unit loads a default test if the suite is empty, and the only
|
4
|
+
# purpose of that test is to fail. As having empty contexts is a common
|
5
|
+
# practice, we decided to overwrite TestSuite#empty? in order to
|
6
|
+
# allow them. Having a failure when no tests have been defined seems
|
7
|
+
# counter-intuitive.
|
8
|
+
class Test::Unit::TestSuite
|
9
|
+
unless method_defined?(:empty?)
|
10
|
+
def empty?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# We added setup, test and context as class methods, and the instance
|
17
|
+
# method setup now iterates on the setup blocks. Note that all setup
|
18
|
+
# blocks must be defined with the block syntax. Adding a setup instance
|
19
|
+
# method defeats the purpose of this library.
|
20
|
+
class Test::Unit::TestCase
|
21
|
+
def self.setup(&block)
|
22
|
+
setup_blocks << block
|
23
|
+
end
|
24
|
+
|
25
|
+
def setup
|
26
|
+
self.class.setup_blocks.each do |block|
|
27
|
+
instance_eval(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.context(name, &block)
|
32
|
+
subclass = Class.new(self.superclass)
|
33
|
+
subclass.setup_blocks.unshift(*setup_blocks)
|
34
|
+
subclass.class_eval(&block)
|
35
|
+
const_set(context_name(name), subclass)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.test(name, &block)
|
39
|
+
define_method(test_name(name), &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
alias_method :should, :test
|
44
|
+
alias_method :describe, :context
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def self.setup_blocks
|
50
|
+
@setup_blocks ||= []
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.context_name(name)
|
54
|
+
"Test#{sanitize_name(name).gsub(/(^| )(\w)/) { $2.upcase }}".to_sym
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.test_name(name)
|
58
|
+
"test_#{sanitize_name(name).gsub(/\s+/,'_')}".to_sym
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.sanitize_name(name)
|
62
|
+
name.gsub(/\W+/, ' ').strip
|
63
|
+
end
|
64
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rack'
|
5
|
+
rescue LoadError
|
6
|
+
require 'rubygems'
|
7
|
+
require 'rack'
|
8
|
+
end
|
9
|
+
|
10
|
+
testdir = File.dirname(__FILE__)
|
11
|
+
$LOAD_PATH.unshift testdir unless $LOAD_PATH.include?(testdir)
|
12
|
+
|
13
|
+
libdir = File.dirname(File.dirname(__FILE__)) + '/lib'
|
14
|
+
$LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir)
|
15
|
+
|
16
|
+
require 'contest'
|
17
|
+
require 'rack/test'
|
18
|
+
require 'sinatra/base'
|
19
|
+
require 'sinatra/resources'
|
20
|
+
|
21
|
+
class Sinatra::Base
|
22
|
+
# Allow assertions in request context
|
23
|
+
include Test::Unit::Assertions
|
24
|
+
end
|
25
|
+
|
26
|
+
# App that includes resource/member helpers
|
27
|
+
class ResourceApp < Sinatra::Base
|
28
|
+
register Sinatra::Resources
|
29
|
+
end
|
30
|
+
|
31
|
+
Sinatra::Base.set :environment, :test
|
32
|
+
|
33
|
+
class Test::Unit::TestCase
|
34
|
+
include Rack::Test::Methods
|
35
|
+
|
36
|
+
class << self
|
37
|
+
alias_method :it, :test
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :response, :last_response
|
41
|
+
|
42
|
+
setup do
|
43
|
+
Sinatra::Base.set :environment, :test
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sets up a Sinatra::Base subclass defined with the block
|
47
|
+
# given. Used in setup or individual spec methods to establish
|
48
|
+
# the application.
|
49
|
+
def mock_app(base=ResourceApp, &block)
|
50
|
+
@app = Sinatra.new(base, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def app
|
54
|
+
Rack::Lint.new(@app)
|
55
|
+
end
|
56
|
+
|
57
|
+
def body
|
58
|
+
response.body.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
# Delegate other missing methods to response.
|
62
|
+
def method_missing(name, *args, &block)
|
63
|
+
if response && response.respond_to?(name)
|
64
|
+
response.send(name, *args, &block)
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Also check response since we delegate there.
|
71
|
+
def respond_to?(symbol, include_private=false)
|
72
|
+
super || (response && response.respond_to?(symbol, include_private))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Do not output warnings for the duration of the block.
|
76
|
+
def silence_warnings
|
77
|
+
$VERBOSE, v = nil, $VERBOSE
|
78
|
+
yield
|
79
|
+
ensure
|
80
|
+
$VERBOSE = v
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,973 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
|
3
|
+
# Helper method for easy route pattern matching testing
|
4
|
+
def route_def(pattern)
|
5
|
+
mock_app { get(pattern) { } }
|
6
|
+
end
|
7
|
+
|
8
|
+
class RegexpLookAlike
|
9
|
+
class MatchData
|
10
|
+
def captures
|
11
|
+
["this", "is", "a", "test"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def match(string)
|
16
|
+
::RegexpLookAlike::MatchData.new if string == "/this/is/a/test/"
|
17
|
+
end
|
18
|
+
|
19
|
+
def keys
|
20
|
+
["one", "two", "three", "four"]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class RoutingTest < Test::Unit::TestCase
|
25
|
+
%w[get put post delete].each do |verb|
|
26
|
+
it "defines #{verb.upcase} request handlers with #{verb}" do
|
27
|
+
mock_app {
|
28
|
+
send verb, '/hello' do
|
29
|
+
'Hello World'
|
30
|
+
end
|
31
|
+
}
|
32
|
+
|
33
|
+
request = Rack::MockRequest.new(@app)
|
34
|
+
response = request.request(verb.upcase, '/hello', {})
|
35
|
+
assert response.ok?
|
36
|
+
assert_equal 'Hello World', response.body
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "defines HEAD request handlers with HEAD" do
|
41
|
+
mock_app {
|
42
|
+
head '/hello' do
|
43
|
+
response['X-Hello'] = 'World!'
|
44
|
+
'remove me'
|
45
|
+
end
|
46
|
+
}
|
47
|
+
|
48
|
+
request = Rack::MockRequest.new(@app)
|
49
|
+
response = request.request('HEAD', '/hello', {})
|
50
|
+
assert response.ok?
|
51
|
+
assert_equal 'World!', response['X-Hello']
|
52
|
+
assert_equal '', response.body
|
53
|
+
end
|
54
|
+
|
55
|
+
it "404s when no route satisfies the request" do
|
56
|
+
mock_app {
|
57
|
+
get('/foo') { }
|
58
|
+
}
|
59
|
+
get '/bar'
|
60
|
+
assert_equal 404, status
|
61
|
+
end
|
62
|
+
|
63
|
+
# Not valid for older Sinatras
|
64
|
+
# it "404s and sets X-Cascade header when no route satisfies the request" do
|
65
|
+
# mock_app {
|
66
|
+
# get('/foo') { }
|
67
|
+
# }
|
68
|
+
# get '/bar'
|
69
|
+
# assert_equal 404, status
|
70
|
+
# assert_equal 'pass', response.headers['X-Cascade']
|
71
|
+
# end
|
72
|
+
|
73
|
+
it "overrides the content-type in error handlers" do
|
74
|
+
mock_app {
|
75
|
+
before { content_type 'text/plain' }
|
76
|
+
error Sinatra::NotFound do
|
77
|
+
content_type "text/html"
|
78
|
+
"<h1>Not Found</h1>"
|
79
|
+
end
|
80
|
+
}
|
81
|
+
|
82
|
+
get '/foo'
|
83
|
+
assert_equal 404, status
|
84
|
+
assert_equal 'text/html', response["Content-Type"]
|
85
|
+
assert_equal "<h1>Not Found</h1>", response.body
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'takes multiple definitions of a route' do
|
89
|
+
mock_app {
|
90
|
+
user_agent(/Foo/)
|
91
|
+
get '/foo' do
|
92
|
+
'foo'
|
93
|
+
end
|
94
|
+
|
95
|
+
get '/foo' do
|
96
|
+
'not foo'
|
97
|
+
end
|
98
|
+
}
|
99
|
+
|
100
|
+
get '/foo', {}, 'HTTP_USER_AGENT' => 'Foo'
|
101
|
+
assert ok?
|
102
|
+
assert_equal 'foo', body
|
103
|
+
|
104
|
+
get '/foo'
|
105
|
+
assert ok?
|
106
|
+
assert_equal 'not foo', body
|
107
|
+
end
|
108
|
+
|
109
|
+
it "exposes params with indifferent hash" do
|
110
|
+
mock_app {
|
111
|
+
get '/:foo' do
|
112
|
+
assert_equal 'bar', params['foo']
|
113
|
+
assert_equal 'bar', params[:foo]
|
114
|
+
'well, alright'
|
115
|
+
end
|
116
|
+
}
|
117
|
+
get '/bar'
|
118
|
+
assert_equal 'well, alright', body
|
119
|
+
end
|
120
|
+
|
121
|
+
it "merges named params and query string params in params" do
|
122
|
+
mock_app {
|
123
|
+
get '/:foo' do
|
124
|
+
assert_equal 'bar', params['foo']
|
125
|
+
assert_equal 'biz', params['baz']
|
126
|
+
end
|
127
|
+
}
|
128
|
+
get '/bar?baz=biz'
|
129
|
+
assert ok?
|
130
|
+
end
|
131
|
+
|
132
|
+
it "supports named params like /hello/:person" do
|
133
|
+
mock_app {
|
134
|
+
get '/hello/:person' do
|
135
|
+
"Hello #{params['person']}"
|
136
|
+
end
|
137
|
+
}
|
138
|
+
get '/hello/Frank'
|
139
|
+
assert_equal 'Hello Frank', body
|
140
|
+
end
|
141
|
+
|
142
|
+
it "supports optional named params like /?:foo?/?:bar?" do
|
143
|
+
mock_app {
|
144
|
+
get '/?:foo?/?:bar?' do
|
145
|
+
"foo=#{params[:foo]};bar=#{params[:bar]}"
|
146
|
+
end
|
147
|
+
}
|
148
|
+
|
149
|
+
get '/hello/world'
|
150
|
+
assert ok?
|
151
|
+
assert_equal "foo=hello;bar=world", body
|
152
|
+
|
153
|
+
get '/hello'
|
154
|
+
assert ok?
|
155
|
+
assert_equal "foo=hello;bar=", body
|
156
|
+
|
157
|
+
get '/'
|
158
|
+
assert ok?
|
159
|
+
assert_equal "foo=;bar=", body
|
160
|
+
end
|
161
|
+
|
162
|
+
it "supports single splat params like /*" do
|
163
|
+
mock_app {
|
164
|
+
get '/*' do
|
165
|
+
assert params['splat'].kind_of?(Array)
|
166
|
+
params['splat'].join "\n"
|
167
|
+
end
|
168
|
+
}
|
169
|
+
|
170
|
+
get '/foo'
|
171
|
+
assert_equal "foo", body
|
172
|
+
|
173
|
+
get '/foo/bar/baz'
|
174
|
+
assert_equal "foo/bar/baz", body
|
175
|
+
end
|
176
|
+
|
177
|
+
it "supports mixing multiple splat params like /*/foo/*/*" do
|
178
|
+
mock_app {
|
179
|
+
get '/*/foo/*/*' do
|
180
|
+
assert params['splat'].kind_of?(Array)
|
181
|
+
params['splat'].join "\n"
|
182
|
+
end
|
183
|
+
}
|
184
|
+
|
185
|
+
get '/bar/foo/bling/baz/boom'
|
186
|
+
assert_equal "bar\nbling\nbaz/boom", body
|
187
|
+
|
188
|
+
get '/bar/foo/baz'
|
189
|
+
assert not_found?
|
190
|
+
end
|
191
|
+
|
192
|
+
it "supports mixing named and splat params like /:foo/*" do
|
193
|
+
mock_app {
|
194
|
+
get '/:foo/*' do
|
195
|
+
assert_equal 'foo', params['foo']
|
196
|
+
assert_equal ['bar/baz'], params['splat']
|
197
|
+
end
|
198
|
+
}
|
199
|
+
|
200
|
+
get '/foo/bar/baz'
|
201
|
+
assert ok?
|
202
|
+
end
|
203
|
+
|
204
|
+
it "matches a dot ('.') as part of a named param" do
|
205
|
+
mock_app {
|
206
|
+
get '/:foo/:bar' do
|
207
|
+
params[:foo]
|
208
|
+
end
|
209
|
+
}
|
210
|
+
|
211
|
+
get '/user@example.com/name'
|
212
|
+
assert_equal 200, response.status
|
213
|
+
assert_equal 'user@example.com', body
|
214
|
+
end
|
215
|
+
|
216
|
+
it "matches a literal dot ('.') outside of named params" do
|
217
|
+
mock_app {
|
218
|
+
get '/:file.:ext' do
|
219
|
+
assert_equal 'pony', params[:file]
|
220
|
+
assert_equal 'jpg', params[:ext]
|
221
|
+
'right on'
|
222
|
+
end
|
223
|
+
}
|
224
|
+
|
225
|
+
get '/pony.jpg'
|
226
|
+
assert_equal 200, response.status
|
227
|
+
assert_equal 'right on', body
|
228
|
+
end
|
229
|
+
|
230
|
+
it "literally matches . in paths" do
|
231
|
+
route_def '/test.bar'
|
232
|
+
|
233
|
+
get '/test.bar'
|
234
|
+
assert ok?
|
235
|
+
get 'test0bar'
|
236
|
+
assert not_found?
|
237
|
+
end
|
238
|
+
|
239
|
+
it "literally matches $ in paths" do
|
240
|
+
route_def '/test$/'
|
241
|
+
|
242
|
+
get '/test$/'
|
243
|
+
assert ok?
|
244
|
+
end
|
245
|
+
|
246
|
+
it "literally matches + in paths" do
|
247
|
+
route_def '/te+st/'
|
248
|
+
|
249
|
+
get '/te%2Bst/'
|
250
|
+
assert ok?
|
251
|
+
get '/teeeeeeest/'
|
252
|
+
assert not_found?
|
253
|
+
end
|
254
|
+
|
255
|
+
it "literally matches () in paths" do
|
256
|
+
route_def '/test(bar)/'
|
257
|
+
|
258
|
+
get '/test(bar)/'
|
259
|
+
assert ok?
|
260
|
+
end
|
261
|
+
|
262
|
+
it "supports basic nested params" do
|
263
|
+
mock_app {
|
264
|
+
get '/hi' do
|
265
|
+
params["person"]["name"]
|
266
|
+
end
|
267
|
+
}
|
268
|
+
|
269
|
+
get "/hi?person[name]=John+Doe"
|
270
|
+
assert ok?
|
271
|
+
assert_equal "John Doe", body
|
272
|
+
end
|
273
|
+
|
274
|
+
it "exposes nested params with indifferent hash" do
|
275
|
+
mock_app {
|
276
|
+
get '/testme' do
|
277
|
+
assert_equal 'baz', params['bar']['foo']
|
278
|
+
assert_equal 'baz', params['bar'][:foo]
|
279
|
+
'well, alright'
|
280
|
+
end
|
281
|
+
}
|
282
|
+
get '/testme?bar[foo]=baz'
|
283
|
+
assert_equal 'well, alright', body
|
284
|
+
end
|
285
|
+
|
286
|
+
it "supports deeply nested params" do
|
287
|
+
expected_params = {
|
288
|
+
"emacs" => {
|
289
|
+
"map" => { "goto-line" => "M-g g" },
|
290
|
+
"version" => "22.3.1"
|
291
|
+
},
|
292
|
+
"browser" => {
|
293
|
+
"firefox" => {"engine" => {"name"=>"spidermonkey", "version"=>"1.7.0"}},
|
294
|
+
"chrome" => {"engine" => {"name"=>"V8", "version"=>"1.0"}}
|
295
|
+
},
|
296
|
+
"paste" => {"name"=>"hello world", "syntax"=>"ruby"}
|
297
|
+
}
|
298
|
+
mock_app {
|
299
|
+
get '/foo' do
|
300
|
+
assert_equal expected_params, params
|
301
|
+
'looks good'
|
302
|
+
end
|
303
|
+
}
|
304
|
+
get '/foo', expected_params
|
305
|
+
assert ok?
|
306
|
+
assert_equal 'looks good', body
|
307
|
+
end
|
308
|
+
|
309
|
+
it "preserves non-nested params" do
|
310
|
+
mock_app {
|
311
|
+
get '/foo' do
|
312
|
+
assert_equal "2", params["article_id"]
|
313
|
+
assert_equal "awesome", params['comment']['body']
|
314
|
+
assert_nil params['comment[body]']
|
315
|
+
'looks good'
|
316
|
+
end
|
317
|
+
}
|
318
|
+
|
319
|
+
get '/foo?article_id=2&comment[body]=awesome'
|
320
|
+
assert ok?
|
321
|
+
assert_equal 'looks good', body
|
322
|
+
end
|
323
|
+
|
324
|
+
it "matches paths that include spaces encoded with %20" do
|
325
|
+
mock_app {
|
326
|
+
get '/path with spaces' do
|
327
|
+
'looks good'
|
328
|
+
end
|
329
|
+
}
|
330
|
+
|
331
|
+
get '/path%20with%20spaces'
|
332
|
+
assert ok?
|
333
|
+
assert_equal 'looks good', body
|
334
|
+
end
|
335
|
+
|
336
|
+
it "matches paths that include spaces encoded with +" do
|
337
|
+
mock_app {
|
338
|
+
get '/path with spaces' do
|
339
|
+
'looks good'
|
340
|
+
end
|
341
|
+
}
|
342
|
+
|
343
|
+
get '/path+with+spaces'
|
344
|
+
assert ok?
|
345
|
+
assert_equal 'looks good', body
|
346
|
+
end
|
347
|
+
|
348
|
+
it "URL decodes named parameters and splats" do
|
349
|
+
mock_app {
|
350
|
+
get '/:foo/*' do
|
351
|
+
assert_equal 'hello world', params['foo']
|
352
|
+
assert_equal ['how are you'], params['splat']
|
353
|
+
nil
|
354
|
+
end
|
355
|
+
}
|
356
|
+
|
357
|
+
get '/hello%20world/how%20are%20you'
|
358
|
+
assert ok?
|
359
|
+
end
|
360
|
+
|
361
|
+
it 'supports regular expressions' do
|
362
|
+
mock_app {
|
363
|
+
get(/^\/foo...\/bar$/) do
|
364
|
+
'Hello World'
|
365
|
+
end
|
366
|
+
}
|
367
|
+
|
368
|
+
get '/foooom/bar'
|
369
|
+
assert ok?
|
370
|
+
assert_equal 'Hello World', body
|
371
|
+
end
|
372
|
+
|
373
|
+
it 'makes regular expression captures available in params[:captures]' do
|
374
|
+
mock_app {
|
375
|
+
get(/^\/fo(.*)\/ba(.*)/) do
|
376
|
+
assert_equal ['orooomma', 'f'], params[:captures]
|
377
|
+
'right on'
|
378
|
+
end
|
379
|
+
}
|
380
|
+
|
381
|
+
get '/foorooomma/baf'
|
382
|
+
assert ok?
|
383
|
+
assert_equal 'right on', body
|
384
|
+
end
|
385
|
+
|
386
|
+
it 'supports regular expression look-alike routes' do
|
387
|
+
mock_app {
|
388
|
+
get(RegexpLookAlike.new) do
|
389
|
+
assert_equal 'this', params[:one]
|
390
|
+
assert_equal 'is', params[:two]
|
391
|
+
assert_equal 'a', params[:three]
|
392
|
+
assert_equal 'test', params[:four]
|
393
|
+
'right on'
|
394
|
+
end
|
395
|
+
}
|
396
|
+
|
397
|
+
get '/this/is/a/test/'
|
398
|
+
assert ok?
|
399
|
+
assert_equal 'right on', body
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'raises a TypeError when pattern is not a String or Regexp' do
|
403
|
+
assert_raise(TypeError) {
|
404
|
+
mock_app { get(42){} }
|
405
|
+
}
|
406
|
+
end
|
407
|
+
|
408
|
+
it "returns response immediately on halt" do
|
409
|
+
mock_app {
|
410
|
+
get '/' do
|
411
|
+
halt 'Hello World'
|
412
|
+
'Boo-hoo World'
|
413
|
+
end
|
414
|
+
}
|
415
|
+
|
416
|
+
get '/'
|
417
|
+
assert ok?
|
418
|
+
assert_equal 'Hello World', body
|
419
|
+
end
|
420
|
+
|
421
|
+
it "halts with a response tuple" do
|
422
|
+
mock_app {
|
423
|
+
get '/' do
|
424
|
+
halt 295, {'Content-Type' => 'text/plain'}, 'Hello World'
|
425
|
+
end
|
426
|
+
}
|
427
|
+
|
428
|
+
get '/'
|
429
|
+
assert_equal 295, status
|
430
|
+
assert_equal 'text/plain', response['Content-Type']
|
431
|
+
assert_equal 'Hello World', body
|
432
|
+
end
|
433
|
+
|
434
|
+
it "halts with an array of strings" do
|
435
|
+
mock_app {
|
436
|
+
get '/' do
|
437
|
+
halt %w[Hello World How Are You]
|
438
|
+
end
|
439
|
+
}
|
440
|
+
|
441
|
+
get '/'
|
442
|
+
assert_equal 'HelloWorldHowAreYou', body
|
443
|
+
end
|
444
|
+
|
445
|
+
it "transitions to the next matching route on pass" do
|
446
|
+
mock_app {
|
447
|
+
get '/:foo' do
|
448
|
+
pass
|
449
|
+
'Hello Foo'
|
450
|
+
end
|
451
|
+
|
452
|
+
get '/*' do
|
453
|
+
assert !params.include?('foo')
|
454
|
+
'Hello World'
|
455
|
+
end
|
456
|
+
}
|
457
|
+
|
458
|
+
get '/bar'
|
459
|
+
assert ok?
|
460
|
+
assert_equal 'Hello World', body
|
461
|
+
end
|
462
|
+
|
463
|
+
it "transitions to 404 when passed and no subsequent route matches" do
|
464
|
+
mock_app {
|
465
|
+
get '/:foo' do
|
466
|
+
pass
|
467
|
+
'Hello Foo'
|
468
|
+
end
|
469
|
+
}
|
470
|
+
|
471
|
+
get '/bar'
|
472
|
+
assert not_found?
|
473
|
+
end
|
474
|
+
|
475
|
+
# it "transitions to 404 and sets X-Cascade header when passed and no subsequent route matches" do
|
476
|
+
# mock_app {
|
477
|
+
# get '/:foo' do
|
478
|
+
# pass
|
479
|
+
# 'Hello Foo'
|
480
|
+
# end
|
481
|
+
#
|
482
|
+
# get '/bar' do
|
483
|
+
# 'Hello Bar'
|
484
|
+
# end
|
485
|
+
# }
|
486
|
+
#
|
487
|
+
# get '/foo'
|
488
|
+
# assert not_found?
|
489
|
+
# assert_equal 'pass', response.headers['X-Cascade']
|
490
|
+
# end
|
491
|
+
#
|
492
|
+
# it "uses optional block passed to pass as route block if no other route is found" do
|
493
|
+
# mock_app {
|
494
|
+
# get "/" do
|
495
|
+
# pass do
|
496
|
+
# "this"
|
497
|
+
# end
|
498
|
+
# "not this"
|
499
|
+
# end
|
500
|
+
# }
|
501
|
+
#
|
502
|
+
# get "/"
|
503
|
+
# assert ok?
|
504
|
+
# assert "this", body
|
505
|
+
# end
|
506
|
+
|
507
|
+
it "passes when matching condition returns false" do
|
508
|
+
mock_app {
|
509
|
+
condition { params[:foo] == 'bar' }
|
510
|
+
get '/:foo' do
|
511
|
+
'Hello World'
|
512
|
+
end
|
513
|
+
}
|
514
|
+
|
515
|
+
get '/bar'
|
516
|
+
assert ok?
|
517
|
+
assert_equal 'Hello World', body
|
518
|
+
|
519
|
+
get '/foo'
|
520
|
+
assert not_found?
|
521
|
+
end
|
522
|
+
|
523
|
+
it "does not pass when matching condition returns nil" do
|
524
|
+
mock_app {
|
525
|
+
condition { nil }
|
526
|
+
get '/:foo' do
|
527
|
+
'Hello World'
|
528
|
+
end
|
529
|
+
}
|
530
|
+
|
531
|
+
get '/bar'
|
532
|
+
assert ok?
|
533
|
+
assert_equal 'Hello World', body
|
534
|
+
end
|
535
|
+
|
536
|
+
it "passes to next route when condition calls pass explicitly" do
|
537
|
+
mock_app {
|
538
|
+
condition { pass unless params[:foo] == 'bar' }
|
539
|
+
get '/:foo' do
|
540
|
+
'Hello World'
|
541
|
+
end
|
542
|
+
}
|
543
|
+
|
544
|
+
get '/bar'
|
545
|
+
assert ok?
|
546
|
+
assert_equal 'Hello World', body
|
547
|
+
|
548
|
+
get '/foo'
|
549
|
+
assert not_found?
|
550
|
+
end
|
551
|
+
|
552
|
+
it "passes to the next route when host_name does not match" do
|
553
|
+
mock_app {
|
554
|
+
host_name 'example.com'
|
555
|
+
get '/foo' do
|
556
|
+
'Hello World'
|
557
|
+
end
|
558
|
+
}
|
559
|
+
get '/foo'
|
560
|
+
assert not_found?
|
561
|
+
|
562
|
+
get '/foo', {}, { 'HTTP_HOST' => 'example.com' }
|
563
|
+
assert_equal 200, status
|
564
|
+
assert_equal 'Hello World', body
|
565
|
+
end
|
566
|
+
|
567
|
+
it "passes to the next route when user_agent does not match" do
|
568
|
+
mock_app {
|
569
|
+
user_agent(/Foo/)
|
570
|
+
get '/foo' do
|
571
|
+
'Hello World'
|
572
|
+
end
|
573
|
+
}
|
574
|
+
get '/foo'
|
575
|
+
assert not_found?
|
576
|
+
|
577
|
+
get '/foo', {}, { 'HTTP_USER_AGENT' => 'Foo Bar' }
|
578
|
+
assert_equal 200, status
|
579
|
+
assert_equal 'Hello World', body
|
580
|
+
end
|
581
|
+
|
582
|
+
it "makes captures in user agent pattern available in params[:agent]" do
|
583
|
+
mock_app {
|
584
|
+
user_agent(/Foo (.*)/)
|
585
|
+
get '/foo' do
|
586
|
+
'Hello ' + params[:agent].first
|
587
|
+
end
|
588
|
+
}
|
589
|
+
get '/foo', {}, { 'HTTP_USER_AGENT' => 'Foo Bar' }
|
590
|
+
assert_equal 200, status
|
591
|
+
assert_equal 'Hello Bar', body
|
592
|
+
end
|
593
|
+
|
594
|
+
it "filters by accept header" do
|
595
|
+
mock_app {
|
596
|
+
get '/', :provides => :xml do
|
597
|
+
request.env['HTTP_ACCEPT']
|
598
|
+
end
|
599
|
+
}
|
600
|
+
|
601
|
+
get '/', {}, { 'HTTP_ACCEPT' => 'application/xml' }
|
602
|
+
assert ok?
|
603
|
+
assert_equal 'application/xml', body
|
604
|
+
assert_equal 'application/xml', response.headers['Content-Type']
|
605
|
+
|
606
|
+
get '/', {}, { :accept => 'text/html' }
|
607
|
+
assert !ok?
|
608
|
+
end
|
609
|
+
|
610
|
+
it "allows multiple mime types for accept header" do
|
611
|
+
types = ['image/jpeg', 'image/pjpeg']
|
612
|
+
|
613
|
+
mock_app {
|
614
|
+
get '/', :provides => types do
|
615
|
+
request.env['HTTP_ACCEPT']
|
616
|
+
end
|
617
|
+
}
|
618
|
+
|
619
|
+
types.each do |type|
|
620
|
+
get '/', {}, { 'HTTP_ACCEPT' => type }
|
621
|
+
assert ok?
|
622
|
+
assert_equal type, body
|
623
|
+
assert_equal type, response.headers['Content-Type']
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
it 'degrades gracefully when optional accept header is not provided' do
|
628
|
+
mock_app {
|
629
|
+
get '/', :provides => :xml do
|
630
|
+
request.env['HTTP_ACCEPT']
|
631
|
+
end
|
632
|
+
get '/' do
|
633
|
+
'default'
|
634
|
+
end
|
635
|
+
}
|
636
|
+
get '/'
|
637
|
+
assert ok?
|
638
|
+
assert_equal 'default', body
|
639
|
+
end
|
640
|
+
|
641
|
+
it 'passes a single url param as block parameters when one param is specified' do
|
642
|
+
mock_app {
|
643
|
+
get '/:foo' do |foo|
|
644
|
+
assert_equal 'bar', foo
|
645
|
+
end
|
646
|
+
}
|
647
|
+
|
648
|
+
get '/bar'
|
649
|
+
assert ok?
|
650
|
+
end
|
651
|
+
|
652
|
+
it 'passes multiple params as block parameters when many are specified' do
|
653
|
+
mock_app {
|
654
|
+
get '/:foo/:bar/:baz' do |foo, bar, baz|
|
655
|
+
assert_equal 'abc', foo
|
656
|
+
assert_equal 'def', bar
|
657
|
+
assert_equal 'ghi', baz
|
658
|
+
end
|
659
|
+
}
|
660
|
+
|
661
|
+
get '/abc/def/ghi'
|
662
|
+
assert ok?
|
663
|
+
end
|
664
|
+
|
665
|
+
it 'passes regular expression captures as block parameters' do
|
666
|
+
mock_app {
|
667
|
+
get(/^\/fo(.*)\/ba(.*)/) do |foo, bar|
|
668
|
+
assert_equal 'orooomma', foo
|
669
|
+
assert_equal 'f', bar
|
670
|
+
'looks good'
|
671
|
+
end
|
672
|
+
}
|
673
|
+
|
674
|
+
get '/foorooomma/baf'
|
675
|
+
assert ok?
|
676
|
+
assert_equal 'looks good', body
|
677
|
+
end
|
678
|
+
|
679
|
+
it "supports mixing multiple splat params like /*/foo/*/* as block parameters" do
|
680
|
+
mock_app {
|
681
|
+
get '/*/foo/*/*' do |foo, bar, baz|
|
682
|
+
assert_equal 'bar', foo
|
683
|
+
assert_equal 'bling', bar
|
684
|
+
assert_equal 'baz/boom', baz
|
685
|
+
'looks good'
|
686
|
+
end
|
687
|
+
}
|
688
|
+
|
689
|
+
get '/bar/foo/bling/baz/boom'
|
690
|
+
assert ok?
|
691
|
+
assert_equal 'looks good', body
|
692
|
+
end
|
693
|
+
|
694
|
+
it 'raises an ArgumentError with block arity > 1 and too many values' do
|
695
|
+
mock_app {
|
696
|
+
get '/:foo/:bar/:baz' do |foo, bar|
|
697
|
+
'quux'
|
698
|
+
end
|
699
|
+
}
|
700
|
+
|
701
|
+
assert_raise(ArgumentError) { get '/a/b/c' }
|
702
|
+
end
|
703
|
+
|
704
|
+
it 'raises an ArgumentError with block param arity > 1 and too few values' do
|
705
|
+
mock_app {
|
706
|
+
get '/:foo/:bar' do |foo, bar, baz|
|
707
|
+
'quux'
|
708
|
+
end
|
709
|
+
}
|
710
|
+
|
711
|
+
assert_raise(ArgumentError) { get '/a/b' }
|
712
|
+
end
|
713
|
+
|
714
|
+
it 'succeeds if no block parameters are specified' do
|
715
|
+
mock_app {
|
716
|
+
get '/:foo/:bar' do
|
717
|
+
'quux'
|
718
|
+
end
|
719
|
+
}
|
720
|
+
|
721
|
+
get '/a/b'
|
722
|
+
assert ok?
|
723
|
+
assert_equal 'quux', body
|
724
|
+
end
|
725
|
+
|
726
|
+
it 'passes all params with block param arity -1 (splat args)' do
|
727
|
+
mock_app {
|
728
|
+
get '/:foo/:bar' do |*args|
|
729
|
+
args.join
|
730
|
+
end
|
731
|
+
}
|
732
|
+
|
733
|
+
get '/a/b'
|
734
|
+
assert ok?
|
735
|
+
assert_equal 'ab', body
|
736
|
+
end
|
737
|
+
|
738
|
+
it 'allows custom route-conditions to be set via route options' do
|
739
|
+
protector = Module.new {
|
740
|
+
def protect(*args)
|
741
|
+
condition {
|
742
|
+
unless authorize(params["user"], params["password"])
|
743
|
+
halt 403, "go away"
|
744
|
+
end
|
745
|
+
}
|
746
|
+
end
|
747
|
+
}
|
748
|
+
|
749
|
+
mock_app {
|
750
|
+
register protector
|
751
|
+
|
752
|
+
helpers do
|
753
|
+
def authorize(username, password)
|
754
|
+
username == "foo" && password == "bar"
|
755
|
+
end
|
756
|
+
end
|
757
|
+
|
758
|
+
get "/", :protect => true do
|
759
|
+
"hey"
|
760
|
+
end
|
761
|
+
}
|
762
|
+
|
763
|
+
get "/"
|
764
|
+
assert forbidden?
|
765
|
+
assert_equal "go away", body
|
766
|
+
|
767
|
+
get "/", :user => "foo", :password => "bar"
|
768
|
+
assert ok?
|
769
|
+
assert_equal "hey", body
|
770
|
+
end
|
771
|
+
|
772
|
+
it "supports the resource method" do
|
773
|
+
mock_app {
|
774
|
+
resource 'users' do
|
775
|
+
get do
|
776
|
+
"users get"
|
777
|
+
end
|
778
|
+
post do
|
779
|
+
"users post"
|
780
|
+
end
|
781
|
+
end
|
782
|
+
resource 'users/:id' do
|
783
|
+
get do
|
784
|
+
"single user get: #{params[:id]}"
|
785
|
+
end
|
786
|
+
put do
|
787
|
+
"single user put: #{params[:id]}"
|
788
|
+
end
|
789
|
+
delete do
|
790
|
+
"single user delete: #{params[:id]}"
|
791
|
+
end
|
792
|
+
get 'details' do
|
793
|
+
"single user details: #{params[:id]}"
|
794
|
+
end
|
795
|
+
end
|
796
|
+
}
|
797
|
+
get '/users'
|
798
|
+
assert_equal "users get", body
|
799
|
+
post '/users'
|
800
|
+
assert_equal "users post", body
|
801
|
+
get '/users/1'
|
802
|
+
assert_equal "single user get: 1", body
|
803
|
+
put '/users/1'
|
804
|
+
assert_equal "single user put: 1", body
|
805
|
+
delete '/users/1'
|
806
|
+
assert_equal "single user delete: 1", body
|
807
|
+
get '/users/1/details'
|
808
|
+
assert_equal "single user details: 1", body
|
809
|
+
end
|
810
|
+
|
811
|
+
it "supports member resources and block params" do
|
812
|
+
mock_app {
|
813
|
+
resource :users do
|
814
|
+
get do
|
815
|
+
"users get"
|
816
|
+
end
|
817
|
+
post do
|
818
|
+
"users post"
|
819
|
+
end
|
820
|
+
member do
|
821
|
+
get do |id|
|
822
|
+
"single user get: #{id}"
|
823
|
+
end
|
824
|
+
put do |id|
|
825
|
+
"single user put: #{id}"
|
826
|
+
end
|
827
|
+
delete do |id|
|
828
|
+
"single user delete: #{id}"
|
829
|
+
end
|
830
|
+
get :details do |id|
|
831
|
+
"single user details: #{id}"
|
832
|
+
end
|
833
|
+
end
|
834
|
+
end
|
835
|
+
}
|
836
|
+
get '/users'
|
837
|
+
assert_equal "users get", body
|
838
|
+
post '/users'
|
839
|
+
assert_equal "users post", body
|
840
|
+
get '/users/1'
|
841
|
+
assert_equal "single user get: 1", body
|
842
|
+
put '/users/1'
|
843
|
+
assert_equal "single user put: 1", body
|
844
|
+
delete '/users/1'
|
845
|
+
assert_equal "single user delete: 1", body
|
846
|
+
get '/users/1/details'
|
847
|
+
assert_equal "single user details: 1", body
|
848
|
+
end
|
849
|
+
|
850
|
+
it "supports arbitrarily nested resources" do
|
851
|
+
mock_app {
|
852
|
+
resource :admin do
|
853
|
+
resource :users do
|
854
|
+
get do
|
855
|
+
"admin users get"
|
856
|
+
end
|
857
|
+
post do
|
858
|
+
"admin users post"
|
859
|
+
end
|
860
|
+
member do
|
861
|
+
get do |id|
|
862
|
+
"admin user get: #{id}"
|
863
|
+
end
|
864
|
+
put do
|
865
|
+
"admin user put: #{params[:id]}"
|
866
|
+
end
|
867
|
+
end
|
868
|
+
end
|
869
|
+
end
|
870
|
+
}
|
871
|
+
get '/admin'
|
872
|
+
assert_equal "<h1>Not Found</h1>", body
|
873
|
+
get '/admin/users'
|
874
|
+
assert_equal "admin users get", body
|
875
|
+
post '/admin/users'
|
876
|
+
assert_equal "admin users post", body
|
877
|
+
get '/admin/users/1'
|
878
|
+
assert_equal "admin user get: 1", body
|
879
|
+
put '/admin/users/1'
|
880
|
+
assert_equal "admin user put: 1", body
|
881
|
+
end
|
882
|
+
|
883
|
+
# NOTE Block params behaves differently under 1.8 and 1.9. Under 1.8, block
|
884
|
+
# param arity is lax: declaring a mismatched number of block params results
|
885
|
+
# in a warning. Under 1.9, block param arity is strict: mismatched block
|
886
|
+
# arity raises an ArgumentError.
|
887
|
+
|
888
|
+
if RUBY_VERSION >= '1.9'
|
889
|
+
|
890
|
+
it 'raises an ArgumentError with block param arity 1 and no values' do
|
891
|
+
mock_app {
|
892
|
+
get '/foo' do |foo|
|
893
|
+
'quux'
|
894
|
+
end
|
895
|
+
}
|
896
|
+
|
897
|
+
assert_raise(ArgumentError) { get '/foo' }
|
898
|
+
end
|
899
|
+
|
900
|
+
it 'raises an ArgumentError with block param arity 1 and too many values' do
|
901
|
+
mock_app {
|
902
|
+
get '/:foo/:bar/:baz' do |foo|
|
903
|
+
'quux'
|
904
|
+
end
|
905
|
+
}
|
906
|
+
|
907
|
+
assert_raise(ArgumentError) { get '/a/b/c' }
|
908
|
+
end
|
909
|
+
|
910
|
+
else
|
911
|
+
|
912
|
+
it 'does not raise an ArgumentError with block param arity 1 and no values' do
|
913
|
+
mock_app {
|
914
|
+
get '/foo' do |foo|
|
915
|
+
'quux'
|
916
|
+
end
|
917
|
+
}
|
918
|
+
|
919
|
+
silence_warnings { get '/foo' }
|
920
|
+
assert ok?
|
921
|
+
assert_equal 'quux', body
|
922
|
+
end
|
923
|
+
|
924
|
+
it 'does not raise an ArgumentError with block param arity 1 and too many values' do
|
925
|
+
mock_app {
|
926
|
+
get '/:foo/:bar/:baz' do |foo|
|
927
|
+
'quux'
|
928
|
+
end
|
929
|
+
}
|
930
|
+
|
931
|
+
silence_warnings { get '/a/b/c' }
|
932
|
+
assert ok?
|
933
|
+
assert_equal 'quux', body
|
934
|
+
end
|
935
|
+
|
936
|
+
end
|
937
|
+
|
938
|
+
it "matches routes defined in superclasses" do
|
939
|
+
base = Class.new(Sinatra::Base)
|
940
|
+
base.get('/foo') { 'foo in baseclass' }
|
941
|
+
|
942
|
+
mock_app(base) {
|
943
|
+
get('/bar') { 'bar in subclass' }
|
944
|
+
}
|
945
|
+
|
946
|
+
get '/foo'
|
947
|
+
assert ok?
|
948
|
+
assert_equal 'foo in baseclass', body
|
949
|
+
|
950
|
+
get '/bar'
|
951
|
+
assert ok?
|
952
|
+
assert_equal 'bar in subclass', body
|
953
|
+
end
|
954
|
+
|
955
|
+
# broken in old Sinatras (not our problem)
|
956
|
+
# it "matches routes in subclasses before superclasses" do
|
957
|
+
# base = Class.new(Sinatra::Base)
|
958
|
+
# base.get('/foo') { 'foo in baseclass' }
|
959
|
+
# base.get('/bar') { 'bar in baseclass' }
|
960
|
+
#
|
961
|
+
# mock_app(base) {
|
962
|
+
# get('/foo') { 'foo in subclass' }
|
963
|
+
# }
|
964
|
+
#
|
965
|
+
# get '/foo'
|
966
|
+
# assert ok?
|
967
|
+
# assert_equal 'foo in subclass', body
|
968
|
+
#
|
969
|
+
# get '/bar'
|
970
|
+
# assert ok?
|
971
|
+
# assert_equal 'bar in baseclass', body
|
972
|
+
# end
|
973
|
+
end
|
data/test/test_app.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#
|
2
|
+
# Sinatra app
|
3
|
+
#
|
4
|
+
# It is put in a separate file to make sure we are getting a realistic test
|
5
|
+
#
|
6
|
+
require 'sinatra/base'
|
7
|
+
require 'sinatra/resources'
|
8
|
+
|
9
|
+
class Application < Sinatra::Base
|
10
|
+
set :app_file, __FILE__
|
11
|
+
register Sinatra::Resources
|
12
|
+
|
13
|
+
resource :users do
|
14
|
+
get do
|
15
|
+
'yo'
|
16
|
+
end
|
17
|
+
|
18
|
+
member do
|
19
|
+
get do
|
20
|
+
'hi'
|
21
|
+
end
|
22
|
+
|
23
|
+
get :recent do |id|
|
24
|
+
"recent: #{id}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
resource :forums do
|
31
|
+
resource :admin do
|
32
|
+
get do
|
33
|
+
'woot'
|
34
|
+
end
|
35
|
+
|
36
|
+
post do
|
37
|
+
'success'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Application.run!
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-resources
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nate Wiger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-04 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: sinatra
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.9.0
|
24
|
+
version:
|
25
|
+
description: Adds resource and member blocks to DSL. Based on widely-followed Sinatra ticket.
|
26
|
+
email: nate@wiger.org
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.rdoc
|
33
|
+
files:
|
34
|
+
- lib/sinatra/resources.rb
|
35
|
+
- test/contest.rb
|
36
|
+
- test/helper.rb
|
37
|
+
- test/routing_test.rb
|
38
|
+
- test/test_app.rb
|
39
|
+
- README.rdoc
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/nateware/sinatra-resources
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options:
|
46
|
+
- --title
|
47
|
+
- Sinatra::Resources -- Simple nested resources for Sinatra
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
requirements:
|
63
|
+
- sinatra, v0.9.0 or greater
|
64
|
+
rubyforge_project: sinatra-resources
|
65
|
+
rubygems_version: 1.3.5
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Simple nested resources for Sinatra
|
69
|
+
test_files: []
|
70
|
+
|