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