roda 0.9.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 +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +709 -0
- data/Rakefile +124 -0
- data/lib/roda.rb +608 -0
- data/lib/roda/plugins/all_verbs.rb +48 -0
- data/lib/roda/plugins/default_headers.rb +50 -0
- data/lib/roda/plugins/error_handler.rb +69 -0
- data/lib/roda/plugins/flash.rb +62 -0
- data/lib/roda/plugins/h.rb +24 -0
- data/lib/roda/plugins/halt.rb +79 -0
- data/lib/roda/plugins/header_matchers.rb +57 -0
- data/lib/roda/plugins/hooks.rb +106 -0
- data/lib/roda/plugins/indifferent_params.rb +47 -0
- data/lib/roda/plugins/middleware.rb +88 -0
- data/lib/roda/plugins/multi_route.rb +77 -0
- data/lib/roda/plugins/not_found.rb +62 -0
- data/lib/roda/plugins/pass.rb +34 -0
- data/lib/roda/plugins/render.rb +217 -0
- data/lib/roda/plugins/streaming.rb +165 -0
- data/spec/composition_spec.rb +19 -0
- data/spec/env_spec.rb +11 -0
- data/spec/integration_spec.rb +63 -0
- data/spec/matchers_spec.rb +658 -0
- data/spec/module_spec.rb +29 -0
- data/spec/opts_spec.rb +42 -0
- data/spec/plugin/all_verbs_spec.rb +29 -0
- data/spec/plugin/default_headers_spec.rb +63 -0
- data/spec/plugin/error_handler_spec.rb +67 -0
- data/spec/plugin/flash_spec.rb +59 -0
- data/spec/plugin/h_spec.rb +13 -0
- data/spec/plugin/halt_spec.rb +62 -0
- data/spec/plugin/header_matchers_spec.rb +61 -0
- data/spec/plugin/hooks_spec.rb +97 -0
- data/spec/plugin/indifferent_params_spec.rb +13 -0
- data/spec/plugin/middleware_spec.rb +52 -0
- data/spec/plugin/multi_route_spec.rb +98 -0
- data/spec/plugin/not_found_spec.rb +99 -0
- data/spec/plugin/pass_spec.rb +23 -0
- data/spec/plugin/render_spec.rb +148 -0
- data/spec/plugin/streaming_spec.rb +52 -0
- data/spec/plugin_spec.rb +61 -0
- data/spec/redirect_spec.rb +24 -0
- data/spec/request_spec.rb +55 -0
- data/spec/response_spec.rb +131 -0
- data/spec/session_spec.rb +35 -0
- data/spec/spec_helper.rb +89 -0
- data/spec/version_spec.rb +8 -0
- metadata +148 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The all_verbs plugin adds methods for http verbs other than
|
4
|
+
# get and post. The following verbs are added, assuming
|
5
|
+
# rack handles them: delete, head, options, link, patch, put,
|
6
|
+
# trace, unlink.
|
7
|
+
#
|
8
|
+
# These methods operate just like Roda's default get and post
|
9
|
+
# methods other that the http verb used, so using them without
|
10
|
+
# any arguments just checks for the request method, while
|
11
|
+
# using them with any arguments also checks that the arguments
|
12
|
+
# match the full path.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# plugin :all_verbs
|
17
|
+
#
|
18
|
+
# route do |r|
|
19
|
+
# r.delete
|
20
|
+
# # Handle DELETE
|
21
|
+
# end
|
22
|
+
# r.put do
|
23
|
+
# # Handle PUT
|
24
|
+
# end
|
25
|
+
# r.patch do
|
26
|
+
# # Handle PATCH
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# The verb methods are defined via metaprogramming, so there
|
31
|
+
# isn't documentation for the individual methods created.
|
32
|
+
module AllVerbs
|
33
|
+
module RequestMethods
|
34
|
+
%w'delete head options link patch put trace unlink'.each do |t|
|
35
|
+
if ::Rack::Request.method_defined?("#{t}?")
|
36
|
+
class_eval(<<-END, __FILE__, __LINE__+1)
|
37
|
+
def #{t}(*args, &block)
|
38
|
+
is_or_on(*args, &block) if #{t}?
|
39
|
+
end
|
40
|
+
END
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
register_plugin(:all_verbs, AllVerbs)
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The default_headers plugin accepts a hash of headers,
|
4
|
+
# and overrides the default_headers method in the
|
5
|
+
# response class to be a copy of the headers.
|
6
|
+
#
|
7
|
+
# Note that when using this module, you should not
|
8
|
+
# attempt to mutate of the values set in the default
|
9
|
+
# headers hash.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# plugin :default_headers, 'Content-Type'=>'text/csv'
|
14
|
+
#
|
15
|
+
# You can also modify the default headers later:
|
16
|
+
#
|
17
|
+
# plugin :default_headers
|
18
|
+
# default_headers['Foo'] = 'bar'
|
19
|
+
# default_headers.merge!('Bar'=>'baz')
|
20
|
+
module DefaultHeaders
|
21
|
+
# Merge the given headers into the existing default headers, if any.
|
22
|
+
def self.configure(app, headers={})
|
23
|
+
app.instance_eval do
|
24
|
+
@default_headers ||= {}
|
25
|
+
@default_headers.merge!(headers)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
# The default response headers to use for the current class.
|
31
|
+
attr_reader :default_headers
|
32
|
+
|
33
|
+
# Copy the default headers into the subclass when inheriting.
|
34
|
+
def inherited(subclass)
|
35
|
+
super
|
36
|
+
subclass.instance_variable_set(:@default_headers, default_headers.dup)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module ResponseMethods
|
41
|
+
# Get the default headers from the related roda class.
|
42
|
+
def default_headers
|
43
|
+
self.class.roda_class.default_headers.dup
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
register_plugin(:default_headers, DefaultHeaders)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The error_handler plugin adds an error handler to the routing,
|
4
|
+
# so that if routing the request raises an error, a nice error
|
5
|
+
# message page can be returned to the user.
|
6
|
+
#
|
7
|
+
# You can provide the error handler as a block to the plugin:
|
8
|
+
#
|
9
|
+
# plugin :error_handler do |e|
|
10
|
+
# "Oh No!"
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Or later via the +error+ class method:
|
14
|
+
#
|
15
|
+
# plugin :error_handler
|
16
|
+
#
|
17
|
+
# error do |e|
|
18
|
+
# "Oh No!"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# In both cases, the exception instance is passed into the block,
|
22
|
+
# and the block can return the request body via a string.
|
23
|
+
#
|
24
|
+
# If an exception is raised, the response status will be set to 500
|
25
|
+
# before executing the error handler. The error handler can change
|
26
|
+
# the response status if necessary.
|
27
|
+
module ErrorHandler
|
28
|
+
# If a block is given, automatically call the +error+ method on
|
29
|
+
# the Roda class with it.
|
30
|
+
def self.configure(app, &block)
|
31
|
+
if block
|
32
|
+
app.error(&block)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
# Install the given block as the error handler, so that if routing
|
38
|
+
# the request raises an exception, the block will be called with
|
39
|
+
# the exception in the scope of the Roda instance.
|
40
|
+
def error(&block)
|
41
|
+
define_method(:handle_error, &block)
|
42
|
+
private :handle_error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module InstanceMethods
|
47
|
+
private
|
48
|
+
|
49
|
+
# If an error occurs, set the response status to 500 and call
|
50
|
+
# the error handler.
|
51
|
+
def _route
|
52
|
+
super
|
53
|
+
rescue => e
|
54
|
+
response.status = 500
|
55
|
+
super{handle_error(e)}
|
56
|
+
end
|
57
|
+
|
58
|
+
# By default, have the error handler reraise the error, so using
|
59
|
+
# the plugin without installing an error handler doesn't change
|
60
|
+
# behavior.
|
61
|
+
def handle_error(e)
|
62
|
+
raise e
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
register_plugin(:error_handler, ErrorHandler)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'sinatra/flash/hash'
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
# The flash plugin adds a +flash+ instance method to Roda,
|
6
|
+
# for typical web application flash handling, where values
|
7
|
+
# set in the current flash hash are available in the next
|
8
|
+
# request.
|
9
|
+
#
|
10
|
+
# With the example below, if a POST request is submitted,
|
11
|
+
# it will redirect and the resulting GET request will
|
12
|
+
# return 'b'.
|
13
|
+
#
|
14
|
+
# plugin :flash
|
15
|
+
#
|
16
|
+
# route do |r|
|
17
|
+
# r.is '' do
|
18
|
+
# r.get do
|
19
|
+
# flash['a']
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# r.post do
|
23
|
+
# flash['a'] = 'b'
|
24
|
+
# r.redirect('')
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# The flash plugin uses sinatra-flash internally, so you
|
30
|
+
# must install sinatra-flash in order to use it.
|
31
|
+
module Flash
|
32
|
+
FlashHash = ::Sinatra::Flash::FlashHash
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
# The internal session key used to store the flash.
|
36
|
+
KEY = :flash
|
37
|
+
|
38
|
+
# Access the flash hash for the current request, loading
|
39
|
+
# it from the session if it is not already loaded.
|
40
|
+
def flash
|
41
|
+
@_flash ||= FlashHash.new((session ? session[KEY] : {}))
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# If the routing doesn't raise an error, rotate the flash
|
47
|
+
# hash in the session so the next request has access to it.
|
48
|
+
def _route
|
49
|
+
res = super
|
50
|
+
|
51
|
+
if f = @_flash
|
52
|
+
session[KEY] = f.next
|
53
|
+
end
|
54
|
+
|
55
|
+
res
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
register_plugin(:flash, Flash)
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The h plugin adds an +h+ instance method that will HTML
|
4
|
+
# escape the input and return it.
|
5
|
+
#
|
6
|
+
# The following example will return "<foo>" as the body.
|
7
|
+
#
|
8
|
+
# plugin :h
|
9
|
+
#
|
10
|
+
# route do |r|
|
11
|
+
# h('<foo>')
|
12
|
+
# end
|
13
|
+
module H
|
14
|
+
module InstanceMethods
|
15
|
+
# HTML escape the input and return the escaped version.
|
16
|
+
def h(s)
|
17
|
+
::Rack::Utils.escape_html(s.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
register_plugin(:h, H)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The halt plugin augments the standard request +halt+ method to handle more than
|
4
|
+
# just rack response arrays.
|
5
|
+
#
|
6
|
+
# After loading the halt plugin:
|
7
|
+
#
|
8
|
+
# plugin :halt
|
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
|
+
# You can call the halt method with an integer to set the response status and return:
|
17
|
+
#
|
18
|
+
# route do |r|
|
19
|
+
# r.halt(403)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Or set the response body and return:
|
23
|
+
#
|
24
|
+
# route do |r|
|
25
|
+
# r.halt("body')
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Or set both:
|
29
|
+
#
|
30
|
+
# route do |r|
|
31
|
+
# r.halt(403, "body')
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Or set response status, headers, and body:
|
35
|
+
#
|
36
|
+
# route do |r|
|
37
|
+
# r.halt(403, {'Content-Type'=>'text/csv'}, "body')
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# 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,
|
42
|
+
# the values are used directly, while with 3 arguments, the headers given are merged into
|
43
|
+
# the existing headers and the given body is written to the existing response body.
|
44
|
+
module Halt
|
45
|
+
module RequestMethods
|
46
|
+
# Expand default halt method to handle status codes, headers, and bodies. See Halt.
|
47
|
+
def halt(*res)
|
48
|
+
case res.length
|
49
|
+
when 0 # do nothing
|
50
|
+
when 1
|
51
|
+
case v = res[0]
|
52
|
+
when Integer
|
53
|
+
response.status = v
|
54
|
+
when String
|
55
|
+
response.write v
|
56
|
+
when Array
|
57
|
+
super
|
58
|
+
else
|
59
|
+
raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
|
60
|
+
end
|
61
|
+
when 2
|
62
|
+
response.status = res[0]
|
63
|
+
response.write res[1]
|
64
|
+
when 3
|
65
|
+
response.status = res[0]
|
66
|
+
response.headers.merge!(res[1])
|
67
|
+
response.write res[2]
|
68
|
+
else
|
69
|
+
raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
|
70
|
+
end
|
71
|
+
|
72
|
+
_halt response.finish
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
register_plugin(:halt, Halt)
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The header_matchers plugin adds hash matchers for matching on less-common
|
4
|
+
# HTTP headers.
|
5
|
+
#
|
6
|
+
# plugin :header_matchers
|
7
|
+
#
|
8
|
+
# It adds a +:header+ matcher for matching on arbitrary headers, which matches
|
9
|
+
# if the header is present:
|
10
|
+
#
|
11
|
+
# route do |r|
|
12
|
+
# r.on :header=>'X-App-Token' do
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# It adds a +:host+ matcher for matching by the host of the request:
|
17
|
+
#
|
18
|
+
# route do |r|
|
19
|
+
# r.on :host=>'foo.example.com' do
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# It adds an +:accept+ matcher for matching based on the Accept header:
|
24
|
+
#
|
25
|
+
# route do |r|
|
26
|
+
# r.on :accept=>'text/csv' do
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Note that the accept matcher is very simple and cannot handle wildcards,
|
31
|
+
# priorities, or anything but a simple comma separated list of mime types.
|
32
|
+
module HeaderMatchers
|
33
|
+
module RequestMethods
|
34
|
+
private
|
35
|
+
|
36
|
+
# Match if the given mimetype is one of the accepted mimetypes.
|
37
|
+
def match_accept(mimetype)
|
38
|
+
if env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
|
39
|
+
response["Content-Type"] = mimetype
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Match if the given uppercase key is present inside the environment.
|
44
|
+
def match_header(key)
|
45
|
+
env[key.upcase.tr("-","_")]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Match if the host of the request is the same as the hostname.
|
49
|
+
def match_host(hostname)
|
50
|
+
hostname === host
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
register_plugin(:header_matchers, HeaderMatchers)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The hooks plugin adds before and after hooks to the request cycle.
|
4
|
+
#
|
5
|
+
# plugin :hooks
|
6
|
+
#
|
7
|
+
# before do
|
8
|
+
# request.redirect('/login') unless logged_in?
|
9
|
+
# @time = Time.now
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# after do |res|
|
13
|
+
# logger.notice("Took #{Time.now - @time} seconds")
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Note that in general, before hooks are not needed, since you can just
|
17
|
+
# run code at the top of the route block:
|
18
|
+
#
|
19
|
+
# route do |r|
|
20
|
+
# r.redirect('/login') unless logged_in?
|
21
|
+
# # ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Note that the after hook is called with the rack response array
|
25
|
+
# of status, headers, and body. If it wants to change the response,
|
26
|
+
# it must mutate this argument, calling <tt>response.status=</tt> inside
|
27
|
+
# an after block will not affect the returned status.
|
28
|
+
#
|
29
|
+
# However, this code makes it easier to write after hooks, as well as
|
30
|
+
# handle cases where before hooks are added after the route block.
|
31
|
+
module Hooks
|
32
|
+
def self.configure(app)
|
33
|
+
@app.instance_exec do
|
34
|
+
@after ||= nil
|
35
|
+
@before ||= nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
# Add an after hook. If there is already an after hook defined,
|
41
|
+
# use a proc that instance_execs the existing after proc and
|
42
|
+
# then instance_execs the given after proc, so that the given
|
43
|
+
# after proc always executes after the previous one.
|
44
|
+
def after(&block)
|
45
|
+
if block
|
46
|
+
@after = if b = @after
|
47
|
+
@after = proc do |res|
|
48
|
+
instance_exec(res, &b)
|
49
|
+
instance_exec(res, &block)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
block
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@after
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add a before hook. If there is already a before hook defined,
|
59
|
+
# use a proc that instance_execs the give before proc and
|
60
|
+
# then instance_execs the existing before proc, so that the given
|
61
|
+
# before proc always executes before the previous one.
|
62
|
+
def before(&block)
|
63
|
+
if block
|
64
|
+
@before = if b = @before
|
65
|
+
@before = proc do
|
66
|
+
instance_exec(&block)
|
67
|
+
instance_exec(&b)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
block
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@before
|
74
|
+
end
|
75
|
+
|
76
|
+
# Copy the before and after hooks into the subclasses
|
77
|
+
# when inheriting
|
78
|
+
def inherited(subclass)
|
79
|
+
super
|
80
|
+
subclass.instance_variable_set(:@before, @before)
|
81
|
+
subclass.instance_variable_set(:@after, @after)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
module InstanceMethods
|
86
|
+
private
|
87
|
+
|
88
|
+
# Before routing, execute the before hooks, and
|
89
|
+
# execute the after hooks before returning.
|
90
|
+
def _route(*, &block)
|
91
|
+
if b = self.class.before
|
92
|
+
instance_exec(&b)
|
93
|
+
end
|
94
|
+
|
95
|
+
res = super
|
96
|
+
ensure
|
97
|
+
if b = self.class.after
|
98
|
+
instance_exec(res, &b)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
register_plugin(:hooks, Hooks)
|
105
|
+
end
|
106
|
+
end
|