scorched 0.27 → 1.0.0.pre

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
  SHA1:
3
- metadata.gz: 492839ee866fd2c9d5eaf33de738c1661008ed93
4
- data.tar.gz: 2ce3ad605482f1d5a96973cddb4fa4ab910d4db7
3
+ metadata.gz: 88dac2edee54f46c4bea80fdb1e9d521ae23eb5e
4
+ data.tar.gz: 8aff4a99ef904555fe547b3f305731fa818c3d46
5
5
  SHA512:
6
- metadata.gz: 2cecfe761d1070e78f7b112dd158aed15768dbe3be19e9953c24c578b36b26487f3f4905632a4346326fe9d3eaa0a948dd7e9724c0aec6643b2bd2f80a02861e
7
- data.tar.gz: 9f21239c86254de3d08c1c03672a053d7a6163b4afafe2c6aa662801702dd36a77b5ed4e93934fce5f74c8d7bf212e84f6cf6757767b7636b5f6f0f374eca374
6
+ metadata.gz: aa17c2cb4ad6bd62eaee321941016471273c1a73a3344e459c57ccd65687aba9ff352b3674e193024afbc5b3f6f65da20b69a35a5a7fddb3446179731200253b
7
+ data.tar.gz: d68e85e3ac64e463ac503d2c6621fee4b60798a055f613e4604287faa80e1753fd2cb03ffa2cf7240b9365100d3c76d3666d165147bc8e04bcfa3e5853ba6aaa
data/CHANGES.md CHANGED
@@ -3,6 +3,11 @@ Changelog
3
3
 
4
4
  _Note that Scorched is yet to reach a v1.0 release. This means breaking changes may still be made. If upgrading the version of Scorched for your project, review this changelog carefully._
5
5
 
6
+ ### v1.0.0.pre
7
+ * Refactored `process` method. Now named `respond`, and breaks out to other methods. The logic surrounding filters and halting has also been modified, as is now more intuitive in my opinion.
8
+ * Removed `@_handled` instance variable, and the related `handled` condition.
9
+ * `redirect` method signature has changed.
10
+ * Scorched now depends on Rack 2.0 or above.
6
11
  ### v0.27
7
12
  * Fixed logic surrounding when a requested is considered "handled" (i.e. matched and dispatched) and exceptions that are raised after dispatch. In simpler terms, the `failed_condition` condition now has better logic in the event of an exception.
8
13
  ### v0.26
data/TODO.md CHANGED
@@ -17,4 +17,3 @@ Refactor Considerations
17
17
  If I undergo a significant refactor of Scorched, here's a list of things I'd like to improve:
18
18
 
19
19
  * Create a basic plugin system for optional features which may incur a performance and/or complexity overhead, e.g. Symbol matchers, content for. This will also negate the need for any kind of `contrib` library.
20
- * Make route dispatching more modular, perhaps by simply breaking it out into more individual methods, making it more practical to override default behaviour.
@@ -1,13 +1,18 @@
1
1
  require File.expand_path('../../lib/scorched.rb', __FILE__)
2
2
 
3
3
  class App < Scorched::Controller
4
- get '/:name' do
5
- @message = greeting(captures[:name])
6
- render :hello
4
+ get '/' do
5
+ 'root'
7
6
  end
8
7
 
9
- def greeting(name)
10
- "Howdy #{name}"
8
+ controller '/' do
9
+ get '/login' do
10
+ 'login'
11
+ end
12
+
13
+ get '/logout' do
14
+ 'logout'
15
+ end
11
16
  end
12
17
  end
13
18
 
@@ -55,7 +55,7 @@ module Scorched
55
55
  [*encodings].any? { |encoding| env['rack-accept.request'].encoding? encoding }
56
56
  },
57
57
  failed_condition: proc { |conditions|
58
- if !matches.empty? && matches.any? { |m| m.failed_condition } && !@_handled
58
+ if !matches.empty? && matches.any? { |m| m.failed_condition } && !@_dispatched
59
59
  matches.first.failed_condition && [*conditions].include?(matches.first.failed_condition[0])
60
60
  end
61
61
  },
@@ -71,9 +71,6 @@ module Scorched
71
71
  method: proc { |methods|
72
72
  [*methods].include?(request.request_method)
73
73
  },
74
- handled: proc { |bool|
75
- @_handled == bool
76
- },
77
74
  proc: proc { |blocks|
78
75
  [*blocks].all? { |b| instance_exec(&b) }
79
76
  },
@@ -118,7 +115,7 @@ module Scorched
118
115
  unless @instance_cache[key]
119
116
  builder = Rack::Builder.new
120
117
  to_load.each { |proc| builder.instance_exec(self, &proc) }
121
- builder.run(lambda { |env| self.new(env).process })
118
+ builder.run(lambda { |env| self.new(env).respond })
122
119
  @instance_cache[key] = builder.to_app
123
120
  end
124
121
  loaded.merge(to_load)
@@ -274,7 +271,8 @@ module Scorched
274
271
 
275
272
  # This is where the magic happens. Applies filters, matches mappings, applies error handlers, catches :halt and
276
273
  # :pass, etc.
277
- def process
274
+ # Returns a rack-compatible tuple
275
+ def respond
278
276
  inner_error = nil
279
277
  rescue_block = proc do |e|
280
278
  (env['rack.exception'] = e && raise) unless filters[:error].any? do |f|
@@ -285,61 +283,47 @@ module Scorched
285
283
  end
286
284
 
287
285
  begin
288
- catch(:halt) do
289
- if config[:strip_trailing_slash] == :redirect && request.path =~ %r{[^/]/+$}
290
- query_string = request.query_string.empty? ? '' : '?' << request.query_string
291
- redirect(request.path.chomp('/') + query_string, 307)
292
- end
293
- eligable_matches = matches.reject { |m| m.failed_condition }
294
- pass if config[:auto_pass] && eligable_matches.empty?
295
- run_filters(:before)
296
- begin
297
- # Re-order matches based on media_type, ensuring priority and definition order are respected appropriately.
298
- eligable_matches.each_with_index.sort_by { |m,idx|
299
- [
300
- m.mapping[:priority] || 0,
301
- [*m.mapping[:conditions][:media_type]].map { |type|
302
- env['scorched.accept'][:accept].rank(type, true)
303
- }.max || 0,
304
- -idx
305
- ]
306
- }.reverse.each { |match,idx|
307
- request.breadcrumb << match
308
- catch(:pass) {
309
- begin
310
- catch(:halt) do
311
- dispatch(match)
312
- end
313
- rescue
314
- @_handled = true
315
- raise
316
- end
317
- @_handled = true
318
- }
319
- break if @_handled
320
- request.breadcrumb.pop
321
- }
322
- unless @_handled
323
- response.status = (!matches.empty? && eligable_matches.empty?) ? 403 : 404
286
+ if config[:strip_trailing_slash] == :redirect && request.path =~ %r{[^/]/+$}
287
+ query_string = request.query_string.empty? ? '' : '?' << request.query_string
288
+ redirect(request.path.chomp('/') + query_string, status: 307, halt: false)
289
+ return response.finish
290
+ end
291
+ pass if config[:auto_pass] && eligable_matches.empty?
292
+
293
+ if run_filters(:before)
294
+ catch(:halt) {
295
+ begin
296
+ try_matches
297
+ rescue => inner_error
298
+ rescue_block.call(inner_error)
324
299
  end
325
- rescue => inner_error
326
- rescue_block.call(inner_error)
327
- end
328
- run_filters(:after)
329
- true
330
- end || begin
331
- run_filters(:before, true)
332
- run_filters(:after, true)
300
+ }
333
301
  end
302
+ run_filters(:after)
334
303
  rescue => outer_error
335
304
  outer_error == inner_error ? raise : catch(:halt) { rescue_block.call(outer_error) }
336
305
  end
337
306
  response.finish
338
307
  end
339
308
 
309
+ # Tries to dispatch to each eligable match. If the first match _passes_, tries the second match and so on.
310
+ # If there are no eligable matches, or all eligable matches pass, an appropriate 4xx response status is set.
311
+ def try_matches
312
+ eligable_matches.each do |match,idx|
313
+ request.breadcrumb << match
314
+ catch(:pass) {
315
+ dispatch(match)
316
+ return true
317
+ }
318
+ request.breadcrumb.pop # Current match passed, so pop the breadcrumb before the next iteration.
319
+ end
320
+ response.status = (!matches.empty? && eligable_matches.empty?) ? 403 : 404
321
+ end
322
+
340
323
  # Dispatches the request to the matched target.
341
- # Overriding this method provides the oppurtunity for one to have more control over how mapping targets are invoked.
324
+ # Overriding this method provides the opportunity for one to have more control over how mapping targets are invoked.
342
325
  def dispatch(match)
326
+ @_dispatched = true
343
327
  target = match.mapping[:target]
344
328
  response.merge! begin
345
329
  if Proc === target
@@ -359,24 +343,41 @@ module Scorched
359
343
  # The `:eligable` attribute of the `Match` object indicates whether the conditions for that mapping passed.
360
344
  # The result is cached for the life time of the controller instance, for the sake of effecient recalling.
361
345
  def matches
362
- return @_matches if @_matches
363
- to_match = request.unmatched_path
364
- to_match = to_match.chomp('/') if config[:strip_trailing_slash] == :ignore && to_match =~ %r{./$}
365
- @_matches = mappings.map { |mapping|
366
- mapping[:pattern].match(to_match) do |match_data|
367
- if match_data.pre_match == ''
368
- if match_data.names.empty?
369
- captures = match_data.captures
370
- else
371
- captures = Hash[match_data.names.map {|v| v.to_sym}.zip(match_data.captures)]
372
- captures.each do |k,v|
373
- captures[k] = symbol_matchers[k][1].call(v) if Array === symbol_matchers[k]
346
+ @_matches ||= begin
347
+ to_match = request.unmatched_path
348
+ to_match = to_match.chomp('/') if config[:strip_trailing_slash] == :ignore && to_match =~ %r{./$}
349
+ mappings.map { |mapping|
350
+ mapping[:pattern].match(to_match) do |match_data|
351
+ if match_data.pre_match == ''
352
+ if match_data.names.empty?
353
+ captures = match_data.captures
354
+ else
355
+ captures = Hash[match_data.names.map {|v| v.to_sym}.zip(match_data.captures)]
356
+ captures.each do |k,v|
357
+ captures[k] = symbol_matchers[k][1].call(v) if Array === symbol_matchers[k]
358
+ end
374
359
  end
360
+ Match.new(mapping, captures, match_data.to_s, check_for_failed_condition(mapping[:conditions]))
375
361
  end
376
- Match.new(mapping, captures, match_data.to_s, check_for_failed_condition(mapping[:conditions]))
377
362
  end
378
- end
379
- }.compact
363
+ }.compact
364
+ end
365
+ end
366
+
367
+ # Returns an ordered list of eligable matches.
368
+ # Orders matches based on media_type, ensuring priority and definition order are respected appropriately.
369
+ # Sorts by mapping priority first, media type appropriateness second, and definition order third.
370
+ def eligable_matches
371
+ @_eligable_matches ||= begin
372
+ matches.select { |m| m.failed_condition.nil? }.each_with_index.sort_by do |m,idx|
373
+ priority = m.mapping[:priority] || 0
374
+ media_type_rank = [*m.mapping[:conditions][:media_type]].map { |type|
375
+ env['scorched.accept'][:accept].rank(type, true)
376
+ }.max || 0
377
+ order = -idx
378
+ [priority, media_type_rank, order]
379
+ end.reverse
380
+ end
380
381
  end
381
382
 
382
383
  # Tests the given conditions, returning the name of the first failed condition, or nil otherwise.
@@ -397,9 +398,10 @@ module Scorched
397
398
  end
398
399
 
399
400
  # Redirects to the specified path or URL. An optional HTTP status is also accepted.
400
- def redirect(url, status = (env['HTTP_VERSION'] == 'HTTP/1.1') ? 303 : 302)
401
+ def redirect(url, status: (env['HTTP_VERSION'] == 'HTTP/1.1') ? 303 : 302, halt: true)
401
402
  response['Location'] = absolute(url)
402
- halt(status)
403
+ response.status = status
404
+ self.halt if halt
403
405
  end
404
406
 
405
407
  # call-seq:
@@ -523,10 +525,10 @@ module Scorched
523
525
 
524
526
  # Takes an optional URL, relative to the applications root, and returns a fully qualified URL.
525
527
  # Example: url('/example?show=30') #=> https://localhost:9292/myapp/example?show=30
526
- def url(path = nil)
528
+ def url(path = nil, scheme: nil)
527
529
  return path if path && URI.parse(path).scheme
528
530
  uri = URI::Generic.build(
529
- scheme: env['rack.url_scheme'],
531
+ scheme: scheme || env['rack.url_scheme'],
530
532
  host: env['SERVER_NAME'],
531
533
  port: env['SERVER_PORT'].to_i,
532
534
  path: env['scorched.root_path'],
@@ -588,20 +590,26 @@ module Scorched
588
590
 
589
591
  private
590
592
 
591
- def run_filters(type, forced_only = false)
593
+ # Returns false if any of the filters halted the request. True otherwise.
594
+ def run_filters(type)
595
+ halted = false
592
596
  tracker = env['scorched.executed_filters'] ||= {before: Set.new, after: Set.new}
593
- filters[type].reject{ |f| tracker[type].include?(f) || (forced_only && !f[:force]) }.each do |f|
594
- unless check_for_failed_condition(f[:conditions])
597
+ filters[type].reject { |f| tracker[type].include?(f) }.each do |f|
598
+ unless check_for_failed_condition(f[:conditions]) || (halted && !f[:force])
595
599
  tracker[type] << f
596
- if forced_only
597
- catch(:halt) do
598
- instance_exec(&f[:proc]); true
599
- end or log.warn "Ignored halt while running forced filters."
600
- else
601
- instance_exec(&f[:proc])
602
- end
600
+ halted = true unless run_filter(f)
603
601
  end
604
602
  end
603
+ !halted
604
+ end
605
+
606
+ # Returns false if the filter halted. True otherwise.
607
+ def run_filter(f)
608
+ catch(:halt) do
609
+ instance_exec(&f[:proc])
610
+ return true
611
+ end
612
+ return false
605
613
  end
606
614
 
607
615
  def log(type = nil, message = nil)
@@ -1,3 +1,3 @@
1
1
  module Scorched
2
2
  Match = Struct.new(:mapping, :captures, :path, :failed_condition)
3
- end
3
+ end
@@ -33,6 +33,8 @@ module Scorched
33
33
  end
34
34
  end
35
35
 
36
+ attr_accessor :halted
37
+
36
38
  alias :to_a :finish
37
39
  alias :to_ary :finish
38
40
  end
@@ -1,3 +1,3 @@
1
1
  module Scorched
2
- VERSION = '0.27'
2
+ VERSION = '1.0.0.pre'
3
3
  end
@@ -14,7 +14,7 @@ Gem::Specification.new 'scorched', Scorched::VERSION do |s|
14
14
 
15
15
  s.required_ruby_version = '>= 2.0.0'
16
16
 
17
- s.add_dependency 'rack', '~> 1.4'
17
+ s.add_dependency 'rack', '~> 2.0'
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', '~> 1.4'
@@ -189,7 +189,7 @@ module Scorched
189
189
 
190
190
  it "can coerce symbol-matched values" do
191
191
  app << {pattern: '/:numeric', target: proc { |env| [200, {}, [request.captures[:numeric].class.name]] }}
192
- rt.get('/45').body.should == 'Fixnum'
192
+ rt.get('/45').body.should == 'Integer'
193
193
  end
194
194
 
195
195
  it "matches routes based on priority, otherwise giving precedence to those defined first" do
@@ -681,15 +681,25 @@ module Scorched
681
681
  rt.get('/').status.should == 600
682
682
  end
683
683
 
684
+ it "prevents matches from being invoked if a before filter has halted" do
685
+ app.before { halt}
686
+ app.get('/') { "success" }
687
+ rt.get('/').body.should_not == 'success'
688
+ end
689
+
684
690
  describe "within filters" do
685
- it "short circuits filters if halted within filter" do
686
- app.before { halt }
691
+ it "only short circuits other filters of same type if halted within filter" do
687
692
  app.after { response.status = 600 }
688
- rt.get('/').status.should_not == 600
693
+ app.after { halt }
694
+ app.after { response.status = 601 }
695
+ rt.get('/').status.should == 600
696
+
697
+ app.before { halt }
698
+ rt.get('/').status.should == 600
689
699
  end
690
700
 
691
701
  it "forced filters are always run" do
692
- app.before { halt }
702
+ app.after { halt }
693
703
  app.after(force: true) { response.status = 600 }
694
704
  app.after { response.status = 700 } # Shouldn't run because it's not forced
695
705
  app.get('/') { 'hello' }
@@ -720,7 +730,7 @@ module Scorched
720
730
  end
721
731
 
722
732
  it "allows the HTTP status to be overridden" do
723
- app.get('/') { redirect '/somewhere', 308 }
733
+ app.get('/') { redirect '/somewhere', status: 308 }
724
734
  rt.get('/').status.should == 308
725
735
  end
726
736
 
@@ -736,9 +746,11 @@ module Scorched
736
746
 
737
747
  it "works in filters" do
738
748
  app.error { redirect '/somewhere' }
739
- app.get('/') { raise "Some error" }
740
- rt.get('/').location.should == '/somewhere'
749
+ app.get('/error') { raise "Some error" }
750
+ rt.get('/error').location.should == '/somewhere'
751
+
741
752
  app.before { redirect '/somewhere_else' }
753
+ app.get('/') { redirect '/meow' }
742
754
  rt.get('/').location.should == '/somewhere_else'
743
755
  end
744
756
  end
@@ -778,17 +790,6 @@ module Scorched
778
790
  }.to raise_error(ArgumentError)
779
791
  end
780
792
 
781
- it "is not considered a match if a mapping passes the request" do
782
- app.get('/*') { pass }
783
- app.get('/nopass') { }
784
- handled = nil
785
- app.after { handled = @_handled }
786
- rt.get('/').status.should == 404 # 404 if matched, but passed
787
- handled.should be_falsey
788
- rt.get('/nopass').status.should == 200
789
- handled.should be_truthy
790
- end
791
-
792
793
  it "is still considered a match if an exception is raised" do
793
794
  app.post('/') { }
794
795
  app.get('/') { raise "Test error"}
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scorched
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.27'
4
+ version: 1.0.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Wardrop
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-12 00:00:00.000000000 Z
11
+ date: 2017-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.4'
19
+ version: '2.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.4'
26
+ version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rack-accept
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -186,12 +186,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
186
186
  version: 2.0.0
187
187
  required_rubygems_version: !ruby/object:Gem::Requirement
188
188
  requirements:
189
- - - ">="
189
+ - - ">"
190
190
  - !ruby/object:Gem::Version
191
- version: '0'
191
+ version: 1.3.1
192
192
  requirements: []
193
193
  rubyforge_project:
194
- rubygems_version: 2.5.1
194
+ rubygems_version: 2.6.12
195
195
  signing_key:
196
196
  specification_version: 4
197
197
  summary: Light-weight, DRY as a desert, web framework for Ruby