roda 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|