scorched 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -0
- data/LICENSE +0 -0
- data/Milestones.md +24 -27
- data/README.md +56 -48
- data/docs/01_preface.md +12 -2
- data/docs/02_fundamentals/01_the_controller.md +132 -11
- data/docs/02_fundamentals/02_configuration.md +22 -20
- data/docs/02_fundamentals/03_routing.md +49 -46
- data/docs/02_fundamentals/04_requests_and_responses.md +14 -9
- data/docs/02_fundamentals/05_filters.md +21 -17
- data/docs/02_fundamentals/06_middleware.md +12 -11
- data/docs/02_fundamentals/07_request_and_session_data.md +67 -61
- data/docs/02_fundamentals/08_views.md +9 -9
- data/docs/02_fundamentals/09_sharing_request_state.md +9 -8
- data/docs/03_further_reading/be_creative.md +31 -30
- data/docs/03_further_reading/code_reloading.md +2 -2
- data/docs/03_further_reading/running_unit_tests.md +2 -2
- data/examples/media_types.ru +2 -2
- data/lib/scorched/collection.rb +0 -0
- data/lib/scorched/controller.rb +27 -18
- data/lib/scorched/dynamic_delegate.rb +0 -0
- data/lib/scorched/error.rb +0 -0
- data/lib/scorched/static.rb +0 -0
- data/lib/scorched/version.rb +1 -1
- data/scorched.gemspec +2 -0
- data/spec/collection_spec.rb +0 -0
- data/spec/controller_spec.rb +107 -27
- data/spec/options_spec.rb +0 -0
- data/spec/public/static.txt +0 -0
- data/spec/request_spec.rb +0 -0
- data/spec/views/composer.erb +0 -0
- data/spec/views/layout.erb +0 -0
- data/spec/views/main.erb +0 -0
- data/spec/views/other.str +0 -0
- data/spec/views/partial.erb +0 -0
- metadata +3 -3
@@ -6,11 +6,12 @@ The only data exchanged between controllers is the Rack environment hash. Using
|
|
6
6
|
|
7
7
|
The Rack idiom is to namespace your keys, typically with your project name, using dots as delimiters. You can further group your keys using more dot-delimited namespaces. Perhaps an example is due:
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
9
|
+
```ruby
|
10
|
+
before user_agent: /MSIE|Windows/ do
|
11
|
+
env['myapp.dangerous_request'] = true
|
12
|
+
end
|
13
|
+
|
14
|
+
get '/' do
|
15
|
+
"Welcome to #{env['myapp.settings.site_name']}!"
|
16
|
+
end
|
17
|
+
```
|
@@ -6,37 +6,38 @@ Effortless REST
|
|
6
6
|
---------------
|
7
7
|
An DRY way to serve multiple content-types:
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
get '/' do
|
33
|
-
view :index
|
34
|
-
[
|
35
|
-
{title: 'Sweet Purple Unicorns', date: '08/03/2013'},
|
36
|
-
{title: 'Mellow Grass Men', date: '21/03/2013'}
|
37
|
-
]
|
38
|
-
end
|
9
|
+
```ruby
|
10
|
+
class App < Scorched::Controller
|
11
|
+
def view(view = nil)
|
12
|
+
view ? env['app.view'] = view : env['app.view']
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
data = response.body.join('')
|
17
|
+
response['Content-type'] = 'text/html'
|
18
|
+
if check_condition?(:media_type, 'text/html')
|
19
|
+
response.body = render(view, locals: {data: data})
|
20
|
+
elsif check_condition?(:media_type, 'application/json')
|
21
|
+
response['Content-type'] = 'application/json'
|
22
|
+
response.body = data.to_json
|
23
|
+
elsif check_condition?(:media_type, 'application/pdf')
|
24
|
+
response['Content-type'] = 'text/plain'
|
25
|
+
response.status = 406
|
26
|
+
response.body = 'PDF rendering service currently unavailable.'
|
27
|
+
else
|
28
|
+
response.body = render(view, locals: {data: data})
|
39
29
|
end
|
30
|
+
end
|
31
|
+
|
32
|
+
get '/' do
|
33
|
+
view :index
|
34
|
+
[
|
35
|
+
{title: 'Sweet Purple Unicorns', date: '08/03/2013'},
|
36
|
+
{title: 'Mellow Grass Men', date: '21/03/2013'}
|
37
|
+
]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
40
41
|
|
41
42
|
|
42
43
|
Authentication and Permissions
|
@@ -7,7 +7,7 @@ A sub-set of the most popular application reloaders are detailed below.
|
|
7
7
|
|
8
8
|
Rerun
|
9
9
|
-----
|
10
|
-
Rerun is a general purpose gem (``gem install rerun``) for watching files and rerunning some command when any of those files change. The following example uses rerun to watch for changes to ".rb" and ".ru" files anywhere in the current directory tree. If a change is made, any previous instance of
|
10
|
+
Rerun is a general purpose gem (``gem install rerun``) for watching files and rerunning some command when any of those files change. The following example uses rerun to watch for changes to ".rb" and ".ru" files anywhere in the current directory tree. If a change is made, any previous instance of `rackup` are shutdown and restarted.
|
11
11
|
|
12
12
|
rerun -p "**/*.{rb,ru}" rackup
|
13
13
|
|
@@ -21,4 +21,4 @@ Phusion Passenger
|
|
21
21
|
-----------------
|
22
22
|
The popular Apache/Nginx module for hosting Rack applications, Phusion Passenger, can be configured to reload your application on every request much like Shotgun does. Passenger can be faster that Shogun, as you can let the underlying web server (Apache or Nginx) to take care of the static file serving, which avoids having to reload your application for every static file request.
|
23
23
|
|
24
|
-
To have Passenger automatically restart your application on every request, create a file named
|
24
|
+
To have Passenger automatically restart your application on every request, create a file named `tmp/always_restart.txt` in the root of your application directory. Passenger will automatically detect this file and behave as intended.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Running Unit Tests
|
2
2
|
==================
|
3
3
|
|
4
|
-
If doing any development on Scorched, such as if you want to fork it or contribute patches to it, you will probably want to run the suite of tests that accompany it. The few dependancies required for running the Scorched unit tests are installed either when you install the Scorched gem, or by running
|
4
|
+
If doing any development on Scorched, such as if you want to fork it or contribute patches to it, you will probably want to run the suite of tests that accompany it. The few dependancies required for running the Scorched unit tests are installed either when you install the Scorched gem, or by running `bundle` in the root of the Scorched source tree.
|
5
5
|
|
6
|
-
All unit tests have been written using RSpec. To run the tests,
|
6
|
+
All unit tests have been written using RSpec. To run the tests, `cd` into the root of the Scorched source tree from a terminal, and run `rspec`.
|
data/examples/media_types.ru
CHANGED
@@ -11,13 +11,13 @@ class MediaTypesExample < Scorched::Controller
|
|
11
11
|
HTML
|
12
12
|
end
|
13
13
|
|
14
|
-
controller
|
14
|
+
controller '/*' do
|
15
15
|
get '/*' do |*captures|
|
16
16
|
request.breadcrumb.inspect
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
get '
|
20
|
+
get '/', media_type: 'application/json' do
|
21
21
|
{name: 'John Falkon', age: 39, occupation: 'Carpet Cleaner'}.to_json
|
22
22
|
end
|
23
23
|
|
data/lib/scorched/collection.rb
CHANGED
File without changes
|
data/lib/scorched/controller.rb
CHANGED
@@ -12,7 +12,8 @@ module Scorched
|
|
12
12
|
:strip_trailing_slash => :redirect, # :redirect => Strips and redirects URL ending in forward slash, :ignore => internally ignores trailing slash, false => does nothing.
|
13
13
|
:static_dir => 'public', # The directory Scorched should serve static files from. Set to false if web server or anything else is serving static files.
|
14
14
|
:logger => nil,
|
15
|
-
:show_exceptions => false
|
15
|
+
:show_exceptions => false,
|
16
|
+
:auto_pass => false, # Automatically _pass_ request back to outer controller if no route matches.
|
16
17
|
}
|
17
18
|
|
18
19
|
render_defaults << {
|
@@ -114,15 +115,16 @@ module Scorched
|
|
114
115
|
# Creates a new controller as a sub-class of self (by default), mapping it to self using the provided mapping
|
115
116
|
# hash if one is provided. Returns the new anonymous controller class.
|
116
117
|
#
|
117
|
-
# Takes
|
118
|
-
# from, a mapping hash
|
118
|
+
# Takes three optional arguments and a block: a pattern, a parent class from which the generated controller class
|
119
|
+
# inherits from, a mapping hash for setting conditions and so on, and of course a block which defines the
|
119
120
|
# controller class.
|
120
121
|
#
|
121
122
|
# It's worth noting, however obvious, that the resulting class will only be a controller if the parent class is
|
122
123
|
# (or inherits from) a Scorched::Controller.
|
123
|
-
def controller(parent_class = self, **mapping, &block)
|
124
|
+
def controller(pattern = '/', parent_class = self, **mapping, &block)
|
124
125
|
c = Class.new(parent_class, &block)
|
125
|
-
|
126
|
+
c.config[:auto_pass] = true if parent_class < Scorched::Controller
|
127
|
+
self << {pattern: pattern, target: c}.merge(mapping)
|
126
128
|
c
|
127
129
|
end
|
128
130
|
|
@@ -199,28 +201,31 @@ module Scorched
|
|
199
201
|
(f[:args].empty? || f[:args].any? { |type| e.is_a?(type) }) && check_conditions?(f[:conditions]) && instance_exec(e, &f[:proc])
|
200
202
|
end
|
201
203
|
end
|
202
|
-
|
203
|
-
match = matches(true).first
|
204
|
+
|
204
205
|
begin
|
205
206
|
catch(:halt) do
|
206
207
|
if config[:strip_trailing_slash] == :redirect && request.path =~ %r{./$}
|
207
208
|
redirect(request.path.chomp('/'))
|
208
209
|
end
|
209
210
|
|
211
|
+
all_matches = matches
|
212
|
+
if all_matches.empty?
|
213
|
+
pass if config[:auto_pass]
|
214
|
+
response.status = 404
|
215
|
+
end
|
216
|
+
|
210
217
|
run_filters(:before)
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
catch(:halt) do
|
218
|
+
begin
|
219
|
+
all_matches.each do |match|
|
220
|
+
request.breadcrumb << match
|
221
|
+
processed = catch(:pass) {
|
222
|
+
target = match[:mapping][:target]
|
217
223
|
response.merge! (Proc === target) ? instance_exec(request.env, &target) : target.call(request.env)
|
218
|
-
|
219
|
-
|
220
|
-
rescue_block.call(inner_error)
|
224
|
+
}
|
225
|
+
processed ? break : request.breadcrumb.pop
|
221
226
|
end
|
222
|
-
|
223
|
-
|
227
|
+
rescue => inner_error
|
228
|
+
rescue_block.call(inner_error)
|
224
229
|
end
|
225
230
|
run_filters(:after)
|
226
231
|
end
|
@@ -281,6 +286,10 @@ module Scorched
|
|
281
286
|
throw :halt
|
282
287
|
end
|
283
288
|
|
289
|
+
def pass
|
290
|
+
throw :pass
|
291
|
+
end
|
292
|
+
|
284
293
|
# Convenience method for accessing Rack request.
|
285
294
|
def request
|
286
295
|
env['scorched.request']
|
File without changes
|
data/lib/scorched/error.rb
CHANGED
File without changes
|
data/lib/scorched/static.rb
CHANGED
File without changes
|
data/lib/scorched/version.rb
CHANGED
data/scorched.gemspec
CHANGED
@@ -11,6 +11,8 @@ Gem::Specification.new 'scorched', Scorched::VERSION do |s|
|
|
11
11
|
s.test_files = Dir.glob('spec/**/*_spec.rb')
|
12
12
|
s.rdoc_options = %w[--line-numbers --inline-source --title Scorched --encoding=UTF-8]
|
13
13
|
|
14
|
+
s.required_ruby_version = '>= 2.0.0'
|
15
|
+
|
14
16
|
s.add_dependency 'rack', '~> 1.4'
|
15
17
|
s.add_dependency 'rack-accept', '~> 0.4'
|
16
18
|
s.add_dependency 'tilt', '~> 1.3'
|
data/spec/collection_spec.rb
CHANGED
File without changes
|
data/spec/controller_spec.rb
CHANGED
@@ -217,34 +217,57 @@ module Scorched
|
|
217
217
|
end
|
218
218
|
|
219
219
|
describe "sub-controllers" do
|
220
|
-
it "
|
221
|
-
app.
|
222
|
-
get('
|
223
|
-
end
|
224
|
-
|
225
|
-
response.status.should == 200
|
226
|
-
response.body.should == 'hello'
|
220
|
+
it "should ignore the already matched portions of the path" do
|
221
|
+
app << {pattern: '/article', target: Class.new(Scorched::Controller) do
|
222
|
+
get('/*') { |title| title }
|
223
|
+
end}
|
224
|
+
rt.get('/article/hello-world').body.should == 'hello-world'
|
227
225
|
end
|
228
226
|
|
229
|
-
|
230
|
-
|
231
|
-
|
227
|
+
describe "controller helper" do
|
228
|
+
it "can be given no arguments" do
|
229
|
+
app.controller do
|
230
|
+
get('/') { 'hello' }
|
231
|
+
end
|
232
|
+
response = rt.get('/')
|
233
|
+
response.status.should == 200
|
234
|
+
response.body.should == 'hello'
|
232
235
|
end
|
233
|
-
app.mappings.first[:priority].should == -1
|
234
|
-
rt.get('/').status.should == 404
|
235
|
-
rt.post('/').body.should == 'ok'
|
236
|
-
end
|
237
236
|
|
238
|
-
|
239
|
-
|
240
|
-
|
237
|
+
it "can be given a pattern" do
|
238
|
+
app.controller '/dog' do
|
239
|
+
get('/') { 'roof' }
|
240
|
+
end
|
241
|
+
response = rt.get('/dog')
|
242
|
+
response.status.should == 200
|
243
|
+
response.body.should == 'roof'
|
241
244
|
end
|
242
|
-
rt.get('/article/hello-world').body.should == 'hello-world'
|
243
|
-
end
|
244
245
|
|
245
|
-
|
246
|
-
|
247
|
-
|
246
|
+
it "inherits from parent class, or any other class" do
|
247
|
+
app.controller.superclass.should == app
|
248
|
+
app.controller('/', String).superclass.should == String
|
249
|
+
end
|
250
|
+
|
251
|
+
it "can take mapping options" do
|
252
|
+
app.controller priority: -1, conditions: {methods: 'POST'} do
|
253
|
+
route('/') { 'ok' }
|
254
|
+
end
|
255
|
+
app.mappings.first[:priority].should == -1
|
256
|
+
rt.get('/').status.should == 404
|
257
|
+
rt.post('/').body.should == 'ok'
|
258
|
+
end
|
259
|
+
|
260
|
+
it "automatically passes to the outer controller when no match" do
|
261
|
+
filters_run = 0
|
262
|
+
app.controller do
|
263
|
+
before { filters_run += 1 }
|
264
|
+
get('/sub') { 'goodbye' }
|
265
|
+
after { filters_run += 1 }
|
266
|
+
end
|
267
|
+
app.get('/') { 'hello' }
|
268
|
+
rt.get('/').body.should == 'hello'
|
269
|
+
filters_run.should == 0
|
270
|
+
end
|
248
271
|
end
|
249
272
|
end
|
250
273
|
|
@@ -311,7 +334,10 @@ module Scorched
|
|
311
334
|
example "before filters run from outermost to inner" do
|
312
335
|
order = []
|
313
336
|
app.before { order << :outer }
|
314
|
-
app.controller
|
337
|
+
app.controller do
|
338
|
+
before { order << :inner }
|
339
|
+
get('/') { }
|
340
|
+
end
|
315
341
|
rt.get('/')
|
316
342
|
order.should == [:outer, :inner]
|
317
343
|
end
|
@@ -319,7 +345,10 @@ module Scorched
|
|
319
345
|
example "after filters run from innermost to outermost" do
|
320
346
|
order = []
|
321
347
|
app.after { order << :outer }
|
322
|
-
app.controller
|
348
|
+
app.controller do
|
349
|
+
get('/') { }
|
350
|
+
after { order << :inner }
|
351
|
+
end
|
323
352
|
rt.get('/')
|
324
353
|
order.should == [:inner, :outer]
|
325
354
|
end
|
@@ -419,7 +448,7 @@ module Scorched
|
|
419
448
|
get '/'do
|
420
449
|
request.env['scorched.simple_counter']
|
421
450
|
end
|
422
|
-
controller
|
451
|
+
controller '/sub_controller' do
|
423
452
|
get '/' do
|
424
453
|
request.env['scorched.simple_counter']
|
425
454
|
end
|
@@ -452,10 +481,10 @@ module Scorched
|
|
452
481
|
rt.get('/').status.should == 401
|
453
482
|
end
|
454
483
|
|
455
|
-
it "
|
484
|
+
it "skips processing filters" do
|
456
485
|
app.after { response.status = 403 }
|
457
486
|
app.get('/') { halt }
|
458
|
-
rt.get('/').status.should ==
|
487
|
+
rt.get('/').status.should == 200
|
459
488
|
end
|
460
489
|
|
461
490
|
it "short circuits filters if halted within filter" do
|
@@ -465,6 +494,36 @@ module Scorched
|
|
465
494
|
end
|
466
495
|
end
|
467
496
|
|
497
|
+
describe "passing" do
|
498
|
+
it "invokes the next match" do
|
499
|
+
app.get('/') { response.body << 'hello'; pass }
|
500
|
+
app.get('/') { response.body << ' there'; pass }
|
501
|
+
app.get('/') { response.body << ' sir' }
|
502
|
+
app.get('/') { response.body << '!' } # Shouldn't be hit
|
503
|
+
rt.get('/').body.should == 'hello there sir'
|
504
|
+
end
|
505
|
+
|
506
|
+
it "invokes the next match in parent controller if passed from filter" do
|
507
|
+
app.controller '/sub' do
|
508
|
+
get('/') { }
|
509
|
+
after do
|
510
|
+
response.body << 'hello'
|
511
|
+
pass
|
512
|
+
end
|
513
|
+
end
|
514
|
+
app.get('/sub') { response.body << ' there' }
|
515
|
+
rt.get('/sub').body.should == 'hello there'
|
516
|
+
end
|
517
|
+
|
518
|
+
it "passing within filter of root controller results in uncaught symbol" do
|
519
|
+
app.before { pass }
|
520
|
+
expect {
|
521
|
+
app.get('/') { }
|
522
|
+
rt.get('/')
|
523
|
+
}.to raise_error(ArgumentError)
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
468
527
|
describe "configuration" do
|
469
528
|
describe "strip_trailing_slash" do
|
470
529
|
it "can be set to strip trailing slash and redirect" do
|
@@ -525,6 +584,27 @@ module Scorched
|
|
525
584
|
}.to raise_error(RuntimeError)
|
526
585
|
end
|
527
586
|
end
|
587
|
+
|
588
|
+
describe "auto_pass" do
|
589
|
+
it "if no match, passes to the outer controller without running any filters" do
|
590
|
+
sub = Class.new(Scorched::Controller) do
|
591
|
+
config[:auto_pass] = true
|
592
|
+
before { response.status = 600 }
|
593
|
+
get('/hello') { 'hello' }
|
594
|
+
after { response.status = 600 }
|
595
|
+
end
|
596
|
+
app << {pattern: '/', target: sub}
|
597
|
+
app.get('/') { 'ok' }
|
598
|
+
rt.get('/').body.should == 'ok'
|
599
|
+
rt.get('/').status.should == 200
|
600
|
+
rt.get('/hello').body.should == 'hello'
|
601
|
+
rt.get('/hello').status.should == 600
|
602
|
+
|
603
|
+
sub.config[:auto_pass] = false
|
604
|
+
rt.get('/').body.should == ''
|
605
|
+
rt.get('/').status.should == 600
|
606
|
+
end
|
607
|
+
end
|
528
608
|
end
|
529
609
|
|
530
610
|
describe "sessions" do
|
data/spec/options_spec.rb
CHANGED
File without changes
|
data/spec/public/static.txt
CHANGED
File without changes
|
data/spec/request_spec.rb
CHANGED
File without changes
|
data/spec/views/composer.erb
CHANGED
File without changes
|
data/spec/views/layout.erb
CHANGED
File without changes
|
data/spec/views/main.erb
CHANGED
File without changes
|
data/spec/views/other.str
CHANGED
File without changes
|
data/spec/views/partial.erb
CHANGED
File without changes
|