roda-cj 0.9.1
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 +13 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +715 -0
- data/Rakefile +124 -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 +108 -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/lib/roda/version.rb +3 -0
- data/lib/roda.rb +610 -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 +683 -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 +123 -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 +136 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The indifferent_params plugin adds a +params+ instance
|
4
|
+
# method which returns a copy of the request params hash
|
5
|
+
# that will automatically convert symbols to strings.
|
6
|
+
# Example:
|
7
|
+
#
|
8
|
+
# plugin :indifferent_params
|
9
|
+
#
|
10
|
+
# route do |r|
|
11
|
+
# params[:foo]
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# The params hash is initialized lazily, so you only pay
|
15
|
+
# the penalty of copying the request params if you call
|
16
|
+
# the +params+ method.
|
17
|
+
module IndifferentParams
|
18
|
+
module InstanceMethods
|
19
|
+
# A copy of the request params that will automatically
|
20
|
+
# convert symbols to strings.
|
21
|
+
def params
|
22
|
+
@_params ||= indifferent_params(request.params)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Recursively process the request params and convert
|
28
|
+
# hashes to support indifferent access, leaving
|
29
|
+
# other values alone.
|
30
|
+
def indifferent_params(params)
|
31
|
+
case params
|
32
|
+
when Hash
|
33
|
+
h = Hash.new{|h, k| h[k.to_s] if Symbol === k}
|
34
|
+
params.each{|k, v| h[k] = indifferent_params(v)}
|
35
|
+
h
|
36
|
+
when Array
|
37
|
+
params.map{|x| indifferent_params(x)}
|
38
|
+
else
|
39
|
+
params
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
register_plugin(:indifferent_params, IndifferentParams)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The middleware plugin allows the Roda app to be used as
|
4
|
+
# rack middleware.
|
5
|
+
#
|
6
|
+
# In the example below, requests to /mid will return Mid
|
7
|
+
# by the Mid middleware, and requests to /app will not be
|
8
|
+
# matched by the Mid middleware, so they will be forwarded
|
9
|
+
# to App.
|
10
|
+
#
|
11
|
+
# class Mid < Roda
|
12
|
+
# plugin :middleware
|
13
|
+
#
|
14
|
+
# route do |r|
|
15
|
+
# r.is "mid" do
|
16
|
+
# "Mid"
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# class App < Roda
|
22
|
+
# use Mid
|
23
|
+
#
|
24
|
+
# route do |r|
|
25
|
+
# r.is "app" do
|
26
|
+
# "App"
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# run App
|
32
|
+
#
|
33
|
+
# Note that once you use the middleware plugin, you can only use the
|
34
|
+
# Roda app as middleware, and you will get errors if you attempt to
|
35
|
+
# use it as a regular app.
|
36
|
+
module Middleware
|
37
|
+
# Forward instances are what is actually used as middleware.
|
38
|
+
class Forwarder
|
39
|
+
# Store the current middleware and the next middleware to call.
|
40
|
+
def initialize(mid, app)
|
41
|
+
@mid = mid.app
|
42
|
+
@app = app
|
43
|
+
end
|
44
|
+
|
45
|
+
# When calling the middleware, first call the current middleware.
|
46
|
+
# If this returns a result, return that result directly. Otherwise,
|
47
|
+
# pass handling of the request to the next middleware.
|
48
|
+
def call(env)
|
49
|
+
res = nil
|
50
|
+
|
51
|
+
call_next = catch(:next) do
|
52
|
+
res = @mid.call(env)
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
if call_next
|
57
|
+
@app.call(env)
|
58
|
+
else
|
59
|
+
res
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module ClassMethods
|
65
|
+
# If an argument is given, this is a middleware app, so create a Forwarder.
|
66
|
+
# Otherwise, this is a usual instance creation, so call super.
|
67
|
+
def new(app=nil)
|
68
|
+
if app
|
69
|
+
Forwarder.new(self, app)
|
70
|
+
else
|
71
|
+
super()
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Override the route block so that if no route matches, we throw so
|
76
|
+
# that the next middleware is called.
|
77
|
+
def route(&block)
|
78
|
+
super do |r|
|
79
|
+
instance_exec(r, &block)
|
80
|
+
throw :next, true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
register_plugin(:middleware, Middleware)
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The multi_route plugin allows for multiple named routes, which the
|
4
|
+
# main route block can dispatch to by name at any point. If the named
|
5
|
+
# route doesn't handle the request, execution will continue, and if the
|
6
|
+
# named route does handle the request, the response by the named route
|
7
|
+
# will be returned.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# plugin :multi_route
|
12
|
+
#
|
13
|
+
# route(:foo) do |r|
|
14
|
+
# r.is 'bar' do
|
15
|
+
# '/foo/bar'
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# route(:bar) do |r|
|
20
|
+
# r.is 'foo' do
|
21
|
+
# '/bar/foo'
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# route do |r|
|
26
|
+
# r.on "foo" do
|
27
|
+
# route :foo
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# r.on "bar" do
|
31
|
+
# route :bar
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# Note that in multi-threaded code, you should not attempt to add a
|
36
|
+
# named route after accepting requests.
|
37
|
+
module MultiRoute
|
38
|
+
# Initialize storage for the named routes.
|
39
|
+
def self.configure(app)
|
40
|
+
app.instance_exec{@named_routes ||= {}}
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
# Copy the named routes into the subclass when inheriting.
|
45
|
+
def inherited(subclass)
|
46
|
+
super
|
47
|
+
subclass.instance_variable_set(:@named_routes, @named_routes.dup)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the named route with the given name.
|
51
|
+
def named_route(name)
|
52
|
+
@named_routes[name]
|
53
|
+
end
|
54
|
+
|
55
|
+
# If the given route has a named, treat it as a named route and
|
56
|
+
# store the route block. Otherwise, this is the main route, so
|
57
|
+
# call super.
|
58
|
+
def route(name=nil, &block)
|
59
|
+
if name
|
60
|
+
@named_routes[name] = block
|
61
|
+
else
|
62
|
+
super(&block)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module InstanceMethods
|
68
|
+
# Dispatch to the named route with the given name.
|
69
|
+
def route(name)
|
70
|
+
instance_exec(request, &self.class.named_route(name))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
register_plugin(:multi_route, MultiRoute)
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The not_found plugin adds a +not_found+ class method which sets
|
4
|
+
# a block that is called whenever a 404 response with an empty body
|
5
|
+
# would be returned. The usual use case for this is the desire for
|
6
|
+
# nice error pages if the page is not found.
|
7
|
+
#
|
8
|
+
# You can provide the block with the plugin call:
|
9
|
+
#
|
10
|
+
# plugin :not_found do
|
11
|
+
# "Where did it go?"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# Or later via a separate call to +not_found+:
|
15
|
+
#
|
16
|
+
# plugin :not_found
|
17
|
+
#
|
18
|
+
# not_found do
|
19
|
+
# "Where did it go?"
|
20
|
+
# end
|
21
|
+
module NotFound
|
22
|
+
# If a block is given, install the block as the not_found handler.
|
23
|
+
def self.configure(app, &block)
|
24
|
+
if block
|
25
|
+
app.not_found(&block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module ClassMethods
|
30
|
+
# Install the given block as the not_found handler.
|
31
|
+
def not_found(&block)
|
32
|
+
define_method(:not_found, &block)
|
33
|
+
private :not_found
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module InstanceMethods
|
38
|
+
private
|
39
|
+
|
40
|
+
# If routing returns a 404 response with an empty body, call
|
41
|
+
# the not_found handler.
|
42
|
+
def _route
|
43
|
+
result = super
|
44
|
+
|
45
|
+
if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
|
46
|
+
super{not_found}
|
47
|
+
else
|
48
|
+
result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Use an empty not_found_handler by default, so that loading
|
53
|
+
# the plugin without defining a not_found handler doesn't
|
54
|
+
# break things.
|
55
|
+
def not_found
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
register_plugin(:not_found, NotFound)
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The pass plugin adds a request +pass+ method to skip the current +on+
|
4
|
+
# block as if it did not match.
|
5
|
+
#
|
6
|
+
# plugin :pass
|
7
|
+
#
|
8
|
+
# route do |r|
|
9
|
+
# r.on "foo/:bar" do |bar|
|
10
|
+
# pass if bar == 'baz'
|
11
|
+
# "/foo/#{bar} (not baz)"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# r.on "foo/baz" do
|
15
|
+
# "/foo/baz"
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
module Pass
|
19
|
+
module RequestMethods
|
20
|
+
# Handle passing inside the current block.
|
21
|
+
def on(*)
|
22
|
+
catch(:pass){super}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Skip the current #on block as if it did not match.
|
26
|
+
def pass
|
27
|
+
throw :pass
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
register_plugin(:pass, Pass)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require "tilt"
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
# The render plugin adds support for template rendering using the tilt
|
6
|
+
# library. Two methods are provided for template rendering, +view+
|
7
|
+
# (which uses the layout) and +render+ (which does not).
|
8
|
+
#
|
9
|
+
# plugin :render
|
10
|
+
#
|
11
|
+
# route do |r|
|
12
|
+
# r.is 'foo' do
|
13
|
+
# view('foo') # renders views/foo.erb inside views/layout.erb
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# r.is 'bar' do
|
17
|
+
# render('bar') # renders views/bar.erb
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# You can provide options to the plugin method, or later by modifying
|
22
|
+
# +render_opts+.
|
23
|
+
#
|
24
|
+
# plugin :render, :engine=>'haml'
|
25
|
+
#
|
26
|
+
# render_opts[:views] = 'admin_views'
|
27
|
+
#
|
28
|
+
# The following options are supported:
|
29
|
+
#
|
30
|
+
# :cache :: A specific cache to store templates in, or nil/false to not
|
31
|
+
# cache templates (useful for development), defaults to true to
|
32
|
+
# automatically use the default template cache.
|
33
|
+
# :engine :: The tilt engine to use for rendering, defaults to 'erb'.
|
34
|
+
# :ext :: The file extension to assume for view files, defaults to the :engine
|
35
|
+
# option.
|
36
|
+
# :layout :: The base name of the layout file, defaults to 'layout'.
|
37
|
+
# :layout_opts :: The options to use when rendering the layout, if different
|
38
|
+
# from the default options.
|
39
|
+
# :opts :: The tilt options used when rendering templates, defaults to
|
40
|
+
# {:outvar=>'@_out_buf'}.
|
41
|
+
# :views :: The directory holding the view files, defaults to 'views' in the
|
42
|
+
# current directory.
|
43
|
+
#
|
44
|
+
# Most of these options can be overridden at runtime by passing options
|
45
|
+
# to the +view+ or +render+ methods:
|
46
|
+
#
|
47
|
+
# view('foo', :ext=>'html.erb')
|
48
|
+
# render('foo', :views=>'admin_views')
|
49
|
+
#
|
50
|
+
# There are a couple of additional options to +view+ and +render+ that are
|
51
|
+
# available at runtime:
|
52
|
+
#
|
53
|
+
# :inline :: Use the value given as the template code, instead of looking
|
54
|
+
# for template code in a file.
|
55
|
+
# :locals :: Hash of local variables to make available inside the template.
|
56
|
+
# :path :: Use the value given as the full pathname for the file, instead
|
57
|
+
# of using the :views and :ext option in combination with the
|
58
|
+
# template name.
|
59
|
+
#
|
60
|
+
# Here's how those options are used:
|
61
|
+
#
|
62
|
+
# view(:inline=>'<%= @foo %>')
|
63
|
+
# render(:path=>'/path/to/template.erb')
|
64
|
+
#
|
65
|
+
# If you pass a hash as the first argument to +view+ or +render+, it should
|
66
|
+
# have either +:inline+ or +:path+ as one of the keys.
|
67
|
+
module Render
|
68
|
+
# Default template cache. Thread-safe so that multiple threads can
|
69
|
+
# simultaneously use the cache.
|
70
|
+
class Cache
|
71
|
+
# Mutex used to synchronize access to the cache. Uses a
|
72
|
+
# singleton mutex to reduce memory.
|
73
|
+
MUTEX = ::Mutex.new
|
74
|
+
|
75
|
+
# Initialize the cache.
|
76
|
+
def initialize
|
77
|
+
MUTEX.synchronize{@cache = {}}
|
78
|
+
end
|
79
|
+
|
80
|
+
# Clear the cache.
|
81
|
+
alias clear initialize
|
82
|
+
|
83
|
+
# If the template is found in the cache under the given key,
|
84
|
+
# return it, otherwise yield to get the template, and
|
85
|
+
# store the template under the given key
|
86
|
+
def fetch(key)
|
87
|
+
unless template = MUTEX.synchronize{@cache[key]}
|
88
|
+
template = yield
|
89
|
+
MUTEX.synchronize{@cache[key] = template}
|
90
|
+
end
|
91
|
+
|
92
|
+
template
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Setup default rendering options. See Render for details.
|
97
|
+
def self.configure(app, opts={})
|
98
|
+
if app.opts[:render]
|
99
|
+
app.opts[:render].merge!(opts)
|
100
|
+
else
|
101
|
+
app.opts[:render] = opts.dup
|
102
|
+
end
|
103
|
+
|
104
|
+
opts = app.opts[:render]
|
105
|
+
opts[:engine] ||= "erb"
|
106
|
+
opts[:ext] = nil unless opts.has_key?(:ext)
|
107
|
+
opts[:views] ||= File.expand_path("views", Dir.pwd)
|
108
|
+
opts[:layout] = "layout" unless opts.has_key?(:layout)
|
109
|
+
opts[:layout_opts] ||= (opts[:layout_opts] || {}).dup
|
110
|
+
opts[:opts] ||= (opts[:opts] || {}).dup
|
111
|
+
opts[:opts][:outvar] ||= '@_out_buf'
|
112
|
+
if RUBY_VERSION >= "1.9"
|
113
|
+
opts[:opts][:default_encoding] ||= Encoding.default_external
|
114
|
+
end
|
115
|
+
cache = opts.fetch(:cache, true)
|
116
|
+
opts[:cache] = Cache.new if cache == true
|
117
|
+
end
|
118
|
+
|
119
|
+
module ClassMethods
|
120
|
+
# Copy the rendering options into the subclass, duping
|
121
|
+
# them as necessary to prevent changes in the subclass
|
122
|
+
# affecting the parent class.
|
123
|
+
def inherited(subclass)
|
124
|
+
super
|
125
|
+
opts = subclass.opts[:render] = render_opts.dup
|
126
|
+
opts[:layout_opts] = opts[:layout_opts].dup
|
127
|
+
opts[:opts] = opts[:opts].dup
|
128
|
+
opts[:cache] = Cache.new if opts[:cache]
|
129
|
+
end
|
130
|
+
|
131
|
+
# Return the render options for this class.
|
132
|
+
def render_opts
|
133
|
+
opts[:render]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
module InstanceMethods
|
138
|
+
# Render the given template. See Render for details.
|
139
|
+
def render(template, opts = {}, &block)
|
140
|
+
if template.is_a?(Hash)
|
141
|
+
if opts.empty?
|
142
|
+
opts = template
|
143
|
+
else
|
144
|
+
opts = opts.merge(template)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
render_opts = render_opts()
|
148
|
+
|
149
|
+
if content = opts[:inline]
|
150
|
+
path = content
|
151
|
+
template_block = Proc.new{content}
|
152
|
+
template_class = ::Tilt[opts[:engine] || render_opts[:engine]]
|
153
|
+
else
|
154
|
+
template_class = ::Tilt
|
155
|
+
unless path = opts[:path]
|
156
|
+
path = template_path(template, opts)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
cached_template(path) do
|
161
|
+
template_class.new(path, 1, render_opts[:opts].merge(opts), &template_block)
|
162
|
+
end.render(self, opts[:locals], &block)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return the render options for the instance's class.
|
166
|
+
def render_opts
|
167
|
+
self.class.render_opts
|
168
|
+
end
|
169
|
+
|
170
|
+
# Render the given template. If there is a default layout
|
171
|
+
# for the class, take the result of the template rendering
|
172
|
+
# and render it inside the layout. See Render for details.
|
173
|
+
def view(template, opts={})
|
174
|
+
if template.is_a?(Hash)
|
175
|
+
if opts.empty?
|
176
|
+
opts = template
|
177
|
+
else
|
178
|
+
opts = opts.merge(template)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
content = render(template, opts)
|
183
|
+
|
184
|
+
if layout = opts.fetch(:layout, render_opts[:layout])
|
185
|
+
if layout_opts = opts[:layout_opts]
|
186
|
+
layout_opts = render_opts[:layout_opts].merge(layout_opts)
|
187
|
+
end
|
188
|
+
|
189
|
+
content = render(layout, layout_opts||{}){content}
|
190
|
+
end
|
191
|
+
|
192
|
+
content
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
# If caching templates, attempt to retrieve the template from the cache. Otherwise, just yield
|
198
|
+
# to get the template.
|
199
|
+
def cached_template(path, &block)
|
200
|
+
if cache = render_opts[:cache]
|
201
|
+
cache.fetch(path, &block)
|
202
|
+
else
|
203
|
+
yield
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# The path for the given template.
|
208
|
+
def template_path(template, opts)
|
209
|
+
render_opts = render_opts()
|
210
|
+
"#{opts[:views] || render_opts[:views]}/#{template}.#{opts[:ext] || render_opts[:ext] || render_opts[:engine]}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
register_plugin(:render, Render)
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The streaming plugin adds support for streaming responses
|
4
|
+
# from roda using the +stream+ method:
|
5
|
+
#
|
6
|
+
# plugin :streaming
|
7
|
+
#
|
8
|
+
# route do |r|
|
9
|
+
# stream do |out|
|
10
|
+
# ['a', 'b', 'c'].each{|v| out << v; sleep 1}
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# In order for streaming to work, any webservers used in
|
15
|
+
# front of the roda app must not buffer responses.
|
16
|
+
#
|
17
|
+
# The stream method takes the following options:
|
18
|
+
#
|
19
|
+
# :callback :: A callback proc to call when the connection is
|
20
|
+
# closed.
|
21
|
+
# :keep_open :: Whether to keep the connection open after the
|
22
|
+
# stream block returns, default is false.
|
23
|
+
# :loop :: Whether to call the stream block continuously until
|
24
|
+
# the connection is closed.
|
25
|
+
#
|
26
|
+
# The implementation was originally taken from Sinatra,
|
27
|
+
# which is also released under the MIT License:
|
28
|
+
#
|
29
|
+
# Copyright (c) 2007, 2008, 2009 Blake Mizerany
|
30
|
+
# Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase
|
31
|
+
#
|
32
|
+
# Permission is hereby granted, free of charge, to any person
|
33
|
+
# obtaining a copy of this software and associated documentation
|
34
|
+
# files (the "Software"), to deal in the Software without
|
35
|
+
# restriction, including without limitation the rights to use,
|
36
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
37
|
+
# copies of the Software, and to permit persons to whom the
|
38
|
+
# Software is furnished to do so, subject to the following
|
39
|
+
# conditions:
|
40
|
+
#
|
41
|
+
# The above copyright notice and this permission notice shall be
|
42
|
+
# included in all copies or substantial portions of the Software.
|
43
|
+
#
|
44
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
45
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
46
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
47
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
48
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
49
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
50
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
51
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
52
|
+
module Streaming
|
53
|
+
# Class of the response body in case you use #stream.
|
54
|
+
#
|
55
|
+
# Three things really matter: The front and back block (back being the
|
56
|
+
# block generating content, front the one sending it to the client) and
|
57
|
+
# the scheduler, integrating with whatever concurrency feature the Rack
|
58
|
+
# handler is using.
|
59
|
+
#
|
60
|
+
# Scheduler has to respond to defer and schedule.
|
61
|
+
class Stream
|
62
|
+
include Enumerable
|
63
|
+
|
64
|
+
# The default scheduler to used when streaming, useful for code
|
65
|
+
# using ruby's default threading support.
|
66
|
+
class Scheduler
|
67
|
+
# Store the stream to schedule.
|
68
|
+
def initialize(stream)
|
69
|
+
@stream = stream
|
70
|
+
end
|
71
|
+
|
72
|
+
# Immediately yield.
|
73
|
+
def defer(*)
|
74
|
+
yield
|
75
|
+
end
|
76
|
+
|
77
|
+
# Close the stream if there is an exception when scheduling,
|
78
|
+
# and reraise the exception if so.
|
79
|
+
def schedule(*)
|
80
|
+
yield
|
81
|
+
rescue Exception
|
82
|
+
@stream.close
|
83
|
+
raise
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Handle streaming options, see Streaming for details.
|
88
|
+
def initialize(opts={}, &back)
|
89
|
+
@scheduler = opts[:scheduler] || Scheduler.new(self)
|
90
|
+
@back = back.to_proc
|
91
|
+
@keep_open = opts[:keep_open]
|
92
|
+
@callbacks = []
|
93
|
+
@closed = false
|
94
|
+
|
95
|
+
if opts[:callback]
|
96
|
+
callback(&opts[:callback])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Add output to the streaming response body.
|
101
|
+
def <<(data)
|
102
|
+
@scheduler.schedule{@front.call(data.to_s)}
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Add the given block as a callback to call when the block closes.
|
107
|
+
def callback(&block)
|
108
|
+
return yield if closed?
|
109
|
+
@callbacks << block
|
110
|
+
end
|
111
|
+
|
112
|
+
# Alias to callback for EventMachine compatibility.
|
113
|
+
alias errback callback
|
114
|
+
|
115
|
+
# If not already closed, close the connection, and call
|
116
|
+
# any callbacks.
|
117
|
+
def close
|
118
|
+
return if closed?
|
119
|
+
@closed = true
|
120
|
+
@scheduler.schedule{@callbacks.each{|c| c.call}}
|
121
|
+
end
|
122
|
+
|
123
|
+
# Whether the connection has already been closed.
|
124
|
+
def closed?
|
125
|
+
@closed
|
126
|
+
end
|
127
|
+
|
128
|
+
# Yield values to the block as they are passed in via #<<.
|
129
|
+
def each(&front)
|
130
|
+
@front = front
|
131
|
+
@scheduler.defer do
|
132
|
+
begin
|
133
|
+
@back.call(self)
|
134
|
+
rescue Exception => e
|
135
|
+
@scheduler.schedule{raise e}
|
136
|
+
end
|
137
|
+
close unless @keep_open
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
module InstanceMethods
|
143
|
+
# Immediately return a streaming response using the current response
|
144
|
+
# status and headers, calling the block to get the streaming response.
|
145
|
+
# See Streaming for details.
|
146
|
+
def stream(opts={}, &block)
|
147
|
+
opts = opts.merge(:scheduler=>EventMachine) if !opts.has_key?(:scheduler) && env['async.callback']
|
148
|
+
|
149
|
+
if opts[:loop]
|
150
|
+
block = proc do |out|
|
151
|
+
until out.closed?
|
152
|
+
yield(out)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
res = response
|
158
|
+
request.halt [res.status || 200, res.headers, Stream.new(opts, &block)]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
register_plugin(:streaming, Streaming)
|
164
|
+
end
|
165
|
+
end
|
data/lib/roda/version.rb
ADDED