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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5df1a902354207de82c8dcb0a6f00c68d9455d20edc05b3e70a5ef2c806a229a
4
- data.tar.gz: 2bd5dac53da50545d9d6f0d832e7928aeefb07ebfcbbd1749c300b0fd71355e4
3
+ metadata.gz: 9e9063c14895226f457fcb9255e206f5915a8aaefde84be0114dabbf4723a677
4
+ data.tar.gz: e773ac700d17abe3b3ce00f94be2665e4753bacffee9ce07fc4e86a00f35afec
5
5
  SHA512:
6
- metadata.gz: 3becda9a8c64fd085500003132cd5bd982d3ba6b67c33c5cabe9d4011377df4072f76e79baedd34a2544ee1546c105305e506dd1b2c5347844fd3742abf396a4
7
- data.tar.gz: 51988a63c584113976d5a99080174d0d008e08f91f1f1aac17d74b25d353e514e54a7a1a1ea95385a96fdba187846a543be82b933721936387fd9b439ceef849
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
- * `:handled` - Whether a mapping in the controller instance was invoked as the target for the request. A mapping that _passes_ a request is not considered a match.
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.
@@ -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
- begin
290
- if config[:strip_trailing_slash] == :redirect && request.path =~ %r{[^/]/+$}
291
- query_string = request.query_string.empty? ? '' : '?' << request.query_string
292
- redirect(request.path.chomp('/') + query_string, status: 307, halt: false)
293
- return response.finish
294
- end
295
- pass if config[:auto_pass] && eligable_matches.empty?
296
-
297
- if run_filters(:before)
298
- catch(:halt) {
299
- begin
300
- try_matches
301
- rescue => inner_error
302
- rescue_block.call(inner_error)
303
- end
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.insert(0, '/') unless abs[0] == '/'
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
- # Returns false if any of the filters halted the request. True otherwise.
599
- def run_filters(type)
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 = env['scorched.executed_filters'] ||= {before: Set.new, after: Set.new}
602
- filters[type].reject { |f| tracker[type].include?(f) }.each do |f|
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[type] << f
605
- halted = true unless run_filter(f)
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 false if the filter halted. True otherwise.
612
- def run_filter(f)
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
@@ -1,3 +1,3 @@
1
1
  module Scorched
2
- VERSION = '1.1.0.pre'
2
+ VERSION = '1.1.1'
3
3
  end
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'
@@ -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.0.pre
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: 2025-02-21 00:00:00.000000000 Z
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: 3.6.5
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: