scorched 1.1.0.pre → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +15 -0
- data/docs/02_fundamentals/03_routing.md +1 -1
- data/docs/02_fundamentals/05_filters.md +19 -0
- data/lib/scorched/controller.rb +74 -29
- data/lib/scorched/version.rb +1 -1
- data/scorched.gemspec +1 -0
- data/spec/controller_spec.rb +96 -1
- metadata +17 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e9063c14895226f457fcb9255e206f5915a8aaefde84be0114dabbf4723a677
|
|
4
|
+
data.tar.gz: e773ac700d17abe3b3ce00f94be2665e4753bacffee9ce07fc4e86a00f35afec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2444d1752e6296881377c74c21a6219b19b9ef415165e89382d4b9f3d80b8e7030c5997cf8fcffeb6377c5f58cd96431019f170ee75577210564798b61514764
|
|
7
|
+
data.tar.gz: 61dae3a5d9f0d4d35d53be9db55c157d0e0cbdeee1bcf504a0d9069fa674ad993e95c865ddcf8165d78ed5c2dd1f567184baecc1be90f69017d329f79642e75c
|
data/CHANGES.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
### v1.1.1
|
|
5
|
+
* Fixed frozen string mutation in `absolute` method.
|
|
6
|
+
### v1.1.0
|
|
7
|
+
* Added `around` filters to allow wrapping application logic around a request. `around` filters wrap around `before/after` filters. Ideal for `begin...ensure...end` where you need to ensure something happens before the request processing ends. For example:
|
|
8
|
+
```
|
|
9
|
+
around do |cont|
|
|
10
|
+
begin
|
|
11
|
+
cont.call
|
|
12
|
+
ensure
|
|
13
|
+
# Ensure something important happens here
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
* Compatible with Ruby 4.0
|
|
18
|
+
|
|
4
19
|
### v1.1.0.pre
|
|
5
20
|
* Updated dependancies
|
|
6
21
|
* Now compatible with (and requires) Rack 3.x
|
|
@@ -117,7 +117,7 @@ Conditions are essentially just pre-requisites that must be met before a mapping
|
|
|
117
117
|
* `:host` - The host name (i.e. domain name) used in the request.
|
|
118
118
|
* `:language` - Languages accepted by the client.
|
|
119
119
|
* `:media_type` - Media types (i.e. content types) accepted by the client.
|
|
120
|
-
* `:
|
|
120
|
+
* `:dispatched` - Whether a mapping in the controller instance was invoked as the target for the request. A mapping that _passes_ a request is not considered dispatched.
|
|
121
121
|
* `:method` - The request method used, e.g. GET, POST, PUT, ... .
|
|
122
122
|
* `:proc` - An on-the-fly condition to be evaluated in the context of the controller instance. Should return true if the condition was satisfied, or false otherwise.
|
|
123
123
|
* `:user_agent` - The user agent string provided with the request. Takes a Regexp or String.
|
|
@@ -42,6 +42,25 @@ end
|
|
|
42
42
|
How you use filters is up to your own imagination and creative problem solving.
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
Around Filters
|
|
46
|
+
--------------
|
|
47
|
+
Around filters wrap the entire before/dispatch/after cycle. The block is passed a single argument - a proc representing the rest of the filter chain - which it must call (e.g. `cont.call`) for request handling to proceed. If the block doesn't call it, the wrapped request chain never runs.
|
|
48
|
+
|
|
49
|
+
Around filters defined earlier (including those defined in superclasses) wrap those defined later. Around filters are ideal for `begin...ensure...end`-style wrapping, where you need to guarantee something happens once the request has finished processing.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
around do |cont|
|
|
53
|
+
begin
|
|
54
|
+
cont.call
|
|
55
|
+
ensure
|
|
56
|
+
# Ensure something important happens here
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Like before and after filters, around filters can have conditions defined on them.
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
Error Filters
|
|
46
65
|
-------------
|
|
47
66
|
Error filters are processed like regular filters, except they're called whenever an exception occurs. If an error filter returns false, it's assumed to be unhandled, and the next error filter is called. This continues until one of the following is true, 1) an error filter returns true, in which case the exception is assumed to be handled, 2) an error filter raises an exception itself, or 3) there are no more error handlers defined on the current controller, in which case the exception is re-raised.
|
data/lib/scorched/controller.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Scorched
|
|
|
10
10
|
include Scorched::Options('symbol_matchers')
|
|
11
11
|
include Scorched::Collection('middleware')
|
|
12
12
|
include Scorched::Collection('before_filters')
|
|
13
|
+
include Scorched::Collection('around_filters')
|
|
13
14
|
include Scorched::Collection('after_filters', true)
|
|
14
15
|
include Scorched::Collection('error_filters')
|
|
15
16
|
|
|
@@ -108,7 +109,7 @@ module Scorched
|
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
def filters
|
|
111
|
-
@filters ||= {before: before_filters, after: after_filters, error: error_filters}
|
|
112
|
+
@filters ||= {before: before_filters, around: around_filters, after: after_filters, error: error_filters}
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
def call(env)
|
|
@@ -213,6 +214,16 @@ module Scorched
|
|
|
213
214
|
filter(:after, force: force, conditions: conditions, &block)
|
|
214
215
|
end
|
|
215
216
|
|
|
217
|
+
# Syntactic sugar for defining an around filter.
|
|
218
|
+
# The block is passed a single argument - a proc representing the rest of the filter chain (including the
|
|
219
|
+
# eventual dispatch to before/matching/after processing). The block must call it (e.g. `cont.call`) for
|
|
220
|
+
# request handling to proceed; if it doesn't, the wrapped chain simply never runs.
|
|
221
|
+
# Around filters defined earlier (including those defined in superclasses) wrap those defined later, with the
|
|
222
|
+
# innermost proc being the actual before/dispatch/after cycle.
|
|
223
|
+
def around(**conditions, &block)
|
|
224
|
+
filter(:around, conditions: conditions, &block)
|
|
225
|
+
end
|
|
226
|
+
|
|
216
227
|
# Syntactic sugar for defining an error filter.
|
|
217
228
|
# Takes one or more optional exception classes for which this error filter should handle. Handles all exceptions
|
|
218
229
|
# by default.
|
|
@@ -286,26 +297,29 @@ module Scorched
|
|
|
286
297
|
end
|
|
287
298
|
end
|
|
288
299
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
300
|
+
if config[:strip_trailing_slash] == :redirect && request.path =~ %r{[^/]/+$}
|
|
301
|
+
query_string = request.query_string.empty? ? '' : '?' << request.query_string
|
|
302
|
+
redirect(request.path.chomp('/') + query_string, status: 307, halt: false)
|
|
303
|
+
return response.finish
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
run_around_filters do
|
|
307
|
+
begin
|
|
308
|
+
pass if config[:auto_pass] && eligable_matches.empty?
|
|
309
|
+
|
|
310
|
+
if run_ba_filters(:before)
|
|
311
|
+
catch(:halt) {
|
|
312
|
+
begin
|
|
313
|
+
try_matches
|
|
314
|
+
rescue => inner_error
|
|
315
|
+
rescue_block.call(inner_error)
|
|
316
|
+
end
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
run_ba_filters(:after)
|
|
320
|
+
rescue => outer_error
|
|
321
|
+
outer_error == inner_error ? raise : catch(:halt) { rescue_block.call(outer_error) }
|
|
305
322
|
end
|
|
306
|
-
run_filters(:after)
|
|
307
|
-
rescue => outer_error
|
|
308
|
-
outer_error == inner_error ? raise : catch(:halt) { rescue_block.call(outer_error) }
|
|
309
323
|
end
|
|
310
324
|
response.finish
|
|
311
325
|
end
|
|
@@ -328,6 +342,9 @@ module Scorched
|
|
|
328
342
|
# Overriding this method provides the opportunity for one to have more control over how mapping targets are invoked.
|
|
329
343
|
def dispatch(match)
|
|
330
344
|
@_dispatched = true
|
|
345
|
+
if String === match.mapping[:target] || Symbol === match.mapping[:target]
|
|
346
|
+
match.mapping[:target] = Object.const_get(match.mapping[:target]) # Resolve symbol to constant, and replace target to avoid future lookups
|
|
347
|
+
end
|
|
331
348
|
target = match.mapping[:target]
|
|
332
349
|
response.merge! begin
|
|
333
350
|
if Proc === target
|
|
@@ -556,9 +573,10 @@ module Scorched
|
|
|
556
573
|
else
|
|
557
574
|
env['scorched.root_path']
|
|
558
575
|
end
|
|
559
|
-
abs
|
|
576
|
+
abs = "/#{abs}" unless abs[0] == '/'
|
|
560
577
|
abs
|
|
561
578
|
end
|
|
579
|
+
|
|
562
580
|
|
|
563
581
|
# We always want this filter to run at the end-point controller, hence we include the conditions within the body of
|
|
564
582
|
# the filter.
|
|
@@ -595,21 +613,48 @@ module Scorched
|
|
|
595
613
|
|
|
596
614
|
private
|
|
597
615
|
|
|
598
|
-
#
|
|
599
|
-
|
|
616
|
+
# Wraps the given block with the chain of around filters, outermost first, and calls it. Each filter is passed
|
|
617
|
+
# a proc representing the rest of the chain (the next around filter, or the wrapped block if there are no more),
|
|
618
|
+
# and is responsible for calling it to continue processing the request.
|
|
619
|
+
# As with before/after filters, a filter inherited by both an outer and inner controller (e.g. via `controller`
|
|
620
|
+
# sub-mapping) only runs once per request, tracked via the shared `env`.
|
|
621
|
+
def run_around_filters(&core)
|
|
622
|
+
tracker = filter_tracker(:around)
|
|
623
|
+
chain = filters[:around].to_a.reverse.reduce(core) do |inner, f|
|
|
624
|
+
proc do
|
|
625
|
+
if tracker.include?(f) || check_for_failed_condition(f[:conditions])
|
|
626
|
+
inner.call
|
|
627
|
+
else
|
|
628
|
+
tracker << f
|
|
629
|
+
instance_exec(inner, &f[:proc])
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
catch(:halt) { chain.call }
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Returns false if any of the before/after filters halted the request. True otherwise.
|
|
637
|
+
def run_ba_filters(type)
|
|
600
638
|
halted = false
|
|
601
|
-
tracker =
|
|
602
|
-
filters[type].reject { |f| tracker
|
|
639
|
+
tracker = filter_tracker(type)
|
|
640
|
+
filters[type].reject { |f| tracker.include?(f) }.each do |f|
|
|
603
641
|
unless check_for_failed_condition(f[:conditions]) || (halted && !f[:force])
|
|
604
|
-
tracker
|
|
605
|
-
halted = true unless
|
|
642
|
+
tracker << f
|
|
643
|
+
halted = true unless run_ba_filter(f)
|
|
606
644
|
end
|
|
607
645
|
end
|
|
608
646
|
!halted
|
|
609
647
|
end
|
|
610
648
|
|
|
611
|
-
# Returns
|
|
612
|
-
|
|
649
|
+
# Returns the set of filters of the given type that have already executed for this request, tracked in `env` so
|
|
650
|
+
# an inherited filter isn't run twice across nested controller dispatches.
|
|
651
|
+
def filter_tracker(type)
|
|
652
|
+
tracker = env['scorched.executed_filters'] ||= {}
|
|
653
|
+
tracker[type] ||= Set.new
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Returns false if the before/after filter halted. True otherwise.
|
|
657
|
+
def run_ba_filter(f)
|
|
613
658
|
catch(:halt) do
|
|
614
659
|
instance_exec(&f[:proc])
|
|
615
660
|
return true
|
data/lib/scorched/version.rb
CHANGED
data/scorched.gemspec
CHANGED
|
@@ -18,6 +18,7 @@ Gem::Specification.new 'scorched', Scorched::VERSION do |s|
|
|
|
18
18
|
s.add_dependency 'rack-accept', '~> 0.4' # Used for Accept-Charset, Accept-Encoding and Accept-Language headers.
|
|
19
19
|
s.add_dependency 'scorched-accept', '~> 0.1' # Used for Accept header.
|
|
20
20
|
s.add_dependency 'tilt', '~> 2.0'
|
|
21
|
+
s.add_dependency 'logger', '~> 1.7'
|
|
21
22
|
s.add_development_dependency 'rack-test', '>= 1.0'
|
|
22
23
|
s.add_development_dependency 'rspec', '>= 3.4'
|
|
23
24
|
s.add_development_dependency 'rake', '>= 10.4'
|
data/spec/controller_spec.rb
CHANGED
|
@@ -421,7 +421,7 @@ module Scorched
|
|
|
421
421
|
end
|
|
422
422
|
end
|
|
423
423
|
|
|
424
|
-
describe "before/after filters" do
|
|
424
|
+
describe "before/after/around filters" do
|
|
425
425
|
they "run directly before and after the target action" do
|
|
426
426
|
order = []
|
|
427
427
|
app.get('/') { order << :action }
|
|
@@ -540,6 +540,101 @@ module Scorched
|
|
|
540
540
|
end
|
|
541
541
|
end
|
|
542
542
|
|
|
543
|
+
describe "after filters" do
|
|
544
|
+
they "wrap around the before/action/after cycle, outermost first" do
|
|
545
|
+
order = []
|
|
546
|
+
app.around { |cont| order << :first_before; cont.call; order << :first_after }
|
|
547
|
+
app.around { |cont| order << :second_before; cont.call; order << :second_after }
|
|
548
|
+
app.before { order << :before }
|
|
549
|
+
app.after { order << :after }
|
|
550
|
+
app.get('/') { order << :action }
|
|
551
|
+
rt.get('/')
|
|
552
|
+
order.should == %i{first_before second_before before action after second_after first_after}
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
they "run in the context of the controller (same as the route)" do
|
|
556
|
+
route_instance = nil
|
|
557
|
+
around_instance = nil
|
|
558
|
+
app.get('/') { route_instance = self }
|
|
559
|
+
app.around { |cont| around_instance = self; cont.call }
|
|
560
|
+
rt.get('/')
|
|
561
|
+
route_instance.should == around_instance
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
they "run even if no route matches" do
|
|
565
|
+
counter = 0
|
|
566
|
+
app.around { |cont| counter += 1; cont.call }
|
|
567
|
+
rt.delete('/').status.should == 404
|
|
568
|
+
counter.should == 1
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
they "can take an optional set of conditions" do
|
|
572
|
+
counter = 0
|
|
573
|
+
app.around(method: ['GET', 'PUT']) { |cont| counter += 1; cont.call }
|
|
574
|
+
app.get('/') { }
|
|
575
|
+
app.put('/') { }
|
|
576
|
+
rt.post('/')
|
|
577
|
+
rt.get('/')
|
|
578
|
+
rt.put('/')
|
|
579
|
+
counter.should == 2
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
they "skip the target action, and everything else in the chain, if the continuation is never called" do
|
|
583
|
+
action_called = false
|
|
584
|
+
app.before { }
|
|
585
|
+
app.around { |cont| }
|
|
586
|
+
app.get('/') { action_called = true }
|
|
587
|
+
rt.get('/').status.should == 200
|
|
588
|
+
action_called.should == false
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
they "still unwind (run code after the continuation) even if the action halts" do
|
|
592
|
+
order = []
|
|
593
|
+
app.around { |cont| order << :before; cont.call; order << :after }
|
|
594
|
+
app.get('/') { order << :action; halt 200, 'stopped' }
|
|
595
|
+
response = rt.get('/')
|
|
596
|
+
response.body.should == 'stopped'
|
|
597
|
+
order.should == %i{before action after}
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
they "still unwind even if the action raises, once the error is handled" do
|
|
601
|
+
order = []
|
|
602
|
+
app.around { |cont| order << :before; cont.call; order << :after }
|
|
603
|
+
app.get('/') { raise 'boom' }
|
|
604
|
+
app.error { |e| order << :error; halt 500 }
|
|
605
|
+
rt.get('/').status.should == 500
|
|
606
|
+
order.should == %i{before error after}
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
describe "nesting" do
|
|
610
|
+
example "around filters inherit but only run once" do
|
|
611
|
+
around_counter = 0
|
|
612
|
+
app.around { |cont| around_counter += 1; cont.call }
|
|
613
|
+
subcontroller = app.controller { get('/') { 'wow' } }
|
|
614
|
+
subcontroller.filters[:around].should == app.filters[:around]
|
|
615
|
+
|
|
616
|
+
rt.get('/')
|
|
617
|
+
around_counter.should == 1
|
|
618
|
+
|
|
619
|
+
# Hitting the subcontroller directly should yield the same results.
|
|
620
|
+
around_counter = 0
|
|
621
|
+
Rack::Test::Session.new(subcontroller).get('/')
|
|
622
|
+
around_counter.should == 1
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
example "around filters wrap from outermost to innermost" do
|
|
626
|
+
order = []
|
|
627
|
+
app.around { |cont| order << :outer_before; cont.call; order << :outer_after }
|
|
628
|
+
app.controller do
|
|
629
|
+
around { |cont| order << :inner_before; cont.call; order << :inner_after }
|
|
630
|
+
get('/') { order << :action }
|
|
631
|
+
end
|
|
632
|
+
rt.get('/')
|
|
633
|
+
order.should == %i{outer_before inner_before action inner_after outer_after}
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
543
638
|
describe "error filters" do
|
|
544
639
|
let(:app) do
|
|
545
640
|
Class.new(Scorched::Controller) do
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scorched
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Wardrop
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rack
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '2.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: logger
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.7'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.7'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: rack-test
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -202,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
202
216
|
- !ruby/object:Gem::Version
|
|
203
217
|
version: '0'
|
|
204
218
|
requirements: []
|
|
205
|
-
rubygems_version:
|
|
219
|
+
rubygems_version: 4.0.13
|
|
206
220
|
specification_version: 4
|
|
207
221
|
summary: Light-weight, DRY as a desert, web framework for Ruby
|
|
208
222
|
test_files:
|