scorched 0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE +7 -0
- data/Milestones.md +65 -0
- data/README.md +78 -0
- data/docs/be_creative.md +32 -0
- data/docs/filters.md +8 -0
- data/docs/routing.md +29 -0
- data/docs/sharing_request_state.md +5 -0
- data/examples/media_types.rb +18 -0
- data/examples/media_types.ru +2 -0
- data/lib/scorched.rb +19 -0
- data/lib/scorched/collection.rb +60 -0
- data/lib/scorched/controller.rb +343 -0
- data/lib/scorched/dynamic_delegate.rb +22 -0
- data/lib/scorched/error.rb +5 -0
- data/lib/scorched/options.rb +54 -0
- data/lib/scorched/request.rb +34 -0
- data/lib/scorched/response.rb +18 -0
- data/lib/scorched/static.rb +16 -0
- data/lib/scorched/version.rb +3 -0
- data/lib/scorched/view_helpers.rb +39 -0
- data/scorched.gemspec +19 -0
- data/spec/collection_spec.rb +46 -0
- data/spec/controller_spec.rb +565 -0
- data/spec/helper.rb +44 -0
- data/spec/options_spec.rb +42 -0
- data/spec/public/static.txt +1 -0
- data/spec/request_spec.rb +2 -0
- data/spec/view_helpers_spec.rb +84 -0
- data/spec/views/composer.erb +1 -0
- data/spec/views/layout.erb +1 -0
- data/spec/views/main.erb +1 -0
- data/spec/views/other.str +1 -0
- data/spec/views/partial.erb +1 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7468f10cbee0373d243fd4d66dd081ea4b1cecc9
|
4
|
+
data.tar.gz: 6225d5221e70a5ba5f3b68d8c6db0144f34d9dfc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91385d8f1501c564b9ba510ab3d4faef763ea4d6cee6d79b2a23c74cf8511352c8b0c7443360031e8b4ae3149c33c216c4b4b7311eac03d3bee9ce348d075b81
|
7
|
+
data.tar.gz: e0d15fc7205e937a8249599833bf9b2a74c9e3abaa3db4decef548eb6292185b53d3da0eede09439efdf5b4deb205b9563d256ed4a93365c3b31270c8db6f5e1
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013 Tom Wardrop
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Milestones.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
Milestones
|
2
|
+
==========
|
3
|
+
|
4
|
+
Changelog
|
5
|
+
---------
|
6
|
+
|
7
|
+
### v0.5
|
8
|
+
* Implemented view rendering using Tilt.
|
9
|
+
* Added session method for convenience, and implemented helper for flash session data.
|
10
|
+
* Added cookie helper for conveniently setting, retrieving and deleting cookies.
|
11
|
+
* Static file serving actually works now
|
12
|
+
* Custom middleware Scorched::Static serves as a thin layer on top of Rack::File.
|
13
|
+
* Added specs for each configuration option.
|
14
|
+
* Using Ruby 2.0 features where applicable. No excuse not to be able to deploy on 2.0 by the time Scorched is ready for production.
|
15
|
+
* Keyword arguments instead of ``*args`` combined with ``Hash === args.last``.
|
16
|
+
* Replaced instances of __FILE__ with __dir__.
|
17
|
+
* Added expected Rack middleware, Rack::MethodOverride and Rack::Head.
|
18
|
+
|
19
|
+
### v0.4
|
20
|
+
* Make filters behave like middleware. Inheritable, but are only executed once.
|
21
|
+
* Improved implementation of Options and Collection classes
|
22
|
+
|
23
|
+
### v0.3 and earlier
|
24
|
+
* Basic request handling and routing
|
25
|
+
* String and Regex URL matching, with capture support
|
26
|
+
* Implemented route conditions
|
27
|
+
* Added HTTP method condition which the route helpers depend on.
|
28
|
+
* Added route helpers
|
29
|
+
* Implemented support for sub-controllers
|
30
|
+
* Implement before and after filters with proper execution order.
|
31
|
+
* Configuration inheritance between controllers. This has been implemented as the Options class.
|
32
|
+
* Mechanism for including Rack middleware.
|
33
|
+
* Added more route conditions e.g. content-type, language, user-agent, etc.
|
34
|
+
* Provide means to `halt` request.
|
35
|
+
* Added redirect helping for halting and redirecting request
|
36
|
+
* Mechanism for handling exceptions in routes and before/after filters.
|
37
|
+
* Added static resource serving. E.g. public folder.
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
Remaining
|
42
|
+
---------
|
43
|
+
Some of these remaining features may be broken out into a separate contributor library to keep the core lean and focused.
|
44
|
+
|
45
|
+
* Make specs for Collection and Options classes more thorough, e.g. test all non-reading modifiers such as clear, delete, etc.
|
46
|
+
* Add view helpers
|
47
|
+
* Add helper to easily read and build HTTP query strings. Takes care of "?" and "&" logic, escaping, etc. This is
|
48
|
+
intended to make link building easier.
|
49
|
+
* Form populator
|
50
|
+
* Provide a default error page somewhat similar to what Sinatra has.
|
51
|
+
* Add debug logging to show each routing hop and the current environment (variables, mode, etc)
|
52
|
+
* Environment optimised defaults
|
53
|
+
* Production
|
54
|
+
* Rack::Protection
|
55
|
+
* Disable static file serving
|
56
|
+
* Development
|
57
|
+
* Verbose logging to STDOUT
|
58
|
+
* use Rack::ShowExceptions
|
59
|
+
|
60
|
+
Unlikely
|
61
|
+
--------
|
62
|
+
* Mutex locking option? I'm of the opinion that the web server should be configured for the concurrency model of the application, rather than the framework.
|
63
|
+
|
64
|
+
|
65
|
+
More things will be added to these lists as they're thought of and considered.
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
Scorched
|
2
|
+
========
|
3
|
+
|
4
|
+
*Light-weight, DRY as a desert, web framework for Ruby. Inspired by Sinatra, this framework is my vision of the next evolutionary step in light-weight ruby web frameworks.*
|
5
|
+
|
6
|
+
Scorched honours the patrons of the past. It's not a complete reinvention of the lightweight web framework, but rather what I hope is an evolutionary enhancement. Most of the concepts are carried forward from predecessors. Scorched merely enhances those concepts in an attempt to extract their full potential, as well as offering up to scrutiny some entirely new idioms. All with the intention to make developing lightweight web apps in Ruby even more enjoyable.
|
7
|
+
|
8
|
+
The name 'Scorched' is inspired by the main goal of the project, which is to DRY-up what the likes of Sinatra and Padrino left moist.
|
9
|
+
|
10
|
+
|
11
|
+
The Errors of Our Past (aka. Areas of Moisty-ness)
|
12
|
+
--------------------------------------------------
|
13
|
+
I think the biggest mistake made by the predecessors of Scorched such as Sinatra/Padrino, was to not leverage the power
|
14
|
+
of the class. The consequences of this made for some awkwardness. Helpers are a classical reinvention of what
|
15
|
+
classes and modules were already made to solve. Scorched implements Controllers as Classes, which in addition to having their own DSL, allow defining and calling traditional class methods. Allowing developers to implement helpers and other common functionality as proper methods not only makes them more predictable and familiar, but of course allow such helpers to be inheritable via plain-old Class inheritance.
|
16
|
+
|
17
|
+
Perhaps another error (or area of sogginess, if you will) has been a lack of consideration for the hierarchical nature of websites, and the fact that sub-directories are often expected to inherit attributes of their parents. Scorched supports sub-controllers to any arbitrary depth, with each controllers filters and route conditions applied along the way. This can assist many areas of web development, including security, restful interfaces, and interchangeable output formats.
|
18
|
+
|
19
|
+
|
20
|
+
Design Philosophy
|
21
|
+
-----------------
|
22
|
+
Scorched has a relatively simple design philosophy. The main objective is to keep Scorched lean and generic. Scorched refrains from expressing too much opinion. The general idea behind Scorched is to give developers all the tools to quickly put together small, medium and perhaps even large websites and applications.
|
23
|
+
|
24
|
+
There's little need for a framework to be opinionated if the opinions of the developer can be quickly and easily built into it on a per-application basis. To do this effectively, developers really need to understand Scorched, and the best way to lower facilitate that is to lower the learning curve by keeping the core design, logical, predictable, and concise.
|
25
|
+
|
26
|
+
|
27
|
+
First Impressions
|
28
|
+
-----------------
|
29
|
+
Below I present a sample of the API as it currently stands:
|
30
|
+
|
31
|
+
class MyApp < Scorched::Controller
|
32
|
+
|
33
|
+
# From the most simple route possible...
|
34
|
+
get '/' do
|
35
|
+
"Hello World"
|
36
|
+
end
|
37
|
+
|
38
|
+
# To something that gets the muscle's flexing a little
|
39
|
+
route '/articles/:title/::opts', 2, methods: ['GET', 'POST'], content_type: :json do
|
40
|
+
# Do what you want in here. Note, the second argument is the optional route priority.
|
41
|
+
end
|
42
|
+
|
43
|
+
# Anonymous controllers allow for convenient route grouping to which filters and conditions can be applied
|
44
|
+
controller conditions: {content_type: :json} do
|
45
|
+
get '/articles/*' do |page|
|
46
|
+
{title: 'Scorched Rocks', body: '...', created_at: '27/08/2012', created_by: 'Bob'}
|
47
|
+
end
|
48
|
+
|
49
|
+
after do
|
50
|
+
response.to_json
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# The things you get for free by using Classes for Controllers (...that's directed at you Padrino)
|
55
|
+
def my_little_helper
|
56
|
+
# Do some crazy awesome stuff that no route can resist using.
|
57
|
+
end
|
58
|
+
|
59
|
+
# You can always avoid the routing helpers and add mappings manually. Anything that responds to #call is a valid target, with the only minor exception being that proc's are instance_exec'd, not #call'd.
|
60
|
+
self << {url: '/admin', priority: 10, target: My3rdPartyAdminApp}
|
61
|
+
self << {url: '**', conditions: {maintenance_mode: true}, target: proc { |env|
|
62
|
+
@request.body << 'Maintenance underway, please be patient.'
|
63
|
+
}}
|
64
|
+
end
|
65
|
+
|
66
|
+
This API shouldn't look too foreign to anyone familiar with frameworks like Sinatra, and the potential power at hand should be obvious. The `route` method demonstrates a few minor features of Scorched:
|
67
|
+
|
68
|
+
* Multi-method routes - Because sometimes the difference between a GET and POST can be a single line of code. If no methods are provided, the route receives all HTTP methods.
|
69
|
+
* Named Wildcards - Not an original idea, but you may note the named wildcard with the double colon. This maps to the '**' glob directive, which will span forward-slashes while matching. The single asterisk (or colon) behaves like the single asterisk glob directive, and will not match forward-slashes.
|
70
|
+
* Route priorities - Routes (referred to as mappings internally) can be assigned priorities. A priority can be any arbitrary number by which the routes are ordered. The higher the number, the higher the priority.
|
71
|
+
* Conditions - Conditions are merely procs defined on the controller which are inherited (and can be overriden) by child controllers. When a request comes in, mappings that match the requested URL, first have their conditions evaluated in the context of the controller instance, before control is handed off to the target associated with that mapping. It's a very simple implementation that comes with a lot of potential.
|
72
|
+
|
73
|
+
That should hopefully give you an example of how the core of Scorched is shaping up. This demonstrates only a subset of what Scorched will offer.
|
74
|
+
|
75
|
+
|
76
|
+
Development Progress
|
77
|
+
--------------------
|
78
|
+
Please refer to [Milestones.md](Milestones.md) for a breakdown of development progress.
|
data/docs/be_creative.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Effortless REST
|
2
|
+
---------------
|
3
|
+
An easy way to serve multiple content-types:
|
4
|
+
|
5
|
+
class App < Scorched::Controller
|
6
|
+
def view(view = nil)
|
7
|
+
view ? env['app.view'] = view : env['app.view']
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
if check_condition?(:media_type, 'text/html')
|
12
|
+
response.body = [render(view)]
|
13
|
+
if check_condition?(:media_type, 'application/json')
|
14
|
+
response['Content-type'] = 'application/json'
|
15
|
+
response.body = [response.body.to_json]
|
16
|
+
elsif check_condition?(:media_type, 'application/pdf')
|
17
|
+
response['Content-type'] = 'application/pdf'
|
18
|
+
# response.body = [render_pdf(view)]
|
19
|
+
else
|
20
|
+
response.body = [render(view)]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/' do
|
25
|
+
view :index
|
26
|
+
[
|
27
|
+
{title: 'Sweet Purple Unicorns', date: '08/03/2013'},
|
28
|
+
{title: 'Mellow Grass Men', date: '21/03/2013'}
|
29
|
+
]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
data/docs/filters.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
|
2
|
+
After Filters
|
3
|
+
-------------
|
4
|
+
|
5
|
+
|
6
|
+
Error Filters
|
7
|
+
-------------
|
8
|
+
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.
|
data/docs/routing.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Patterns
|
2
|
+
========
|
3
|
+
All patterns attempt to match the remaining unmatched portion of the request path (the request path being rack's
|
4
|
+
`path_info` variable). The unmatched path will always begin with a forward slash if the previously matched portion of the
|
5
|
+
path ended at a forward slash, regardless of whether it actually included the forward slash in the match, or if the
|
6
|
+
forward slash was the next character. As an example, if the request was to "/article/21", then both "/article/" => "/21"
|
7
|
+
and "/article" => "/21" would match.
|
8
|
+
|
9
|
+
All patterns match from the beginning of the path. Matches that occur beyond the beginning of the path won't count as a
|
10
|
+
match. So even though the pattern "article" would match "/article/21", it wouldn't count as a match because the match
|
11
|
+
didn't start at a non-zero offset.
|
12
|
+
|
13
|
+
If a pattern contains named captures, unnamed captures will be lost (this is how named regex captures work in Ruby). So
|
14
|
+
if you name one capture, make sure you name any other captures you may want to access.
|
15
|
+
|
16
|
+
String Patterns
|
17
|
+
---------------
|
18
|
+
* `*` - Matches all characters excluding the forward slash.
|
19
|
+
* `**` - Matches all characters including the forward slash.
|
20
|
+
* `:param` - Same as `*` except the capture is named to whatever the string following the single-colon is.
|
21
|
+
* `::param` - Same as `**` except the capture is named to whatever the string following the double-colon is.
|
22
|
+
* `$` - If placed at the end of a pattern, the pattern only matches if it matches the entire path. For routes, this is
|
23
|
+
implied, so it should only be explicitly added if trying to match a dollar sign character. If added anywhere other
|
24
|
+
than the end of the pattern, it will match the dollar sign character.
|
25
|
+
|
26
|
+
|
27
|
+
Regex Patterns
|
28
|
+
--------------
|
29
|
+
Regex patterns offer more power and flexibility than string patterns (naturally). To be continued... (captures, named captures, etc).
|
@@ -0,0 +1,5 @@
|
|
1
|
+
Managing Request State
|
2
|
+
----------------------
|
3
|
+
Because Scorched allows sub-controllers to an arbitrary depth and treats all controllers equal (i.e has no concept of a _root_ controller), it means instance variables cannot be used to track, maintain or share request state between controllers. I mention this because instance variables are the most common way to manage the request state in the likes of Sinatra.
|
4
|
+
|
5
|
+
As an example, consider the Scorched implementation of flash session data. This data must be shared between controllers throughout the life of the request. The solution here, which you can find by looking at the Scorched source code, is to use the Rack environment hash. It's the one object that's accessible throughout the entire life-cycle of a request, and it's what you should use to maintain request state between controllers and even other embedded Rack applications.
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative '../lib/scorched.rb'
|
3
|
+
|
4
|
+
class MediaTypesExample < Scorched::Controller
|
5
|
+
|
6
|
+
get '/', media_type: 'text/html' do
|
7
|
+
<<-HTML
|
8
|
+
<div><strong>Name: </strong> John Falkon</div>
|
9
|
+
<div><strong>Age: </strong> 39</div>
|
10
|
+
<div><strong>Occupation: </strong> Carpet Cleaner</div>
|
11
|
+
HTML
|
12
|
+
end
|
13
|
+
|
14
|
+
get '/,', media_type: 'application/json' do
|
15
|
+
{name: 'John Falkon', age: 39, occupation: 'Carpet Cleaner'}.to_json
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/lib/scorched.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Gems
|
2
|
+
require 'rack'
|
3
|
+
require 'rack/accept'
|
4
|
+
require 'tilt'
|
5
|
+
|
6
|
+
# Stdlib
|
7
|
+
require 'set'
|
8
|
+
require 'logger'
|
9
|
+
|
10
|
+
require_relative 'scorched/static'
|
11
|
+
require_relative 'scorched/dynamic_delegate'
|
12
|
+
require_relative 'scorched/options'
|
13
|
+
require_relative 'scorched/collection'
|
14
|
+
require_relative 'scorched/view_helpers'
|
15
|
+
require_relative 'scorched/controller'
|
16
|
+
require_relative 'scorched/error'
|
17
|
+
require_relative 'scorched/request'
|
18
|
+
require_relative 'scorched/response'
|
19
|
+
require_relative 'scorched/version'
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Scorched
|
2
|
+
class Collection < Set
|
3
|
+
# Redefine all methods as delegates of the underlying local set.
|
4
|
+
extend DynamicDelegate
|
5
|
+
alias_each(Set.instance_methods(false)) { |m| "_#{m}" }
|
6
|
+
delegate 'to_set', *Set.instance_methods(false).reject { |m|
|
7
|
+
[:<<, :add, :add?, :clear, :delete, :delete?, :delete_if, :merge, :replace, :subtract].include? m
|
8
|
+
}
|
9
|
+
|
10
|
+
# sets parent Collection object and returns self
|
11
|
+
def parent!(parent)
|
12
|
+
@parent = parent
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_set(inherit = true)
|
17
|
+
if inherit && (Set === @parent || Array === @parent)
|
18
|
+
# An important attribute of a Scorched::Collection is that the merged set is ordered from inner to outer.
|
19
|
+
Set.new.merge(self._to_a).merge(@parent.to_set)
|
20
|
+
else
|
21
|
+
Set.new.merge(self._to_a)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_a(inherit = true)
|
26
|
+
to_set(inherit).to_a
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
"#<#{self.class}: #{_inspect}, #{to_set.inspect}>"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
def Collection(accessor_name)
|
36
|
+
m = Module.new
|
37
|
+
m.class_eval <<-MOD
|
38
|
+
class << self
|
39
|
+
def included(klass)
|
40
|
+
klass.extend(ClassMethods)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
def #{accessor_name}
|
46
|
+
@#{accessor_name} || begin
|
47
|
+
parent = superclass.#{accessor_name} if superclass.respond_to?(:#{accessor_name}) && Scorched::Collection === superclass.#{accessor_name}
|
48
|
+
@#{accessor_name} = Collection.new.parent!(parent)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def #{accessor_name}(*args)
|
54
|
+
self.class.#{accessor_name}(*args)
|
55
|
+
end
|
56
|
+
MOD
|
57
|
+
m
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,343 @@
|
|
1
|
+
module Scorched
|
2
|
+
class Controller
|
3
|
+
include ViewHelpers
|
4
|
+
include Scorched::Options('config')
|
5
|
+
include Scorched::Options('view_config')
|
6
|
+
include Scorched::Options('conditions')
|
7
|
+
include Scorched::Collection('middleware')
|
8
|
+
include Scorched::Collection('before_filters')
|
9
|
+
include Scorched::Collection('after_filters')
|
10
|
+
include Scorched::Collection('error_filters')
|
11
|
+
|
12
|
+
config << {
|
13
|
+
:strip_trailing_slash => :redirect, # :redirect => Strips and redirects URL ending in forward slash, :ignore => internally ignores trailing slash, false => does nothing.
|
14
|
+
:static_dir => 'public', # The directory Scorched should serve static files from. Set to false if web server or anything else is serving static files.
|
15
|
+
:logger => Logger.new(STDOUT)
|
16
|
+
}
|
17
|
+
|
18
|
+
view_config << {
|
19
|
+
:dir => 'views', # The directory containing all the view templates, relative to the application root.
|
20
|
+
:layout => false, # The default layout template to use, relative to the view directory. Set to false for no default layout.
|
21
|
+
:engine => :erb
|
22
|
+
}
|
23
|
+
|
24
|
+
conditions << {
|
25
|
+
charset: proc { |charsets|
|
26
|
+
[*charsets].any? { |charset| request.env['rack-accept.request'].charset? charset }
|
27
|
+
},
|
28
|
+
encoding: proc { |encodings|
|
29
|
+
[*encodings].any? { |encoding| request.env['rack-accept.request'].encoding? encoding }
|
30
|
+
},
|
31
|
+
host: proc { |host|
|
32
|
+
(Regexp === host) ? host =~ request.host : host == request.host
|
33
|
+
},
|
34
|
+
language: proc { |languages|
|
35
|
+
[*languages].any? { |language| request.env['rack-accept.request'].language? language }
|
36
|
+
},
|
37
|
+
media_type: proc { |types|
|
38
|
+
[*types].any? { |type| request.env['rack-accept.request'].media_type? type }
|
39
|
+
},
|
40
|
+
methods: proc { |accepts|
|
41
|
+
[*accepts].include?(request.request_method)
|
42
|
+
},
|
43
|
+
user_agent: proc { |user_agent|
|
44
|
+
(Regexp === user_agent) ? user_agent =~ request.user_agent : user_agent == request.user_agent
|
45
|
+
},
|
46
|
+
status: proc { |statuses|
|
47
|
+
[*statuses].include?(response.status)
|
48
|
+
},
|
49
|
+
}
|
50
|
+
|
51
|
+
middleware << proc { |this|
|
52
|
+
use Rack::Head
|
53
|
+
use Rack::MethodOverride
|
54
|
+
use Rack::Accept
|
55
|
+
use Scorched::Static, :dir => this.config[:static_dir] if this.config[:static_dir]
|
56
|
+
use Rack::Logger, this.config[:logger] if this.config[:logger]
|
57
|
+
}
|
58
|
+
|
59
|
+
class << self
|
60
|
+
|
61
|
+
def mappings
|
62
|
+
@mappings ||= []
|
63
|
+
end
|
64
|
+
|
65
|
+
def filters
|
66
|
+
@filters ||= {before: before_filters, after: after_filters, error: error_filters}
|
67
|
+
end
|
68
|
+
|
69
|
+
def call(env)
|
70
|
+
loaded = env['scorched.middleware'] ||= Set.new
|
71
|
+
app = lambda do |env|
|
72
|
+
instance = self.new(env)
|
73
|
+
instance.action
|
74
|
+
end
|
75
|
+
|
76
|
+
builder = Rack::Builder.new
|
77
|
+
middleware.reject{ |v| loaded.include? v }.each do |proc|
|
78
|
+
builder.instance_exec(self, &proc)
|
79
|
+
loaded << proc
|
80
|
+
end
|
81
|
+
builder.run(app)
|
82
|
+
builder.call(env)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Generates and assigns mapping hash from the given arguments.
|
86
|
+
#
|
87
|
+
# Accepts the following keyword arguments:
|
88
|
+
# :url - The url pattern to match on. Required.
|
89
|
+
# :target - A proc to execute, or some other object that responds to #call. Required.
|
90
|
+
# :priority - Negative or positive integer for giving a priority to the mapped item.
|
91
|
+
# :conditions - A hash of condition:value pairs
|
92
|
+
# Raises ArgumentError if required key values are not provided.
|
93
|
+
def map(url: nil, priority: nil, conditions: {}, target: nil)
|
94
|
+
raise ArgumentError, "Mapping must specify url pattern and target" unless url && target
|
95
|
+
priority = priority.to_i
|
96
|
+
insert_pos = mappings.take_while { |v| priority <= v[:priority] }.length
|
97
|
+
mappings.insert(insert_pos, {
|
98
|
+
url: compile(url),
|
99
|
+
priority: priority,
|
100
|
+
conditions: conditions,
|
101
|
+
target: target
|
102
|
+
})
|
103
|
+
end
|
104
|
+
alias :<< :map
|
105
|
+
|
106
|
+
# Creates a new controller as a sub-class of self (by default), mapping it to self using the provided mapping
|
107
|
+
# hash if one is provided. Returns the new anonymous controller class.
|
108
|
+
#
|
109
|
+
# Takes two optional arguments and a block: a parent class from which the generated controller class inherits
|
110
|
+
# from, a mapping hash to automatically map the new controller, and of course a block which defines the
|
111
|
+
# controller class.
|
112
|
+
#
|
113
|
+
# It's worth noting, however obvious, that the resulting class will only be a controller if the parent class is
|
114
|
+
# (or inherits from) a Scorched::Controller.
|
115
|
+
def controller(parent_class = self, **mapping, &block)
|
116
|
+
c = Class.new(parent_class, &block)
|
117
|
+
self << {url: '/', target: c}.merge(mapping)
|
118
|
+
c
|
119
|
+
end
|
120
|
+
|
121
|
+
# Generates and returns a new route proc from the given block, and optionally maps said proc using the given args.
|
122
|
+
def route(url = nil, priority = nil, **conditions, &block)
|
123
|
+
target = lambda do |env|
|
124
|
+
env['rack.response'].body << instance_exec(*env['rack.request'].captures, &block)
|
125
|
+
env['rack.response']
|
126
|
+
end
|
127
|
+
self << {url: compile(url, true), priority: priority, conditions: conditions, target: target} if url
|
128
|
+
target
|
129
|
+
end
|
130
|
+
|
131
|
+
['get', 'post', 'put', 'delete', 'head', 'options', 'patch'].each do |method|
|
132
|
+
methods = (method == 'get') ? ['GET', 'HEAD'] : [method.upcase]
|
133
|
+
define_method(method) do |*args, **conditions, &block|
|
134
|
+
conditions.merge!(methods: methods)
|
135
|
+
route(*args, **conditions, &block)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def filter(type, *args, **conditions, &block)
|
140
|
+
filters[type.to_sym] << {args: args, conditions: conditions, proc: block}
|
141
|
+
end
|
142
|
+
|
143
|
+
# A bit of syntactic sugar for #filter.
|
144
|
+
['before', 'after', 'error'].each do |type|
|
145
|
+
define_method(type) do |*args, &block|
|
146
|
+
filter(type, *args, &block)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# Parses and compiles the given URL string pattern into a regex if not already, returning the resulting regexp
|
153
|
+
# object. Accepts an optional _match_to_end_ argument which will ensure the generated pattern matches to the end
|
154
|
+
# of the string.
|
155
|
+
def compile(url, match_to_end = false)
|
156
|
+
return url if Regexp === url
|
157
|
+
raise Error, "Can't compile URL of type #{url.class}. Must be String or Regexp." unless String === url
|
158
|
+
match_to_end = !!url.sub!(/\$$/, '') || match_to_end
|
159
|
+
pattern = url.split(%r{(\*{1,2}|(?<!\\):{1,2}[^/*$]+)}).each_slice(2).map { |unmatched, match|
|
160
|
+
Regexp.escape(unmatched) << begin
|
161
|
+
if %w{* **}.include? match
|
162
|
+
match == '*' ? "([^/]+)" : "(.+)"
|
163
|
+
elsif match
|
164
|
+
match[0..1] == '::' ? "(?<#{match[2..-1]}>.+)" : "(?<#{match[1..-1]}>[^/]+)"
|
165
|
+
else
|
166
|
+
''
|
167
|
+
end
|
168
|
+
end
|
169
|
+
}.join
|
170
|
+
pattern << '$' if match_to_end
|
171
|
+
Regexp.new(pattern)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def method_missing(method, *args, &block)
|
176
|
+
(self.class.respond_to? method) ? self.class.__send__(method, *args, &block) : super
|
177
|
+
end
|
178
|
+
|
179
|
+
def initialize(env)
|
180
|
+
define_singleton_method :env do
|
181
|
+
env
|
182
|
+
end
|
183
|
+
env['rack.request'] ||= Request.new(env)
|
184
|
+
env['rack.response'] ||= Response.new
|
185
|
+
end
|
186
|
+
|
187
|
+
def action
|
188
|
+
inner_error = nil
|
189
|
+
rescue_block = proc do |e|
|
190
|
+
raise unless filters[:error].any? do |f|
|
191
|
+
(f[:args].empty? || f[:args].any? { |type| e.is_a?(type) }) && check_conditions?(f[:conditions]) && instance_exec(e, &f[:proc])
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
match = matches(true).first
|
196
|
+
begin
|
197
|
+
catch(:halt) do
|
198
|
+
if config[:strip_trailing_slash] == :redirect && request.path =~ %r{./$}
|
199
|
+
redirect(request.path.chomp('/'))
|
200
|
+
end
|
201
|
+
|
202
|
+
run_filters(:before)
|
203
|
+
if match
|
204
|
+
request.breadcrumb << match
|
205
|
+
# Proc's are executed in the context of this controller instance.
|
206
|
+
target = match[:mapping][:target]
|
207
|
+
begin
|
208
|
+
catch(:halt) do
|
209
|
+
response.merge! (Proc === target) ? instance_exec(request.env, &target) : target.call(request.env)
|
210
|
+
end
|
211
|
+
rescue => inner_error
|
212
|
+
rescue_block.call(inner_error)
|
213
|
+
end
|
214
|
+
else
|
215
|
+
response.status = 404
|
216
|
+
end
|
217
|
+
run_filters(:after)
|
218
|
+
end
|
219
|
+
rescue => outer_error
|
220
|
+
outer_error == inner_error ? raise : rescue_block.call(outer_error)
|
221
|
+
end
|
222
|
+
response
|
223
|
+
end
|
224
|
+
|
225
|
+
def match?
|
226
|
+
!matches(true).empty?
|
227
|
+
end
|
228
|
+
|
229
|
+
# Finds mappings that match the currently unmatched portion of the request path, returning an array of all matches.
|
230
|
+
# If _short_circuit_ is set to true, it stops matching at the first positive match, returning only a single match.
|
231
|
+
def matches(short_circuit = false)
|
232
|
+
to_match = request.unmatched_path
|
233
|
+
to_match = to_match.chomp('/') if config[:strip_trailing_slash] == :ignore && to_match =~ %r{./$}
|
234
|
+
matches = []
|
235
|
+
mappings.each do |m|
|
236
|
+
m[:url].match(to_match) do |match_data|
|
237
|
+
if match_data.pre_match == ''
|
238
|
+
if check_conditions?(m[:conditions])
|
239
|
+
if match_data.names.empty?
|
240
|
+
captures = match_data.captures
|
241
|
+
else
|
242
|
+
captures = Hash[match_data.names.map{|v| v.to_sym}.zip match_data.captures]
|
243
|
+
end
|
244
|
+
matches << {mapping: m, captures: captures, url: match_data.to_s}
|
245
|
+
break if short_circuit
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
matches
|
251
|
+
end
|
252
|
+
|
253
|
+
def check_conditions?(conds)
|
254
|
+
if !conds
|
255
|
+
true
|
256
|
+
else
|
257
|
+
conds.all? { |c,v| check_condition?(c, v) }
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def check_condition?(c, v)
|
262
|
+
raise Error, "The condition `#{c}` either does not exist, or is not a Proc object" unless Proc === self.conditions[c]
|
263
|
+
instance_exec(v, &self.conditions[c])
|
264
|
+
end
|
265
|
+
|
266
|
+
def redirect(url, status = 307)
|
267
|
+
response['Location'] = url
|
268
|
+
halt(status)
|
269
|
+
end
|
270
|
+
|
271
|
+
def halt(status = 200)
|
272
|
+
response.status = status
|
273
|
+
throw :halt
|
274
|
+
end
|
275
|
+
|
276
|
+
# Convenience method for accessing Rack request.
|
277
|
+
def request
|
278
|
+
env['rack.request']
|
279
|
+
end
|
280
|
+
|
281
|
+
# Convenience method for accessing Rack response.
|
282
|
+
def response
|
283
|
+
env['rack.response']
|
284
|
+
end
|
285
|
+
|
286
|
+
# Convenience method for accessing Rack session.
|
287
|
+
def session
|
288
|
+
env['rack.session']
|
289
|
+
end
|
290
|
+
|
291
|
+
# Flash session storage helper.
|
292
|
+
# Stores session data until the next time this method is called with the same arguments, at which point it's reset.
|
293
|
+
# The typical use case is to provide feedback to the user on the previous action they performed.
|
294
|
+
def flash(key = :flash)
|
295
|
+
raise Error, "Flash session data cannot be used without a valid Rack session" unless session
|
296
|
+
flash_hash = env['scorched.flash'] ||= {}
|
297
|
+
flash_hash[key] ||= {}
|
298
|
+
session[key] ||= {}
|
299
|
+
unless session[key].methods(false).include? :[]=
|
300
|
+
session[key].define_singleton_method(:[]=) do |k, v|
|
301
|
+
flash_hash[key][k] = v
|
302
|
+
end
|
303
|
+
end
|
304
|
+
session[key]
|
305
|
+
end
|
306
|
+
|
307
|
+
after do
|
308
|
+
env['scorched.flash'].each { |k,v| session[k] = v } if session && env['scorched.flash']
|
309
|
+
end
|
310
|
+
|
311
|
+
# Serves a thin layer of convenience to Rack's built-in methods: Request#cookies, Response#set_cookie, and
|
312
|
+
# Response#delete_cookie.
|
313
|
+
# If only one argument is given, the specified cookie is retreived and returned.
|
314
|
+
# If both arguments are supplied, the cookie is either set or deleted, depending on whether the second argument is
|
315
|
+
# nil, or otherwise is a hash containing the key/value pair ``:value => nil``.
|
316
|
+
# If you wish to set a cookie to an empty value without deleting it, you pass an empty string as the value
|
317
|
+
def cookie(name, *value)
|
318
|
+
name = name.to_s
|
319
|
+
if value.empty?
|
320
|
+
request.cookies[name]
|
321
|
+
else
|
322
|
+
value = Hash === value[0] ? value[0] : {value: value}
|
323
|
+
if value[:value].nil?
|
324
|
+
response.delete_cookie(name, value)
|
325
|
+
else
|
326
|
+
response.set_cookie(name, value)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
|
333
|
+
def run_filters(type)
|
334
|
+
tracker = env['scorched.filters'] ||= {before: Set.new, after: Set.new}
|
335
|
+
filters[type].reject{ |f| tracker[type].include? f }.each do |f|
|
336
|
+
if check_conditions?(f[:conditions])
|
337
|
+
tracker[type] << f
|
338
|
+
instance_exec(&f[:proc])
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|