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 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
+