roda 0.9.0 → 1.0.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 +4 -4
- data/CHANGELOG +62 -0
- data/README.rdoc +362 -167
- data/Rakefile +2 -2
- data/doc/release_notes/1.0.0.txt +329 -0
- data/lib/roda.rb +553 -180
- data/lib/roda/plugins/_erubis_escaping.rb +28 -0
- data/lib/roda/plugins/all_verbs.rb +7 -9
- data/lib/roda/plugins/backtracking_array.rb +92 -0
- data/lib/roda/plugins/content_for.rb +46 -0
- data/lib/roda/plugins/csrf.rb +60 -0
- data/lib/roda/plugins/flash.rb +53 -7
- data/lib/roda/plugins/halt.rb +8 -14
- data/lib/roda/plugins/head.rb +56 -0
- data/lib/roda/plugins/header_matchers.rb +2 -2
- data/lib/roda/plugins/json.rb +84 -0
- data/lib/roda/plugins/multi_route.rb +50 -10
- data/lib/roda/plugins/not_allowed.rb +140 -0
- data/lib/roda/plugins/pass.rb +13 -6
- data/lib/roda/plugins/per_thread_caching.rb +70 -0
- data/lib/roda/plugins/render.rb +20 -33
- data/lib/roda/plugins/render_each.rb +61 -0
- data/lib/roda/plugins/symbol_matchers.rb +79 -0
- data/lib/roda/plugins/symbol_views.rb +40 -0
- data/lib/roda/plugins/view_subdirs.rb +53 -0
- data/lib/roda/version.rb +3 -0
- data/spec/matchers_spec.rb +61 -5
- data/spec/plugin/_erubis_escaping_spec.rb +29 -0
- data/spec/plugin/backtracking_array_spec.rb +38 -0
- data/spec/plugin/content_for_spec.rb +34 -0
- data/spec/plugin/csrf_spec.rb +49 -0
- data/spec/plugin/flash_spec.rb +69 -5
- data/spec/plugin/head_spec.rb +35 -0
- data/spec/plugin/json_spec.rb +50 -0
- data/spec/plugin/multi_route_spec.rb +22 -6
- data/spec/plugin/not_allowed_spec.rb +55 -0
- data/spec/plugin/pass_spec.rb +8 -2
- data/spec/plugin/per_thread_caching_spec.rb +28 -0
- data/spec/plugin/render_each_spec.rb +30 -0
- data/spec/plugin/render_spec.rb +7 -1
- data/spec/plugin/symbol_matchers_spec.rb +68 -0
- data/spec/plugin/symbol_views_spec.rb +32 -0
- data/spec/plugin/view_subdirs_spec.rb +45 -0
- data/spec/plugin_spec.rb +11 -1
- data/spec/redirect_spec.rb +21 -4
- data/spec/request_spec.rb +9 -0
- metadata +49 -5
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'erubis'
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
# The _erubis_escaping plugin is an internal plugin that provides a
|
6
|
+
# subclass of Erubis::EscapedEruby with a bugfix and an optimization.
|
7
|
+
module ErubisEscaping
|
8
|
+
# Optimized subclass that fixes escaping of postfix conditionals.
|
9
|
+
class Eruby < Erubis::EscapedEruby
|
10
|
+
# Set escaping class to a local variable, so you don't need a
|
11
|
+
# constant lookup per escape.
|
12
|
+
def convert_input(codebuf, input)
|
13
|
+
codebuf << '_erubis_xml_helper = Erubis::XmlHelper;'
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
# Fix bug in Erubis::EscapedEruby where postfix conditionals inside
|
18
|
+
# <%= %> are broken (e.g. <%= foo if bar %> ), and optimize by using
|
19
|
+
# a local variable instead of a constant lookup.
|
20
|
+
def add_expr_escaped(src, code)
|
21
|
+
src << " #{@bufvar} << _erubis_xml_helper.escape_xml((" << code << '));'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
register_plugin(:_erubis_escaping, ErubisEscaping)
|
27
|
+
end
|
28
|
+
end
|
@@ -16,7 +16,7 @@ class Roda
|
|
16
16
|
# plugin :all_verbs
|
17
17
|
#
|
18
18
|
# route do |r|
|
19
|
-
# r.delete
|
19
|
+
# r.delete do
|
20
20
|
# # Handle DELETE
|
21
21
|
# end
|
22
22
|
# r.put do
|
@@ -30,14 +30,12 @@ class Roda
|
|
30
30
|
# The verb methods are defined via metaprogramming, so there
|
31
31
|
# isn't documentation for the individual methods created.
|
32
32
|
module AllVerbs
|
33
|
-
|
34
|
-
%w'delete head options link patch put trace unlink'.each do |
|
35
|
-
if ::Rack::Request.method_defined?("#{
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
40
|
-
END
|
33
|
+
def self.configure(app)
|
34
|
+
%w'delete head options link patch put trace unlink'.each do |v|
|
35
|
+
if ::Rack::Request.method_defined?("#{v}?")
|
36
|
+
app.request_module do
|
37
|
+
app::RodaRequest.def_verb_method(self, v)
|
38
|
+
end
|
41
39
|
end
|
42
40
|
end
|
43
41
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The backtracking_array plugin changes the handling of array
|
4
|
+
# matchers such that if one of the array entries matches, but
|
5
|
+
# a later match argument fails, it will backtrack and try the
|
6
|
+
# next entry in the array. For example, the following match
|
7
|
+
# block does not match +/a/b+ by default:
|
8
|
+
#
|
9
|
+
# r.is ['a', 'a/b'] do |path|
|
10
|
+
# # ...
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# This is because the <tt>'a'</tt> entry in the array matches, which
|
14
|
+
# makes the array match. However, the next matcher is the
|
15
|
+
# terminal matcher (since +r.is+ was used), and since the
|
16
|
+
# path is not terminal as it still contains +/b+ after
|
17
|
+
# matching <tt>'a'</tt>.
|
18
|
+
#
|
19
|
+
# With the backtracking_array plugin, when the terminal matcher
|
20
|
+
# fails, matching will go on to the next entry in the array,
|
21
|
+
# <tt>'a/b'</tt>, which will also match. Since <tt>'a/b'</tt>
|
22
|
+
# matches the path fully, the terminal matcher also matches,
|
23
|
+
# and the match block yields.
|
24
|
+
module BacktrackingArray
|
25
|
+
module RequestMethods
|
26
|
+
PATH_INFO = "PATH_INFO".freeze
|
27
|
+
SCRIPT_NAME = "SCRIPT_NAME".freeze
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# When matching for a single array, after a successful
|
32
|
+
# array element match, attempt to match all remaining
|
33
|
+
# elements. If the remaining elements could not be
|
34
|
+
# matched, reset the state and continue to the next
|
35
|
+
# entry in the array.
|
36
|
+
def _match_array(arg, rest=nil)
|
37
|
+
return super unless rest
|
38
|
+
env = @env
|
39
|
+
|
40
|
+
script = env[SCRIPT_NAME]
|
41
|
+
path = env[PATH_INFO]
|
42
|
+
caps = captures.dup
|
43
|
+
arg.each do |v|
|
44
|
+
if match(v, rest)
|
45
|
+
if v.is_a?(String)
|
46
|
+
captures.push(v)
|
47
|
+
end
|
48
|
+
|
49
|
+
if match_all(rest)
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Matching all remaining elements failed, reset state
|
54
|
+
captures.replace(caps)
|
55
|
+
env[SCRIPT_NAME] = script
|
56
|
+
env[PATH_INFO] = path
|
57
|
+
end
|
58
|
+
end
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
# If any of the args are an array, handle backtracking such
|
63
|
+
# that if a later matcher fails, we roll back to the current
|
64
|
+
# matcher and proceed to the next entry in the array.
|
65
|
+
def match_all(args)
|
66
|
+
args = args.dup
|
67
|
+
until args.empty?
|
68
|
+
arg = args.shift
|
69
|
+
if match(arg, args)
|
70
|
+
return true if arg.is_a?(Array)
|
71
|
+
else
|
72
|
+
return
|
73
|
+
end
|
74
|
+
end
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
# When matching an array, include the remaining arguments,
|
79
|
+
# otherwise, just match the single argument.
|
80
|
+
def match(v, rest = nil)
|
81
|
+
if v.is_a?(Array)
|
82
|
+
_match_array(v, rest)
|
83
|
+
else
|
84
|
+
super(v)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
register_plugin(:backtracking_array, BacktrackingArray)
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The content_for plugin is designed to be used with the
|
4
|
+
# render plugin, allowing you to store content inside one
|
5
|
+
# template, and retrieve that content inside a separate
|
6
|
+
# template. Most commonly, this is so view templates
|
7
|
+
# can set content for the layout template to display outside
|
8
|
+
# of the normal content pane.
|
9
|
+
#
|
10
|
+
# The content_for template probably only works with erb
|
11
|
+
# templates, and requires that you don't override the
|
12
|
+
# +:outvar+ render option. In the template in which you
|
13
|
+
# want to store content, call content_for with a block:
|
14
|
+
#
|
15
|
+
# <% content_for :foo do %>
|
16
|
+
# Some content here.
|
17
|
+
# <% end %>
|
18
|
+
#
|
19
|
+
# In the template in which you want to retrieve content,
|
20
|
+
# call content_for without the block:
|
21
|
+
#
|
22
|
+
# <%= content_for :foo %>
|
23
|
+
module ContentFor
|
24
|
+
module InstanceMethods
|
25
|
+
# If called with a block, store content enclosed by block
|
26
|
+
# under the given key. If called without a block, retrieve
|
27
|
+
# stored content with the given key, or return nil if there
|
28
|
+
# is no content stored with that key.
|
29
|
+
def content_for(key, &block)
|
30
|
+
if block
|
31
|
+
@_content_for ||= {}
|
32
|
+
buf_was = @_out_buf
|
33
|
+
@_out_buf = ''
|
34
|
+
yield
|
35
|
+
@_content_for[key] = @_out_buf
|
36
|
+
@_out_buf = buf_was
|
37
|
+
elsif @_content_for
|
38
|
+
@_content_for[key]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
register_plugin(:content_for, ContentFor)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rack/csrf'
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
# The csrf plugin adds CSRF protection using rack_csrf, along with
|
6
|
+
# some csrf helper methods to use in your views. To use it, load
|
7
|
+
# the plugin, with the options hash passed to Rack::Csrf:
|
8
|
+
#
|
9
|
+
# plugin :csrf, :raise=>true
|
10
|
+
#
|
11
|
+
# This adds the following instance methods:
|
12
|
+
#
|
13
|
+
# csrf_field :: The field name to use for the hidden/meta csrf tag.
|
14
|
+
# csrf_header :: The http header name to use for submitting csrf token via
|
15
|
+
# headers (useful for javascript).
|
16
|
+
# csrf_metatag :: An html meta tag string containing the token, suitable
|
17
|
+
# for placing in the page header
|
18
|
+
# csrf_tag :: An html hidden input tag string containing the token, suitable
|
19
|
+
# for placing in an html form.
|
20
|
+
# csrf_token :: The value of the csrf token, in case it needs to be accessed
|
21
|
+
# directly.
|
22
|
+
module Csrf
|
23
|
+
CSRF = ::Rack::Csrf
|
24
|
+
|
25
|
+
# Load the Rack::Csrf middleware into the app with the given options.
|
26
|
+
def self.configure(app, opts={})
|
27
|
+
app.use CSRF, opts
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
# The name of the hidden/meta csrf tag.
|
32
|
+
def csrf_field
|
33
|
+
CSRF.field
|
34
|
+
end
|
35
|
+
|
36
|
+
# The http header name to use for submitting csrf token via headers.
|
37
|
+
def csrf_header
|
38
|
+
CSRF.header
|
39
|
+
end
|
40
|
+
|
41
|
+
# An html meta tag string containing the token.
|
42
|
+
def csrf_metatag(opts={})
|
43
|
+
CSRF.metatag(env, opts)
|
44
|
+
end
|
45
|
+
|
46
|
+
# An html hidden input tag string containing the token.
|
47
|
+
def csrf_tag
|
48
|
+
CSRF.tag(env)
|
49
|
+
end
|
50
|
+
|
51
|
+
# The value of the csrf token.
|
52
|
+
def csrf_token
|
53
|
+
CSRF.token(env)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
register_plugin(:csrf, Csrf)
|
59
|
+
end
|
60
|
+
end
|
data/lib/roda/plugins/flash.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'delegate'
|
2
2
|
|
3
3
|
class Roda
|
4
4
|
module RodaPlugins
|
@@ -25,20 +25,66 @@ class Roda
|
|
25
25
|
# end
|
26
26
|
# end
|
27
27
|
# end
|
28
|
-
#
|
29
|
-
# The flash plugin uses sinatra-flash internally, so you
|
30
|
-
# must install sinatra-flash in order to use it.
|
31
28
|
module Flash
|
32
|
-
|
29
|
+
# Simple flash hash, where assiging to the hash updates the flash
|
30
|
+
# used in the following request.
|
31
|
+
class FlashHash < DelegateClass(Hash)
|
32
|
+
# The flash hash for the next request. This
|
33
|
+
# is what gets written to by #[]=.
|
34
|
+
attr_reader :next
|
35
|
+
|
36
|
+
# The flash hash for the current request
|
37
|
+
alias now __getobj__
|
38
|
+
|
39
|
+
# Setup the next hash when initializing, and handle treat nil
|
40
|
+
# as a new empty hash.
|
41
|
+
def initialize(hash={})
|
42
|
+
super(hash||{})
|
43
|
+
@next = {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Update the next hash with the given key and value.
|
47
|
+
def []=(k, v)
|
48
|
+
@next[k] = v
|
49
|
+
end
|
50
|
+
|
51
|
+
# Remove given key from the next hash, or clear the next hash if
|
52
|
+
# no argument is given.
|
53
|
+
def discard(key=(no_arg=true))
|
54
|
+
if no_arg
|
55
|
+
@next.clear
|
56
|
+
else
|
57
|
+
@next.delete(key)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Copy the entry with the given key from the current hash to the
|
62
|
+
# next hash, or copy all entries from the current hash to the
|
63
|
+
# next hash if no argument is given.
|
64
|
+
def keep(key=(no_arg=true))
|
65
|
+
if no_arg
|
66
|
+
@next.merge!(self)
|
67
|
+
else
|
68
|
+
self[key] = self[key]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Replace the current hash with the next hash and clear the next hash.
|
73
|
+
def sweep
|
74
|
+
replace(@next)
|
75
|
+
@next.clear
|
76
|
+
self
|
77
|
+
end
|
78
|
+
end
|
33
79
|
|
34
80
|
module InstanceMethods
|
35
81
|
# The internal session key used to store the flash.
|
36
|
-
KEY = :
|
82
|
+
KEY = :_flash
|
37
83
|
|
38
84
|
# Access the flash hash for the current request, loading
|
39
85
|
# it from the session if it is not already loaded.
|
40
86
|
def flash
|
41
|
-
@_flash ||= FlashHash.new(
|
87
|
+
@_flash ||= FlashHash.new(session[KEY])
|
42
88
|
end
|
43
89
|
|
44
90
|
private
|
data/lib/roda/plugins/halt.rb
CHANGED
@@ -1,18 +1,12 @@
|
|
1
1
|
class Roda
|
2
2
|
module RodaPlugins
|
3
|
-
# The halt plugin augments the standard request +halt+ method to
|
4
|
-
#
|
3
|
+
# The halt plugin augments the standard request +halt+ method to allow the response
|
4
|
+
# status, body, or headers to be changed when halting.
|
5
5
|
#
|
6
6
|
# After loading the halt plugin:
|
7
7
|
#
|
8
8
|
# plugin :halt
|
9
9
|
#
|
10
|
-
# You can call halt with no arguments to immediately stop processing:
|
11
|
-
#
|
12
|
-
# route do |r|
|
13
|
-
# r.halt
|
14
|
-
# end
|
15
|
-
#
|
16
10
|
# You can call the halt method with an integer to set the response status and return:
|
17
11
|
#
|
18
12
|
# route do |r|
|
@@ -22,23 +16,23 @@ class Roda
|
|
22
16
|
# Or set the response body and return:
|
23
17
|
#
|
24
18
|
# route do |r|
|
25
|
-
# r.halt(
|
19
|
+
# r.halt('body')
|
26
20
|
# end
|
27
21
|
#
|
28
22
|
# Or set both:
|
29
23
|
#
|
30
24
|
# route do |r|
|
31
|
-
# r.halt(403,
|
25
|
+
# r.halt(403, 'body')
|
32
26
|
# end
|
33
27
|
#
|
34
28
|
# Or set response status, headers, and body:
|
35
29
|
#
|
36
30
|
# route do |r|
|
37
|
-
# r.halt(403, {'Content-Type'=>'text/csv'},
|
31
|
+
# r.halt(403, {'Content-Type'=>'text/csv'}, 'body')
|
38
32
|
# end
|
39
33
|
#
|
40
34
|
# Note that there is a difference between provide status, headers, and body as separate
|
41
|
-
# arguments and providing them as a rack response array. With a rack response array,
|
35
|
+
# arguments and providing them as a single rack response array. With a rack response array,
|
42
36
|
# the values are used directly, while with 3 arguments, the headers given are merged into
|
43
37
|
# the existing headers and the given body is written to the existing response body.
|
44
38
|
module Halt
|
@@ -54,7 +48,7 @@ class Roda
|
|
54
48
|
when String
|
55
49
|
response.write v
|
56
50
|
when Array
|
57
|
-
|
51
|
+
throw :halt, v
|
58
52
|
else
|
59
53
|
raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
|
60
54
|
end
|
@@ -69,7 +63,7 @@ class Roda
|
|
69
63
|
raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
|
70
64
|
end
|
71
65
|
|
72
|
-
|
66
|
+
super()
|
73
67
|
end
|
74
68
|
end
|
75
69
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The head plugin attempts to automatically handle HEAD requests,
|
4
|
+
# by treating them as GET requests and returning an empty body
|
5
|
+
# without modifying the response status or response headers.
|
6
|
+
#
|
7
|
+
# So for the following routes,
|
8
|
+
#
|
9
|
+
# route do |r|
|
10
|
+
# r.root do
|
11
|
+
# 'root'
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# r.get 'a' do
|
15
|
+
# 'a'
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# r.is 'b', :method=>[:get, :post] do
|
19
|
+
# 'b'
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# HEAD requests for +/+, +/a+, and +/b+ will all return 200 status
|
24
|
+
# with an empty body.
|
25
|
+
module Head
|
26
|
+
module InstanceMethods
|
27
|
+
# Always use an empty response body for head requests, with a
|
28
|
+
# content length of 0.
|
29
|
+
def call(*)
|
30
|
+
res = super
|
31
|
+
if request.head?
|
32
|
+
res[2] = []
|
33
|
+
end
|
34
|
+
res
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module RequestMethods
|
39
|
+
# Consider HEAD requests as GET requests.
|
40
|
+
def is_get?
|
41
|
+
super || head?
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# If the current request is a HEAD request, match if one of
|
47
|
+
# the given methods is a GET request.
|
48
|
+
def match_method(method)
|
49
|
+
super || (!method.is_a?(Array) && head? && method.to_s.upcase == 'GET')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
register_plugin(:head, Head)
|
55
|
+
end
|
56
|
+
end
|