scorched 0.27 → 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|