sinatra-resources 0.1.0

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