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 +4 -4
- data/CHANGES.md +5 -0
- data/TODO.md +0 -1
- data/examples/simple.ru +10 -5
- data/lib/scorched/controller.rb +88 -80
- data/lib/scorched/match.rb +1 -1
- data/lib/scorched/response.rb +2 -0
- data/lib/scorched/version.rb +1 -1
- data/scorched.gemspec +1 -1
- data/spec/controller_spec.rb +20 -19
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88dac2edee54f46c4bea80fdb1e9d521ae23eb5e
|
4
|
+
data.tar.gz: 8aff4a99ef904555fe547b3f305731fa818c3d46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/examples/simple.ru
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
require File.expand_path('../../lib/scorched.rb', __FILE__)
|
2
2
|
|
3
3
|
class App < Scorched::Controller
|
4
|
-
get '
|
5
|
-
|
6
|
-
render :hello
|
4
|
+
get '/' do
|
5
|
+
'root'
|
7
6
|
end
|
8
7
|
|
9
|
-
|
10
|
-
|
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
|
|
data/lib/scorched/controller.rb
CHANGED
@@ -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 } && !@
|
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).
|
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
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
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
|
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
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
379
|
-
|
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
|
401
|
+
def redirect(url, status: (env['HTTP_VERSION'] == 'HTTP/1.1') ? 303 : 302, halt: true)
|
401
402
|
response['Location'] = absolute(url)
|
402
|
-
|
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
|
-
|
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)
|
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
|
-
|
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)
|
data/lib/scorched/match.rb
CHANGED
data/lib/scorched/response.rb
CHANGED
data/lib/scorched/version.rb
CHANGED
data/scorched.gemspec
CHANGED
@@ -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', '~>
|
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'
|
data/spec/controller_spec.rb
CHANGED
@@ -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 == '
|
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
|
-
|
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.
|
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:
|
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:
|
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: '
|
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: '
|
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:
|
191
|
+
version: 1.3.1
|
192
192
|
requirements: []
|
193
193
|
rubyforge_project:
|
194
|
-
rubygems_version: 2.
|
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
|