roda 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +34 -0
- data/README.rdoc +18 -13
- data/Rakefile +8 -0
- data/doc/conventions.rdoc +163 -0
- data/doc/release_notes/1.1.0.txt +226 -0
- data/lib/roda.rb +51 -22
- data/lib/roda/plugins/assets.rb +613 -0
- data/lib/roda/plugins/caching.rb +215 -0
- data/lib/roda/plugins/chunked.rb +278 -0
- data/lib/roda/plugins/error_email.rb +112 -0
- data/lib/roda/plugins/flash.rb +3 -3
- data/lib/roda/plugins/hooks.rb +1 -1
- data/lib/roda/plugins/indifferent_params.rb +3 -3
- data/lib/roda/plugins/middleware.rb +3 -8
- data/lib/roda/plugins/multi_route.rb +110 -18
- data/lib/roda/plugins/not_allowed.rb +3 -3
- data/lib/roda/plugins/path.rb +38 -0
- data/lib/roda/plugins/render.rb +18 -16
- data/lib/roda/plugins/render_each.rb +0 -2
- data/lib/roda/plugins/streaming.rb +1 -2
- data/lib/roda/plugins/view_subdirs.rb +7 -1
- data/lib/roda/version.rb +1 -1
- data/spec/assets/css/app.scss +1 -0
- data/spec/assets/css/no_access.css +1 -0
- data/spec/assets/css/raw.css +1 -0
- data/spec/assets/js/head/app.js +1 -0
- data/spec/integration_spec.rb +95 -3
- data/spec/matchers_spec.rb +2 -2
- data/spec/plugin/assets_spec.rb +413 -0
- data/spec/plugin/caching_spec.rb +335 -0
- data/spec/plugin/chunked_spec.rb +182 -0
- data/spec/plugin/default_headers_spec.rb +6 -5
- data/spec/plugin/error_email_spec.rb +76 -0
- data/spec/plugin/multi_route_spec.rb +120 -0
- data/spec/plugin/not_allowed_spec.rb +14 -3
- data/spec/plugin/path_spec.rb +29 -0
- data/spec/plugin/render_each_spec.rb +6 -1
- data/spec/plugin/symbol_matchers_spec.rb +7 -2
- data/spec/request_spec.rb +10 -0
- data/spec/response_spec.rb +47 -0
- data/spec/views/about.erb +1 -0
- data/spec/views/about.str +1 -0
- data/spec/views/content-yield.erb +1 -0
- data/spec/views/home.erb +2 -0
- data/spec/views/home.str +2 -0
- data/spec/views/layout-alternative.erb +2 -0
- data/spec/views/layout-yield.erb +3 -0
- data/spec/views/layout.erb +2 -0
- data/spec/views/layout.str +2 -0
- metadata +57 -2
@@ -0,0 +1,215 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The caching plugin adds methods related to HTTP caching.
|
4
|
+
#
|
5
|
+
# For proper caching, you should use either the +last_modified+ or
|
6
|
+
# +etag+ request methods.
|
7
|
+
#
|
8
|
+
# r.get '/albums/:d' do |album_id|
|
9
|
+
# @album = Album[album_id]
|
10
|
+
# r.last_modified @album.updated_at
|
11
|
+
# view('album')
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # or
|
15
|
+
#
|
16
|
+
# r.get '/albums/:d' do |album_id|
|
17
|
+
# @album = Album[album_id]
|
18
|
+
# r.etag @album.sha1
|
19
|
+
# view('album')
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Both +last_modified+ or +etag+ will immediately halt processing
|
23
|
+
# if there have been no modifications since the last time the
|
24
|
+
# client requested the resource, assuming the client uses the
|
25
|
+
# appropriate HTTP 1.1 request headers.
|
26
|
+
#
|
27
|
+
# This plugin also includes the +cache_control+ and +expires+
|
28
|
+
# response methods. The +cache_control+ method sets the
|
29
|
+
# Cache-Control header using the given hash:
|
30
|
+
#
|
31
|
+
# response.cache_control :public=>true, :max_age=>60
|
32
|
+
# # Cache-Control: public, max-age=60
|
33
|
+
#
|
34
|
+
# The +expires+ method is similar, but in addition
|
35
|
+
# to setting the HTTP 1.1 Cache-Control header, it also sets
|
36
|
+
# the HTTP 1.0 Expires header:
|
37
|
+
#
|
38
|
+
# response.expires 60, :public=>true
|
39
|
+
# # Cache-Control: public, max-age=60
|
40
|
+
# # Expires: Mon, 29 Sep 2014 21:25:47 GMT
|
41
|
+
#
|
42
|
+
# The implementation was originally taken from Sinatra,
|
43
|
+
# which is also released under the MIT License:
|
44
|
+
#
|
45
|
+
# Copyright (c) 2007, 2008, 2009 Blake Mizerany
|
46
|
+
# Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase
|
47
|
+
#
|
48
|
+
# Permission is hereby granted, free of charge, to any person
|
49
|
+
# obtaining a copy of this software and associated documentation
|
50
|
+
# files (the "Software"), to deal in the Software without
|
51
|
+
# restriction, including without limitation the rights to use,
|
52
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
53
|
+
# copies of the Software, and to permit persons to whom the
|
54
|
+
# Software is furnished to do so, subject to the following
|
55
|
+
# conditions:
|
56
|
+
#
|
57
|
+
# The above copyright notice and this permission notice shall be
|
58
|
+
# included in all copies or substantial portions of the Software.
|
59
|
+
#
|
60
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
61
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
62
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
63
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
64
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
65
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
66
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
67
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
68
|
+
module Caching
|
69
|
+
module RequestMethods
|
70
|
+
LAST_MODIFIED = 'Last-Modified'.freeze
|
71
|
+
HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
|
72
|
+
HTTP_IF_MATCH = 'HTTP_IF_MATCH'.freeze
|
73
|
+
HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
|
74
|
+
HTTP_IF_UNMODIFIED_SINCE = 'HTTP_IF_UNMODIFIED_SINCE'.freeze
|
75
|
+
ETAG = 'ETag'.freeze
|
76
|
+
STAR = '*'.freeze
|
77
|
+
|
78
|
+
# Set the last modified time of the resource using the Last-Modified header.
|
79
|
+
# The +time+ argument should be a Time instance.
|
80
|
+
#
|
81
|
+
# If the current request includes an If-Modified-Since header that is
|
82
|
+
# equal or later than the time specified, immediately returns a response
|
83
|
+
# with a 304 status.
|
84
|
+
#
|
85
|
+
# If the current request includes an If-Unmodified-Since header that is
|
86
|
+
# before than the time specified, immediately returns a response
|
87
|
+
# with a 412 status.
|
88
|
+
def last_modified(time)
|
89
|
+
return unless time
|
90
|
+
response[LAST_MODIFIED] = time.httpdate
|
91
|
+
e = env
|
92
|
+
return if e[HTTP_IF_NONE_MATCH]
|
93
|
+
status = response.status
|
94
|
+
|
95
|
+
if (!status || status == 200) && (ims = time_from_header(e[HTTP_IF_MODIFIED_SINCE])) && ims >= time.to_i
|
96
|
+
response.status = 304
|
97
|
+
halt
|
98
|
+
end
|
99
|
+
|
100
|
+
if (!status || (status >= 200 && status < 300) || status == 412) && (ius = time_from_header(e[HTTP_IF_UNMODIFIED_SINCE])) && ius < time.to_i
|
101
|
+
response.status = 412
|
102
|
+
halt
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Set the response entity tag using the ETag header.
|
107
|
+
#
|
108
|
+
# The +value+ argument is an identifier that uniquely
|
109
|
+
# identifies the current version of the resource.
|
110
|
+
# Options:
|
111
|
+
# :weak :: Use a weak cache validator (a strong cache validator is the default)
|
112
|
+
# :new_resource :: Whether this etag should match an etag of * (true for POST, false otherwise)
|
113
|
+
#
|
114
|
+
# When the current request includes an If-None-Match header with a
|
115
|
+
# matching etag, immediately returns a response with a 304 or 412 status,
|
116
|
+
# depending on the request method.
|
117
|
+
#
|
118
|
+
# When the current request includes an If-Match header with a
|
119
|
+
# etag that doesn't match, immediately returns a response with a 412 status.
|
120
|
+
def etag(value, opts={})
|
121
|
+
# Before touching this code, please double check RFC 2616 14.24 and 14.26.
|
122
|
+
weak = opts[:weak]
|
123
|
+
new_resource = opts.fetch(:new_resource){post?}
|
124
|
+
|
125
|
+
response[ETAG] = etag = "#{'W/' if weak}\"#{value}\""
|
126
|
+
status = response.status
|
127
|
+
e = env
|
128
|
+
|
129
|
+
if (!status || (status >= 200 && status < 300) || status == 304)
|
130
|
+
if etag_matches?(e[HTTP_IF_NONE_MATCH], etag, new_resource)
|
131
|
+
response.status = (request_method =~ /\AGET|HEAD|OPTIONS|TRACE\z/i ? 304 : 412)
|
132
|
+
halt
|
133
|
+
end
|
134
|
+
|
135
|
+
if ifm = e[HTTP_IF_MATCH]
|
136
|
+
unless etag_matches?(ifm, etag, new_resource)
|
137
|
+
response.status = 412
|
138
|
+
halt
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Helper method checking if a ETag value list includes the current ETag.
|
147
|
+
def etag_matches?(list, etag, new_resource)
|
148
|
+
return unless list
|
149
|
+
return !new_resource if list == STAR
|
150
|
+
list.to_s.split(/\s*,\s*/).include?(etag)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Helper method parsing a time value from an HTTP header, returning the
|
154
|
+
# time as an integer.
|
155
|
+
def time_from_header(t)
|
156
|
+
Time.httpdate(t).to_i if t
|
157
|
+
rescue ArgumentError
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
module ResponseMethods
|
162
|
+
UNDERSCORE = '_'.freeze
|
163
|
+
DASH = '-'.freeze
|
164
|
+
COMMA = ', '.freeze
|
165
|
+
CACHE_CONTROL = 'Cache-Control'.freeze
|
166
|
+
EXPIRES = 'Expires'.freeze
|
167
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
168
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
169
|
+
|
170
|
+
# Specify response freshness policy for using the Cache-Control header.
|
171
|
+
# Options can can any non-value directives (:public, :private, :no_cache,
|
172
|
+
# :no_store, :must_revalidate, :proxy_revalidate), with true as the value.
|
173
|
+
# Options can also contain value directives (:max_age, :s_maxage).
|
174
|
+
#
|
175
|
+
# response.cache_control :public=>true, :max_age => 60
|
176
|
+
# # => Cache-Control: public, max-age=60
|
177
|
+
#
|
178
|
+
# See RFC 2616 / 14.9 for more on standard cache control directives:
|
179
|
+
# http://tools.ietf.org/html/rfc2616#section-14.9.1
|
180
|
+
def cache_control(opts)
|
181
|
+
values = []
|
182
|
+
opts.each do |k, v|
|
183
|
+
next unless v
|
184
|
+
k = k.to_s.tr(UNDERSCORE, DASH)
|
185
|
+
values << (v == true ? k : "#{k}=#{v}")
|
186
|
+
end
|
187
|
+
|
188
|
+
self[CACHE_CONTROL] = values.join(COMMA) unless values.empty?
|
189
|
+
end
|
190
|
+
|
191
|
+
# Set Cache-Control header with the max_age given. max_age should
|
192
|
+
# be an integer number of seconds that the current request should be
|
193
|
+
# cached for. Also sets the Expires header, useful if you have
|
194
|
+
# HTTP 1.0 clients (Cache-Control is an HTTP 1.1 header).
|
195
|
+
def expires(max_age, opts={})
|
196
|
+
cache_control(opts.merge(:max_age=>max_age))
|
197
|
+
self[EXPIRES] = (Time.now + max_age).httpdate
|
198
|
+
end
|
199
|
+
|
200
|
+
# Remove Content-Type and Content-Length for 304 responses.
|
201
|
+
def finish
|
202
|
+
a = super
|
203
|
+
if a[0] == 304
|
204
|
+
h = a[1]
|
205
|
+
h.delete(CONTENT_TYPE)
|
206
|
+
h.delete(CONTENT_LENGTH)
|
207
|
+
end
|
208
|
+
a
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
register_plugin(:caching, Caching)
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The chunked plugin allows you to stream responses to clients using
|
4
|
+
# Transfer-Encoding: chunked. This can significantly improve performance
|
5
|
+
# of page rendering on the client, as it flushes the headers and top part
|
6
|
+
# of the layout template (generally containing references to the stylesheet
|
7
|
+
# and javascript assets) before rendering the content template.
|
8
|
+
#
|
9
|
+
# This allows the client to fetch the assets while the template is still
|
10
|
+
# being rendered. Additionally, this plugin makes it easy to defer
|
11
|
+
# executing code required to render the content template until after
|
12
|
+
# the top part of the layout has been flushed, so the client can fetch the
|
13
|
+
# assets while the application is still doing the necessary processing in
|
14
|
+
# order to render the content template, such as retrieving values from a
|
15
|
+
# database.
|
16
|
+
#
|
17
|
+
# There are a couple disadvantages of streaming using chunked encoding.
|
18
|
+
# First is that the layout must be rendered before the content, so any state
|
19
|
+
# changes made in your content template will not affect the layout template.
|
20
|
+
# Second, error handling is reduced, since if an error occurs while
|
21
|
+
# rendering a template, a successful response code has already been sent.
|
22
|
+
#
|
23
|
+
# To use chunked encoding for a response, just call the chunked method
|
24
|
+
# instead of view:
|
25
|
+
#
|
26
|
+
# r.root do
|
27
|
+
# chunked(:index)
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# If you want to execute code after flushing the top part of the layout
|
31
|
+
# template, but before rendering the content template, pass a block to
|
32
|
+
# chunked:
|
33
|
+
#
|
34
|
+
# r.root do
|
35
|
+
# chunked(:index) do
|
36
|
+
# # expensive calculation here
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# If you want to chunk all responses, pass the :chunk_by_default option
|
41
|
+
# when loading the plugin:
|
42
|
+
#
|
43
|
+
# plugin :chunked, :chunk_by_default => true
|
44
|
+
#
|
45
|
+
# then you can just use the normal view method:
|
46
|
+
#
|
47
|
+
# r.root do
|
48
|
+
# view(:index)
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# and it will chunk the response. Note that you still need to call
|
52
|
+
# chunked if you want to pass a block of code to be executed after flushing
|
53
|
+
# the layout and before rendering the content template. Also, before you
|
54
|
+
# enable chunking by default, you need to make sure that none of your
|
55
|
+
# content templates make state changes that affect the layout template.
|
56
|
+
# Additionally, make sure nowhere in your app are you doing any processing
|
57
|
+
# after the call to view.
|
58
|
+
#
|
59
|
+
# If you use :chunk_by_default, but want to turn off chunking for a view,
|
60
|
+
# call no_chunk!:
|
61
|
+
#
|
62
|
+
# r.root do
|
63
|
+
# no_chunk!
|
64
|
+
# view(:index)
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# Inside your layout or content templates, you can call the flush method
|
68
|
+
# to flush the current result of the template to the user, useful for
|
69
|
+
# streaming large datasets.
|
70
|
+
#
|
71
|
+
# <% (1..100).each do |i| %>
|
72
|
+
# <%= i %>
|
73
|
+
# <% sleep 0.1 %>
|
74
|
+
# <% flush %>
|
75
|
+
# <% end %>
|
76
|
+
#
|
77
|
+
# Note that you should not call flush from inside subtemplates of the
|
78
|
+
# content or layout templates, unless you are also calling flush directly
|
79
|
+
# before rendering the subtemplate, and also directly injecting the
|
80
|
+
# subtemplate into the current template without modification. So if you
|
81
|
+
# are using the above template code in a subtemplate, in your content
|
82
|
+
# template you should do:
|
83
|
+
#
|
84
|
+
# <% flush %><%= render(:subtemplate) %>
|
85
|
+
#
|
86
|
+
# If you want to use chunked encoding when rendering a template, but don't
|
87
|
+
# want to use a layout, pass the :layout=>false option to chunked.
|
88
|
+
#
|
89
|
+
# r.root do
|
90
|
+
# chunked(:index, :layout=>false)
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# In order to handle errors in chunked responses, you can override the
|
94
|
+
# handle_chunk_error method:
|
95
|
+
#
|
96
|
+
# def handle_chunk_error(e)
|
97
|
+
# env['rack.logger'].error(e)
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# It is possible to set @_out_buf to an error notification and call
|
101
|
+
# flush to output the message to the client inside handle_chunk_error.
|
102
|
+
#
|
103
|
+
# In order for chunking to work, you must make sure that no proxies between
|
104
|
+
# the application and the client buffer responses. Also, this
|
105
|
+
# plugin only works for HTTP/1.1 requests since Transfer-Encoding: chunked
|
106
|
+
# is not supported in HTTP/1.0. If an HTTP/1.0 request is submitted, this
|
107
|
+
# plugin will automatically fallback to the normal template rendering.
|
108
|
+
# Note that some proxies including nginx default to HTTP/1.0 even if the
|
109
|
+
# client supports HTTP/1.1. For nginx, set the proxy_http_version to 1.1.
|
110
|
+
#
|
111
|
+
# If you are using nginx and have it set to buffer proxy responses by
|
112
|
+
# default, you can turn this off on a per response basis using the
|
113
|
+
# X-Accel-Buffering header. To set this header or similar headers for
|
114
|
+
# all chunked responses, pass a :headers option when loading the plugin:
|
115
|
+
#
|
116
|
+
# plugin :chunked, :headers=>{'X-Accel-Buffering'=>'no'}
|
117
|
+
#
|
118
|
+
# The chunked plugin requires the render plugin, and only works for
|
119
|
+
# template engines that store their template output variable in
|
120
|
+
# @_out_buf. Also, it only works if the content template is directly
|
121
|
+
# injected into the layout template without modification.
|
122
|
+
#
|
123
|
+
# If using the chunked plugin with the flash plugin, make sure you
|
124
|
+
# call the flash method early in your route block. If the flash
|
125
|
+
# method is not called until template rendering, the flash may not be
|
126
|
+
# rotated.
|
127
|
+
module Chunked
|
128
|
+
HTTP_VERSION = 'HTTP_VERSION'.freeze
|
129
|
+
HTTP11 = "HTTP/1.1".freeze
|
130
|
+
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
|
131
|
+
CHUNKED = 'chunked'.freeze
|
132
|
+
OPTS = {}.freeze
|
133
|
+
|
134
|
+
# Depend on the render plugin
|
135
|
+
def self.load_dependencies(app, opts=OPTS)
|
136
|
+
app.plugin :render
|
137
|
+
end
|
138
|
+
|
139
|
+
# Set plugin specific options. Options:
|
140
|
+
# :chunk_by_default :: chunk all calls to view by default
|
141
|
+
# :headers :: Set default additional headers to use when calling view
|
142
|
+
def self.configure(app, opts=OPTS)
|
143
|
+
app.opts[:chunk_by_default] = opts[:chunk_by_default]
|
144
|
+
app.opts[:chunk_headers] = opts[:headers]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Rack response body instance for chunked responses
|
148
|
+
class Body
|
149
|
+
CHUNK_SIZE = "%x\r\n".freeze
|
150
|
+
CRLF = "\r\n".freeze
|
151
|
+
FINISH = "0\r\n\r\n".freeze
|
152
|
+
|
153
|
+
# Save the scope of the current request handling.
|
154
|
+
def initialize(scope)
|
155
|
+
@scope = scope
|
156
|
+
end
|
157
|
+
|
158
|
+
# For each response chunk yielded by the scope,
|
159
|
+
# yield it it to the caller in chunked format, starting
|
160
|
+
# with the size of the request in ASCII hex format, then
|
161
|
+
# the chunk. After all chunks have been yielded, yield
|
162
|
+
# a 0 sized chunk to finish the response.
|
163
|
+
def each
|
164
|
+
@scope.each_chunk do |chunk|
|
165
|
+
next if !chunk || chunk.empty?
|
166
|
+
yield(CHUNK_SIZE % chunk.bytesize)
|
167
|
+
yield(chunk)
|
168
|
+
yield(CRLF)
|
169
|
+
end
|
170
|
+
ensure
|
171
|
+
yield(FINISH)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
module InstanceMethods
|
176
|
+
# Disable chunking for the current request. Mostly useful when
|
177
|
+
# chunking is turned on by default.
|
178
|
+
def no_chunk!
|
179
|
+
@_chunked = false
|
180
|
+
end
|
181
|
+
|
182
|
+
# If chunking by default, call chunked if it hasn't yet been
|
183
|
+
# called and chunking is not specifically disabled.
|
184
|
+
def view(*a)
|
185
|
+
if opts[:chunk_by_default] && !defined?(@_chunked)
|
186
|
+
chunked(*a)
|
187
|
+
else
|
188
|
+
super
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Render a response to the user in chunks. See Chunked for
|
193
|
+
# an overview.
|
194
|
+
def chunked(template, opts=OPTS, &block)
|
195
|
+
unless defined?(@_chunked)
|
196
|
+
@_chunked = env[HTTP_VERSION] == HTTP11
|
197
|
+
end
|
198
|
+
|
199
|
+
unless @_chunked
|
200
|
+
# If chunking is disabled, do a normal rendering of the view.
|
201
|
+
yield if block
|
202
|
+
return view(template, opts)
|
203
|
+
end
|
204
|
+
|
205
|
+
if template.is_a?(Hash)
|
206
|
+
if opts.empty?
|
207
|
+
opts = template
|
208
|
+
else
|
209
|
+
opts = opts.merge(template)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Hack so that the arguments don't need to be passed
|
214
|
+
# through the response and body objects.
|
215
|
+
@_each_chunk_args = [template, opts, block]
|
216
|
+
|
217
|
+
res = response
|
218
|
+
headers = res.headers
|
219
|
+
if chunk_headers = self.opts[:chunk_headers]
|
220
|
+
headers.merge!(chunk_headers)
|
221
|
+
end
|
222
|
+
headers[TRANSFER_ENCODING] = CHUNKED
|
223
|
+
|
224
|
+
throw :halt, res.finish_with_body(Body.new(self))
|
225
|
+
end
|
226
|
+
|
227
|
+
# Yield each chunk of the template rendering separately.
|
228
|
+
def each_chunk
|
229
|
+
response.body.each{|s| yield s}
|
230
|
+
|
231
|
+
template, opts, block = @_each_chunk_args
|
232
|
+
|
233
|
+
# Use a lambda for the flusher, so that a call to flush
|
234
|
+
# by a template can result in this method yielding a chunk
|
235
|
+
# of the response.
|
236
|
+
@_flusher = lambda do
|
237
|
+
yield @_out_buf
|
238
|
+
@_out_buf = ''
|
239
|
+
end
|
240
|
+
|
241
|
+
if layout = opts.fetch(:layout, render_opts[:layout])
|
242
|
+
if layout_opts = opts[:layout_opts]
|
243
|
+
layout_opts = render_opts[:layout_opts].merge(layout_opts)
|
244
|
+
end
|
245
|
+
|
246
|
+
@_out_buf = render(layout, layout_opts||OPTS) do
|
247
|
+
flush
|
248
|
+
block.call if block
|
249
|
+
yield opts[:content] || render(template, opts)
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
else
|
253
|
+
yield if block
|
254
|
+
yield view(template, opts)
|
255
|
+
end
|
256
|
+
|
257
|
+
flush
|
258
|
+
rescue => e
|
259
|
+
handle_chunk_error(e)
|
260
|
+
end
|
261
|
+
|
262
|
+
# By default, raise the exception.
|
263
|
+
def handle_chunk_error(e)
|
264
|
+
raise e
|
265
|
+
end
|
266
|
+
|
267
|
+
# Call the flusher if one is defined. If one is not defined, this
|
268
|
+
# is a no-op, so flush can be used inside views without breaking
|
269
|
+
# things if chunking is not used.
|
270
|
+
def flush
|
271
|
+
@_flusher.call if @_flusher
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
register_plugin(:chunked, Chunked)
|
277
|
+
end
|
278
|
+
end
|