scorched 1.0.3 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca89aa0414bd6f8f6ac42afd5fb7816198b4a9bdbf6b008cd32dbd5dcac89c7e
4
- data.tar.gz: 34b43e1c6975f32954c52aeaccb21ebd797b31abd5adeaeb2430dd0cdf5ae848
3
+ metadata.gz: 7a838c96acf81063ddc76cc2a26fe3497b7fd83de1b095ba4d9cef042835b9ed
4
+ data.tar.gz: 0de14593266fcf5df232a6f56cc89ca4de8d9b7de9d8a4df2fbc173974d55aca
5
5
  SHA512:
6
- metadata.gz: 752e607e45d78a21de7acaca053cca0d5f2b0b3b73f485c72a9c4779302c982fbf56496b248132ff7b4a895f2d50da2cfb579b7e6f7a7a008a4e5dd189eb4b8d
7
- data.tar.gz: a0680f3c093aff1b1de0e612f3bffb052e1fdb3d4d39b54b8838a2ae295a26bb65fabe7263d4dd3239ea5687014ed772fd5d9a10f0886260f27a9d435d4179d4
6
+ metadata.gz: a76db1e57fc1a1a62428e3e3e728ecc4eeee9d504579e5a7894c4e07ed0d819306f0ea2b73725e605e129ee188d864e1cbaf145c2e7eda6e1aea39e4f51ef930
7
+ data.tar.gz: 7b7185d2a622ac50111c0e3fbd5104e962ae2afc26c9058d07493b1a3666111bbf2b274c8afef1967697f319a268f1ff97d94511914509de4ed1bd4fd008b872
data/CHANGES.md CHANGED
@@ -1,6 +1,23 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ### v1.1.0
5
+ * 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:
6
+ ```
7
+ around do |cont|
8
+ begin
9
+ cont.call
10
+ ensure
11
+ # Ensure something important happens here
12
+ end
13
+ end
14
+ ```
15
+ * Compatible with Ruby 4.0
16
+
17
+ ### v1.1.0.pre
18
+ * Updated dependancies
19
+ * Now compatible with (and requires) Rack 3.x
20
+ * No longer escapes "+" into space characters
4
21
  ### v1.0.3
5
22
  * Fixed issue where routes with a `media_type` condition were prioritised over those without for HTTP requests accepting "*/*"
6
23
  ### v1.0.2
@@ -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
@@ -595,21 +612,48 @@ module Scorched
595
612
 
596
613
  private
597
614
 
598
- # Returns false if any of the filters halted the request. True otherwise.
599
- def run_filters(type)
615
+ # Wraps the given block with the chain of around filters, outermost first, and calls it. Each filter is passed
616
+ # a proc representing the rest of the chain (the next around filter, or the wrapped block if there are no more),
617
+ # and is responsible for calling it to continue processing the request.
618
+ # As with before/after filters, a filter inherited by both an outer and inner controller (e.g. via `controller`
619
+ # sub-mapping) only runs once per request, tracked via the shared `env`.
620
+ def run_around_filters(&core)
621
+ tracker = filter_tracker(:around)
622
+ chain = filters[:around].to_a.reverse.reduce(core) do |inner, f|
623
+ proc do
624
+ if tracker.include?(f) || check_for_failed_condition(f[:conditions])
625
+ inner.call
626
+ else
627
+ tracker << f
628
+ instance_exec(inner, &f[:proc])
629
+ end
630
+ end
631
+ end
632
+ catch(:halt) { chain.call }
633
+ end
634
+
635
+ # Returns false if any of the before/after filters halted the request. True otherwise.
636
+ def run_ba_filters(type)
600
637
  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|
638
+ tracker = filter_tracker(type)
639
+ filters[type].reject { |f| tracker.include?(f) }.each do |f|
603
640
  unless check_for_failed_condition(f[:conditions]) || (halted && !f[:force])
604
- tracker[type] << f
605
- halted = true unless run_filter(f)
641
+ tracker << f
642
+ halted = true unless run_ba_filter(f)
606
643
  end
607
644
  end
608
645
  !halted
609
646
  end
610
647
 
611
- # Returns false if the filter halted. True otherwise.
612
- def run_filter(f)
648
+ # Returns the set of filters of the given type that have already executed for this request, tracked in `env` so
649
+ # an inherited filter isn't run twice across nested controller dispatches.
650
+ def filter_tracker(type)
651
+ tracker = env['scorched.executed_filters'] ||= {}
652
+ tracker[type] ||= Set.new
653
+ end
654
+
655
+ # Returns false if the before/after filter halted. True otherwise.
656
+ def run_ba_filter(f)
613
657
  catch(:halt) do
614
658
  instance_exec(&f[:proc])
615
659
  return true
@@ -33,7 +33,7 @@ module Scorched
33
33
  # The unescaped URL, excluding the escaped forward-slash and percent. The resulting string will always be safe
34
34
  # to unescape again in situations where the forward-slash or percent are expected and valid characters.
35
35
  def unescaped_path
36
- path_info.split(/(%25|%2F)/i).each_slice(2).map { |v, m| CGI.unescape(v) << (m || '') }.join('')
36
+ path_info.split(/(%25|%2F|\+)/i).each_slice(2).map { |v, m| CGI.unescape(v) << (m || '') }.join('')
37
37
  end
38
38
 
39
39
  private
@@ -5,10 +5,10 @@ module Scorched
5
5
  def merge!(response)
6
6
  return self if response == self
7
7
  if Rack::Response === response
8
- response = [response.status, response.header, response]
8
+ response = [response.status, response.headers, response]
9
9
  end
10
10
  self.status, self.body = response[0], response[2]
11
- self.header.merge!(response[1])
11
+ self.headers.merge!(response[1])
12
12
  self
13
13
  end
14
14
 
@@ -24,12 +24,12 @@ module Scorched
24
24
  self['Content-Type'] ||= 'text/html;charset=utf-8'
25
25
  @block = block if block
26
26
  if [204, 205, 304].include?(status.to_i)
27
- header.delete "Content-Type"
28
- header.delete "Content-Length"
27
+ delete_header "Content-Type"
28
+ delete_header "Content-Length"
29
29
  close
30
- [status.to_i, header, []]
30
+ [status.to_i, headers, []]
31
31
  else
32
- [status.to_i, header, body]
32
+ [status.to_i, headers, body]
33
33
  end
34
34
  end
35
35
 
@@ -12,7 +12,7 @@ module Scorched
12
12
  protected
13
13
 
14
14
  def file_server
15
- @file_server ||= Rack::File.new(@dir)
15
+ @file_server ||= Rack::Files.new(@dir)
16
16
  end
17
17
  end
18
18
  end
@@ -1,3 +1,3 @@
1
1
  module Scorched
2
- VERSION = '1.0.3'
2
+ VERSION = '1.1.0'
3
3
  end
data/lib/scorched.rb CHANGED
@@ -2,6 +2,7 @@ ENV['APP_ENV'] ||= ENV['RACK_ENV'] || 'development'
2
2
 
3
3
  # Gems
4
4
  require 'rack'
5
+ require 'rack/session'
5
6
  require 'rack/accept'
6
7
  require 'scorched/accept'
7
8
  require 'tilt'
data/scorched.gemspec CHANGED
@@ -13,10 +13,12 @@ Gem::Specification.new 'scorched', Scorched::VERSION do |s|
13
13
 
14
14
  s.required_ruby_version = '>= 2.0.0'
15
15
 
16
- s.add_dependency 'rack', '~> 2.0'
16
+ s.add_dependency 'rack', '~> 3.0'
17
+ s.add_dependency 'rack-session', '~> 2.1'
17
18
  s.add_dependency 'rack-accept', '~> 0.4' # Used for Accept-Charset, Accept-Encoding and Accept-Language headers.
18
19
  s.add_dependency 'scorched-accept', '~> 0.1' # Used for Accept header.
19
20
  s.add_dependency 'tilt', '~> 2.0'
21
+ s.add_dependency 'logger', '~> 1.7'
20
22
  s.add_development_dependency 'rack-test', '>= 1.0'
21
23
  s.add_development_dependency 'rspec', '>= 3.4'
22
24
  s.add_development_dependency 'rake', '>= 10.4'
@@ -1,4 +1,5 @@
1
1
  require_relative './helper.rb'
2
+ require 'securerandom'
2
3
 
3
4
  module Scorched
4
5
  describe Controller do
@@ -72,7 +73,7 @@ module Scorched
72
73
  response.status.should == 200
73
74
  end
74
75
 
75
- it "unescapes all characters except for the forward-slash and percent sign" do
76
+ it "unescapes all characters except for the forward-slash, percent sign and plus" do
76
77
  app.map pattern: '/a (quite) big fish', target: generic_handler
77
78
  rt.get('/a%20%28quite%29%20big%20fish').status.should == 200
78
79
  app.map pattern: '/article/100%25 big%2Fsmall', target: generic_handler
@@ -80,6 +81,13 @@ module Scorched
80
81
  app.map pattern: '/*$', target: generic_handler
81
82
  rt.get('/page%2Fabout').status.should == 200
82
83
  rt.get('/page/about').status.should == 404
84
+
85
+ app.get('/foo/*') { |x| x }
86
+ app.map pattern: '/bar', target: Class.new(Scorched::Controller) {
87
+ get('/*') { |x| x }
88
+ }
89
+ rt.get('/foo/hello%2Bworld').body.should == 'hello+world'
90
+ rt.get('/bar/hello%2Bworld').body.should == 'hello+world'
83
91
  end
84
92
 
85
93
  it "unmatched path doesn't always begin with a forward slash" do
@@ -342,7 +350,7 @@ module Scorched
342
350
  inner_env['SCRIPT_NAME'].should == '/article'
343
351
  inner_env['PATH_INFO'].should == '/name'
344
352
  end
345
-
353
+
346
354
  example "PATH_INFO and SCRIPT_NAME joined, should produce a full path" do
347
355
  app.controller '/article/' do
348
356
  get '/name' do
@@ -413,7 +421,7 @@ module Scorched
413
421
  end
414
422
  end
415
423
 
416
- describe "before/after filters" do
424
+ describe "before/after/around filters" do
417
425
  they "run directly before and after the target action" do
418
426
  order = []
419
427
  app.get('/') { order << :action }
@@ -532,6 +540,101 @@ module Scorched
532
540
  end
533
541
  end
534
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
+
535
638
  describe "error filters" do
536
639
  let(:app) do
537
640
  Class.new(Scorched::Controller) do
@@ -962,14 +1065,14 @@ module Scorched
962
1065
  app.get('/') { rack_session = session }
963
1066
  rt.get('/')
964
1067
  rack_session.should be_nil
965
- app.middleware << proc { use Rack::Session::Cookie, secret: 'test' }
1068
+ app.middleware << proc { use Rack::Session::Cookie, secret: SecureRandom.gen_random(64) }
966
1069
  rt.get('/')
967
1070
  rack_session.should be_a(Rack::Session::Abstract::SessionHash)
968
1071
  end
969
1072
 
970
1073
  describe "flash" do
971
1074
  before(:each) do
972
- app.middleware << proc { use Rack::Session::Cookie, secret: 'test' }
1075
+ app.middleware << proc { use Rack::Session::Cookie, secret: SecureRandom.gen_random(64) }
973
1076
  end
974
1077
 
975
1078
  it "keeps session variables that live for one page load" do
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scorched
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Wardrop
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-02-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rack
@@ -16,14 +15,28 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '2.0'
18
+ version: '3.0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: '2.0'
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rack-session
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
27
40
  - !ruby/object:Gem::Dependency
28
41
  name: rack-accept
29
42
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +79,20 @@ dependencies:
66
79
  - - "~>"
67
80
  - !ruby/object:Gem::Version
68
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'
69
96
  - !ruby/object:Gem::Dependency
70
97
  name: rack-test
71
98
  requirement: !ruby/object:Gem::Requirement
@@ -175,7 +202,6 @@ homepage: http://scorchedrb.com
175
202
  licenses:
176
203
  - MIT
177
204
  metadata: {}
178
- post_install_message:
179
205
  rdoc_options: []
180
206
  require_paths:
181
207
  - lib
@@ -190,8 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
216
  - !ruby/object:Gem::Version
191
217
  version: '0'
192
218
  requirements: []
193
- rubygems_version: 3.3.7
194
- signing_key:
219
+ rubygems_version: 4.0.13
195
220
  specification_version: 4
196
221
  summary: Light-weight, DRY as a desert, web framework for Ruby
197
222
  test_files: