roda 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +70 -0
- data/README.rdoc +261 -302
- data/Rakefile +1 -1
- data/doc/release_notes/1.2.0.txt +406 -0
- data/lib/roda.rb +206 -124
- data/lib/roda/plugins/all_verbs.rb +11 -10
- data/lib/roda/plugins/assets.rb +5 -5
- data/lib/roda/plugins/backtracking_array.rb +12 -5
- data/lib/roda/plugins/caching.rb +10 -8
- data/lib/roda/plugins/class_level_routing.rb +94 -0
- data/lib/roda/plugins/content_for.rb +6 -0
- data/lib/roda/plugins/default_headers.rb +4 -11
- data/lib/roda/plugins/delay_build.rb +42 -0
- data/lib/roda/plugins/delegate.rb +64 -0
- data/lib/roda/plugins/drop_body.rb +33 -0
- data/lib/roda/plugins/empty_root.rb +48 -0
- data/lib/roda/plugins/environments.rb +68 -0
- data/lib/roda/plugins/error_email.rb +1 -2
- data/lib/roda/plugins/error_handler.rb +1 -1
- data/lib/roda/plugins/halt.rb +7 -5
- data/lib/roda/plugins/head.rb +4 -2
- data/lib/roda/plugins/header_matchers.rb +17 -9
- data/lib/roda/plugins/hooks.rb +16 -32
- data/lib/roda/plugins/json.rb +4 -10
- data/lib/roda/plugins/mailer.rb +233 -0
- data/lib/roda/plugins/match_affix.rb +48 -0
- data/lib/roda/plugins/multi_route.rb +9 -11
- data/lib/roda/plugins/multi_run.rb +81 -0
- data/lib/roda/plugins/named_templates.rb +93 -0
- data/lib/roda/plugins/not_allowed.rb +43 -48
- data/lib/roda/plugins/path.rb +63 -2
- data/lib/roda/plugins/render.rb +79 -48
- data/lib/roda/plugins/render_each.rb +6 -0
- data/lib/roda/plugins/sinatra_helpers.rb +523 -0
- data/lib/roda/plugins/slash_path_empty.rb +25 -0
- data/lib/roda/plugins/static_path_info.rb +64 -0
- data/lib/roda/plugins/streaming.rb +1 -1
- data/lib/roda/plugins/view_subdirs.rb +12 -8
- data/lib/roda/version.rb +1 -1
- data/spec/integration_spec.rb +33 -0
- data/spec/plugin/backtracking_array_spec.rb +24 -18
- data/spec/plugin/class_level_routing_spec.rb +138 -0
- data/spec/plugin/delay_build_spec.rb +23 -0
- data/spec/plugin/delegate_spec.rb +20 -0
- data/spec/plugin/drop_body_spec.rb +20 -0
- data/spec/plugin/empty_root_spec.rb +14 -0
- data/spec/plugin/environments_spec.rb +31 -0
- data/spec/plugin/h_spec.rb +1 -3
- data/spec/plugin/header_matchers_spec.rb +14 -0
- data/spec/plugin/hooks_spec.rb +3 -5
- data/spec/plugin/mailer_spec.rb +191 -0
- data/spec/plugin/match_affix_spec.rb +22 -0
- data/spec/plugin/multi_run_spec.rb +31 -0
- data/spec/plugin/named_templates_spec.rb +65 -0
- data/spec/plugin/path_spec.rb +66 -2
- data/spec/plugin/render_spec.rb +46 -1
- data/spec/plugin/sinatra_helpers_spec.rb +534 -0
- data/spec/plugin/slash_path_empty_spec.rb +22 -0
- data/spec/plugin/static_path_info_spec.rb +50 -0
- data/spec/request_spec.rb +23 -0
- data/spec/response_spec.rb +12 -1
- metadata +48 -6
data/lib/roda/plugins/render.rb
CHANGED
@@ -25,18 +25,19 @@ class Roda
|
|
25
25
|
# The following options are supported:
|
26
26
|
#
|
27
27
|
# :cache :: nil/false to not cache templates (useful for development), defaults
|
28
|
-
# to true to automatically use the
|
28
|
+
# to true unless RACK_ENV is development to automatically use the
|
29
|
+
# default template cache.
|
29
30
|
# :engine :: The tilt engine to use for rendering, defaults to 'erb'.
|
30
31
|
# :escape :: Use Roda's Erubis escaping support, which makes <%= %> escape output,
|
31
|
-
#
|
32
|
-
#
|
32
|
+
# <tt><%== %></tt> not escape output, and handles postfix conditions inside
|
33
|
+
# <tt><%= %></tt> tags.
|
33
34
|
# :ext :: The file extension to assume for view files, defaults to the :engine
|
34
35
|
# option.
|
35
36
|
# :layout :: The base name of the layout file, defaults to 'layout'.
|
36
37
|
# :layout_opts :: The options to use when rendering the layout, if different
|
37
38
|
# from the default options.
|
38
39
|
# :opts :: The tilt options used when rendering templates, defaults to
|
39
|
-
# {:outvar=>'@_out_buf'}
|
40
|
+
# <tt>{:outvar=>'@_out_buf', :default_encoding=>Encoding.default_external}</tt>.
|
40
41
|
# :views :: The directory holding the view files, defaults to 'views' in the
|
41
42
|
# current directory.
|
42
43
|
#
|
@@ -58,6 +59,13 @@ class Roda
|
|
58
59
|
# :path :: Use the value given as the full pathname for the file, instead
|
59
60
|
# of using the :views and :ext option in combination with the
|
60
61
|
# template name.
|
62
|
+
# :template :: Provides the name of the template to use. This allows you
|
63
|
+
# pass a single options hash to the render/view method, while
|
64
|
+
# still allowing you to specify the template name.
|
65
|
+
# :template_block :: Pass this block when creating the underlying template,
|
66
|
+
# ignored when using :inline.
|
67
|
+
# :template_class :: Provides the template class to use, inside of using
|
68
|
+
# Tilt or a Tilt[:engine].
|
61
69
|
#
|
62
70
|
# Here's how those options are used:
|
63
71
|
#
|
@@ -89,15 +97,21 @@ class Roda
|
|
89
97
|
opts[:views] ||= File.expand_path("views", Dir.pwd)
|
90
98
|
opts[:layout] = "layout" unless opts.has_key?(:layout)
|
91
99
|
opts[:layout_opts] ||= (opts[:layout_opts] || {}).dup
|
100
|
+
|
101
|
+
if layout = opts[:layout]
|
102
|
+
layout = {:template=>layout} unless layout.is_a?(Hash)
|
103
|
+
opts[:layout_opts] = opts[:layout_opts].merge(layout)
|
104
|
+
end
|
105
|
+
|
92
106
|
opts[:opts] ||= (opts[:opts] || {}).dup
|
93
107
|
opts[:opts][:outvar] ||= '@_out_buf'
|
94
|
-
if RUBY_VERSION >= "1.9"
|
95
|
-
opts[:opts][:default_encoding]
|
108
|
+
if RUBY_VERSION >= "1.9" && !opts[:opts].has_key?(:default_encoding)
|
109
|
+
opts[:opts][:default_encoding] = Encoding.default_external
|
96
110
|
end
|
97
111
|
if opts[:escape]
|
98
112
|
opts[:opts][:engine_class] = ErubisEscaping::Eruby
|
99
113
|
end
|
100
|
-
opts[:cache] = app.thread_safe_cache if opts.fetch(:cache,
|
114
|
+
opts[:cache] = app.thread_safe_cache if opts.fetch(:cache, ENV['RACK_ENV'] != 'development')
|
101
115
|
end
|
102
116
|
|
103
117
|
module ClassMethods
|
@@ -106,7 +120,7 @@ class Roda
|
|
106
120
|
# affecting the parent class.
|
107
121
|
def inherited(subclass)
|
108
122
|
super
|
109
|
-
opts = subclass.opts[:render]
|
123
|
+
opts = subclass.opts[:render]
|
110
124
|
opts[:layout_opts] = opts[:layout_opts].dup
|
111
125
|
opts[:opts] = opts[:opts].dup
|
112
126
|
opts[:cache] = thread_safe_cache if opts[:cache]
|
@@ -121,28 +135,11 @@ class Roda
|
|
121
135
|
module InstanceMethods
|
122
136
|
# Render the given template. See Render for details.
|
123
137
|
def render(template, opts = OPTS, &block)
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
end
|
130
|
-
end
|
131
|
-
render_opts = render_opts()
|
132
|
-
|
133
|
-
if content = opts[:inline]
|
134
|
-
path = content
|
135
|
-
template_block = Proc.new{content}
|
136
|
-
template_class = ::Tilt[opts[:engine] || render_opts[:engine]]
|
137
|
-
else
|
138
|
-
template_class = ::Tilt
|
139
|
-
unless path = opts[:path]
|
140
|
-
path = template_path(template, opts)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
cached_template(path) do
|
145
|
-
template_class.new(path, 1, render_opts[:opts].merge(opts), &template_block)
|
138
|
+
opts = find_template(parse_template_opts(template, opts))
|
139
|
+
cached_template(opts) do
|
140
|
+
template_opts = render_opts[:opts]
|
141
|
+
template_opts = template_opts.merge(opts[:opts]) if opts[:opts]
|
142
|
+
opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block])
|
146
143
|
end.render(self, (opts[:locals]||OPTS), &block)
|
147
144
|
end
|
148
145
|
|
@@ -157,22 +154,16 @@ class Roda
|
|
157
154
|
# for the class, take the result of the template rendering
|
158
155
|
# and render it inside the layout. See Render for details.
|
159
156
|
def view(template, opts=OPTS)
|
160
|
-
|
161
|
-
|
162
|
-
opts = template
|
163
|
-
else
|
164
|
-
opts = opts.merge(template)
|
165
|
-
end
|
166
|
-
end
|
157
|
+
opts = parse_template_opts(template, opts)
|
158
|
+
content = opts[:content] || render(opts)
|
167
159
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
layout_opts = render_opts[:layout_opts].merge(layout_opts)
|
160
|
+
if layout = opts.fetch(:layout, (OPTS if render_opts[:layout]))
|
161
|
+
layout_opts = render_opts[:layout_opts]
|
162
|
+
if opts[:layout_opts]
|
163
|
+
layout_opts = opts[:layout_opts].merge(layout_opts)
|
173
164
|
end
|
174
165
|
|
175
|
-
content = render(layout, layout_opts
|
166
|
+
content = render(layout, layout_opts){content}
|
176
167
|
end
|
177
168
|
|
178
169
|
content
|
@@ -182,10 +173,11 @@ class Roda
|
|
182
173
|
|
183
174
|
# If caching templates, attempt to retrieve the template from the cache. Otherwise, just yield
|
184
175
|
# to get the template.
|
185
|
-
def cached_template(
|
176
|
+
def cached_template(opts, &block)
|
186
177
|
if cache = render_opts[:cache]
|
187
|
-
|
188
|
-
|
178
|
+
key = opts[:key]
|
179
|
+
unless template = cache[key]
|
180
|
+
template = cache[key] = yield
|
189
181
|
end
|
190
182
|
template
|
191
183
|
else
|
@@ -193,10 +185,49 @@ class Roda
|
|
193
185
|
end
|
194
186
|
end
|
195
187
|
|
188
|
+
# Given the template name and options, return the template class, template path/content,
|
189
|
+
# and template block to use for the render.
|
190
|
+
def find_template(opts)
|
191
|
+
if content = opts[:inline]
|
192
|
+
path = opts[:path] = content
|
193
|
+
template_class = opts[:template_class] ||= ::Tilt[opts[:engine] || render_opts[:engine]]
|
194
|
+
opts[:template_block] = Proc.new{content}
|
195
|
+
else
|
196
|
+
path = opts[:path] ||= template_path(opts)
|
197
|
+
template_class = opts[:template_class]
|
198
|
+
opts[:template_class] ||= ::Tilt
|
199
|
+
end
|
200
|
+
|
201
|
+
if render_opts[:cache]
|
202
|
+
template_opts = opts[:opts]
|
203
|
+
template_block = opts[:template_block] if !content
|
204
|
+
|
205
|
+
key = if template_class || template_opts || template_block
|
206
|
+
[path, template_class, template_opts, template_block]
|
207
|
+
else
|
208
|
+
path
|
209
|
+
end
|
210
|
+
opts[:key] = key
|
211
|
+
end
|
212
|
+
|
213
|
+
opts
|
214
|
+
end
|
215
|
+
|
216
|
+
# Return a single hash combining the template and opts arguments.
|
217
|
+
def parse_template_opts(template, opts)
|
218
|
+
template = {:template=>template} unless template.is_a?(Hash)
|
219
|
+
opts.merge(template)
|
220
|
+
end
|
221
|
+
|
222
|
+
# The name to use for the template. By default, just converts the :template option to a string.
|
223
|
+
def template_name(opts)
|
224
|
+
opts[:template].to_s
|
225
|
+
end
|
226
|
+
|
196
227
|
# The path for the given template.
|
197
|
-
def template_path(
|
228
|
+
def template_path(opts)
|
198
229
|
render_opts = render_opts()
|
199
|
-
"#{opts[:views] || render_opts[:views]}/#{
|
230
|
+
"#{opts[:views] || render_opts[:views]}/#{template_name(opts)}.#{opts[:ext] || render_opts[:ext] || render_opts[:engine]}"
|
200
231
|
end
|
201
232
|
end
|
202
233
|
end
|
@@ -23,6 +23,12 @@ class Roda
|
|
23
23
|
# the template will be +bar+. You can use <tt>:local=>nil</tt> to
|
24
24
|
# not set a local variable inside the template.
|
25
25
|
module RenderEach
|
26
|
+
# Load the render plugin before this plugin, since this plugin
|
27
|
+
# calls the render method.
|
28
|
+
def self.load_dependencies(app)
|
29
|
+
app.plugin :render
|
30
|
+
end
|
31
|
+
|
26
32
|
module InstanceMethods
|
27
33
|
# For each value in enum, render the given template using the
|
28
34
|
# given opts. The template and options hash are passed to +render+.
|
@@ -0,0 +1,523 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The sinatra_helpers plugin ports most of the helper methods
|
4
|
+
# defined in Sinatra::Helpers to Roda, other than those
|
5
|
+
# helpers that were already covered by other plugins such
|
6
|
+
# as caching and streaming.
|
7
|
+
#
|
8
|
+
# Unlike Sinatra, the helper methods are added to either
|
9
|
+
# the request or response classes instead of directly to
|
10
|
+
# the scope of the route block. However, for consistency
|
11
|
+
# with Sinatra, delegate methods are added to the scope
|
12
|
+
# of the route block that call the methods on the request
|
13
|
+
# or response. If you do not want to pollute the namespace
|
14
|
+
# of the route block, you should load the plugin with the
|
15
|
+
# :delegate => false option:
|
16
|
+
#
|
17
|
+
# plugin :sinatra_helpers, :delegate=>false
|
18
|
+
#
|
19
|
+
# == Class Methods Added
|
20
|
+
#
|
21
|
+
# The only class method added by this plugin is +mime_type+,
|
22
|
+
# which is a shortcut for retrieving or setting MIME types
|
23
|
+
# in Rack's MIME database:
|
24
|
+
#
|
25
|
+
# Roda.mime_type 'csv' # => 'text/csv'
|
26
|
+
# Roda.mime_type 'foobar', 'application/foobar' # set
|
27
|
+
#
|
28
|
+
# == Request Methods Added
|
29
|
+
#
|
30
|
+
# In addition to adding the following methods, this changes
|
31
|
+
# +redirect+ to use a 303 response status code by default for
|
32
|
+
# HTTP 1.1 non-GET requests, and to automatically use
|
33
|
+
# absolute URIs if the +:absolute_redirects+ Roda class option
|
34
|
+
# is true, and to automatically prefix redirect paths with the
|
35
|
+
# script name if the +:prefixed_redirects+ Roda class option is
|
36
|
+
# true.
|
37
|
+
#
|
38
|
+
# When adding delegate methods, a logger method is added to
|
39
|
+
# the route block scope that calls the logger method on the request.
|
40
|
+
#
|
41
|
+
# === back
|
42
|
+
#
|
43
|
+
# +back+ is an alias to referrer, so you can do:
|
44
|
+
#
|
45
|
+
# redirect back
|
46
|
+
#
|
47
|
+
# === error
|
48
|
+
#
|
49
|
+
# +error+ sets the response status code to 500 (or a status code you provide),
|
50
|
+
# and halts the request. It takes an optional body:
|
51
|
+
#
|
52
|
+
# error # 500 response, empty boby
|
53
|
+
# error 501 # 501 reponse, empty body
|
54
|
+
# error 'b' # 500 response, 'b' body
|
55
|
+
# error 501, 'b' # 501 response, 'b' body
|
56
|
+
#
|
57
|
+
# === not_found
|
58
|
+
#
|
59
|
+
# +not_found+ sets the response status code to 404 and halts the request.
|
60
|
+
# It takes an optional body:
|
61
|
+
#
|
62
|
+
# not_found # 404 response, empty body
|
63
|
+
# not_found 'b' # 404 response, 'b' body
|
64
|
+
#
|
65
|
+
# === uri
|
66
|
+
#
|
67
|
+
# +uri+ by default returns absolute URIs that are prefixed
|
68
|
+
# by the script name:
|
69
|
+
#
|
70
|
+
# request.script_name # => '/foo'
|
71
|
+
# uri '/bar' # => 'http://example.org/foo/bar'
|
72
|
+
#
|
73
|
+
# You can turn of the absolute or script name prefixing if you want:
|
74
|
+
#
|
75
|
+
# uri '/bar', false # => '/foo/bar'
|
76
|
+
# uri '/bar', true, false # => 'http://example.org/bar'
|
77
|
+
# uri '/bar', false, false # => '/bar'
|
78
|
+
#
|
79
|
+
# This method is aliased as +url+ and +to+.
|
80
|
+
#
|
81
|
+
# === send_file
|
82
|
+
#
|
83
|
+
# This will serve the file with the given path from the file system:
|
84
|
+
#
|
85
|
+
# send_file 'path/to/file.txt'
|
86
|
+
#
|
87
|
+
# Options:
|
88
|
+
#
|
89
|
+
# :disposition :: Set the Content-Disposition to the given disposition.
|
90
|
+
# :filename :: Set the Content-Disposition to attachment (unless :disposition is set),
|
91
|
+
# and set the filename parameter to the value.
|
92
|
+
# :last_modified :: Explicitly set the Last-Modified header to the given value, and
|
93
|
+
# return a not modified response if there has not been modified since
|
94
|
+
# the previous request. This option requires the caching plugin.
|
95
|
+
# :status :: Override the status for the response.
|
96
|
+
# :type :: Set the Content-Type to use for this response.
|
97
|
+
#
|
98
|
+
# == Response Methods Added
|
99
|
+
#
|
100
|
+
# === body
|
101
|
+
#
|
102
|
+
# When called with an argument or block, +body+ sets the body, otherwise
|
103
|
+
# it returns the body:
|
104
|
+
#
|
105
|
+
# body # => []
|
106
|
+
# body('b') # set body to 'b'
|
107
|
+
# body{'b'} # set body to 'b', but don't call until body is needed
|
108
|
+
#
|
109
|
+
# === body=
|
110
|
+
#
|
111
|
+
# +body+ sets the body to the given value:
|
112
|
+
#
|
113
|
+
# response.body = 'v'
|
114
|
+
#
|
115
|
+
# This method is not delegated to the scope of the route block,
|
116
|
+
# call +body+ with an argument to set the value.
|
117
|
+
#
|
118
|
+
# === status
|
119
|
+
#
|
120
|
+
# When called with an argument, +status+ sets the status, otherwise
|
121
|
+
# it returns the status:
|
122
|
+
#
|
123
|
+
# status # => 200
|
124
|
+
# status(301) # sets status to 301
|
125
|
+
#
|
126
|
+
# === headers
|
127
|
+
#
|
128
|
+
# When called with an argument, +headers+ merges the given headers
|
129
|
+
# into the current headers, otherwise it returns the headers:
|
130
|
+
#
|
131
|
+
# headers['Foo'] = 'Bar'
|
132
|
+
# headers 'Foo' => 'Bar'
|
133
|
+
#
|
134
|
+
# === mime_type
|
135
|
+
#
|
136
|
+
# +mime_type+ just calls the Roda class method to get the mime_type.
|
137
|
+
#
|
138
|
+
# === content_type
|
139
|
+
#
|
140
|
+
# When called with an argument, +content_type+ sets the Content-Type
|
141
|
+
# based on the argument, otherwise it returns the Content-Type.
|
142
|
+
#
|
143
|
+
# mime_type # => 'text/html'
|
144
|
+
# mime_type 'csv' # set Content-Type to 'text/csv'
|
145
|
+
# mime_type :csv # set Content-Type to 'text/csv'
|
146
|
+
# mime_type '.csv' # set Content-Type to 'text/csv'
|
147
|
+
# mime_type 'text/csv' # set Content-Type to 'text/csv'
|
148
|
+
#
|
149
|
+
# Options:
|
150
|
+
#
|
151
|
+
# :charset :: Set the charset for the mime type to the given charset, if the charset is
|
152
|
+
# not already set in the mime type.
|
153
|
+
# :default :: Uses the given type if the mime type is not known. If this option is not
|
154
|
+
# used and the mime type is not known, an exception will be raised.
|
155
|
+
#
|
156
|
+
# === attachment
|
157
|
+
#
|
158
|
+
# When called with no filename, +attachment+ just sets the Content-Disposition
|
159
|
+
# to attachment. When called with a filename, this sets the Content-Disposition
|
160
|
+
# to attachment with the appropriate filename parameter, and if the filename
|
161
|
+
# extension is recognized, this also sets the Content-Type to the appropriate
|
162
|
+
# MIME type if not already set.
|
163
|
+
#
|
164
|
+
# attachment # set Content-Disposition to 'attachment'
|
165
|
+
# attachment 'a.csv' # set Content-Disposition to 'attachment;filename="a.csv"',
|
166
|
+
# # also set Content-Type to 'text/csv'
|
167
|
+
#
|
168
|
+
# === status predicates
|
169
|
+
#
|
170
|
+
# This adds the following predicate methods for checking the status:
|
171
|
+
#
|
172
|
+
# informational? # 100-199
|
173
|
+
# success? # 200-299
|
174
|
+
# redirect? # 300-399
|
175
|
+
# client_error? # 400-499
|
176
|
+
# not_found? # 404
|
177
|
+
# server_error? # 500-599
|
178
|
+
#
|
179
|
+
# If the status has not yet been set for the response, these will
|
180
|
+
# return +nil+.
|
181
|
+
#
|
182
|
+
# == License
|
183
|
+
#
|
184
|
+
# The implementation was originally taken from Sinatra,
|
185
|
+
# which is also released under the MIT License:
|
186
|
+
#
|
187
|
+
# Copyright (c) 2007, 2008, 2009 Blake Mizerany
|
188
|
+
# Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase
|
189
|
+
#
|
190
|
+
# Permission is hereby granted, free of charge, to any person
|
191
|
+
# obtaining a copy of this software and associated documentation
|
192
|
+
# files (the "Software"), to deal in the Software without
|
193
|
+
# restriction, including without limitation the rights to use,
|
194
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
195
|
+
# copies of the Software, and to permit persons to whom the
|
196
|
+
# Software is furnished to do so, subject to the following
|
197
|
+
# conditions:
|
198
|
+
#
|
199
|
+
# The above copyright notice and this permission notice shall be
|
200
|
+
# included in all copies or substantial portions of the Software.
|
201
|
+
#
|
202
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
203
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
204
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
205
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
206
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
207
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
208
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
209
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
210
|
+
module SinatraHelpers
|
211
|
+
CONTENT_TYPE = "Content-Type".freeze
|
212
|
+
CONTENT_DISPOSITION = "Content-Disposition".freeze
|
213
|
+
CONTENT_LENGTH = "Content-Length".freeze
|
214
|
+
OCTET_STREAM = 'application/octet-stream'.freeze
|
215
|
+
ATTACHMENT = 'attachment'.freeze
|
216
|
+
HTTP_VERSION = 'HTTP_VERSION'.freeze
|
217
|
+
HTTP11 = "HTTP/1.1".freeze
|
218
|
+
HTTP_X_FORWARDED_HOST = "HTTP_X_FORWARDED_HOST".freeze
|
219
|
+
EMPTY_STRING = ''.freeze
|
220
|
+
SLASH = '/'.freeze
|
221
|
+
SEMICOLON = ';'.freeze
|
222
|
+
COMMA = ', '.freeze
|
223
|
+
CHARSET = 'charset'.freeze
|
224
|
+
OPTS = {}.freeze
|
225
|
+
|
226
|
+
# Add delegate methods to the route block scope
|
227
|
+
# calling request or response methods, unless the
|
228
|
+
# :delegate option is false.
|
229
|
+
def self.configure(app, opts=OPTS)
|
230
|
+
app.send(:include, DelegateMethods) unless opts[:delegate] == false
|
231
|
+
end
|
232
|
+
|
233
|
+
# Class used when the response body is set explicitly, instead
|
234
|
+
# of using Roda's default body array and response.write to
|
235
|
+
# write to it.
|
236
|
+
class DelayedBody
|
237
|
+
# Save the block that will return the body, it won't be
|
238
|
+
# called until the body is needed.
|
239
|
+
def initialize(&block)
|
240
|
+
@block = block
|
241
|
+
end
|
242
|
+
|
243
|
+
# If the body is a String, yield it, otherwise yield each string
|
244
|
+
# returned by calling each on the body.
|
245
|
+
def each
|
246
|
+
v = value
|
247
|
+
if v.is_a?(String)
|
248
|
+
yield v
|
249
|
+
else
|
250
|
+
v.each{|s| yield s}
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Assume that if the body has been set directly that it is
|
255
|
+
# never empty.
|
256
|
+
def empty?
|
257
|
+
false
|
258
|
+
end
|
259
|
+
|
260
|
+
# Return the body as a single string, mostly useful during testing.
|
261
|
+
def join
|
262
|
+
a = []
|
263
|
+
each{|s| a << s}
|
264
|
+
a.join
|
265
|
+
end
|
266
|
+
|
267
|
+
# Calculate the length for the body.
|
268
|
+
def length
|
269
|
+
length = 0
|
270
|
+
each{|s| length += s.bytesize}
|
271
|
+
length
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
# Cache the body returned by the block. This way the block won't
|
277
|
+
# be called multiple times.
|
278
|
+
def value
|
279
|
+
@value ||= @block.call
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
module RequestMethods
|
284
|
+
# Alias for referrer
|
285
|
+
def back
|
286
|
+
referrer
|
287
|
+
end
|
288
|
+
|
289
|
+
# Halt processing and return the error status provided with the given code and
|
290
|
+
# optional body.
|
291
|
+
# If a single argument is given and it is not an integer, consider it the body
|
292
|
+
# and use a 500 status code.
|
293
|
+
def error(code=500, body = nil)
|
294
|
+
unless code.is_a?(Integer)
|
295
|
+
body = code
|
296
|
+
code = 500
|
297
|
+
end
|
298
|
+
|
299
|
+
response.status = code
|
300
|
+
response.body = body if body
|
301
|
+
halt
|
302
|
+
end
|
303
|
+
|
304
|
+
# Halt processing and return a 404 response with an optional body.
|
305
|
+
def not_found(body = nil)
|
306
|
+
error(404, body)
|
307
|
+
end
|
308
|
+
|
309
|
+
# If the absolute_redirects or :prefixed_redirects roda class options has been set, respect those
|
310
|
+
# and update the path.
|
311
|
+
def redirect(path=(no_add_script_name = true; default_redirect_path), status=default_redirect_status)
|
312
|
+
opts = roda_class.opts
|
313
|
+
absolute_redirects = opts[:absolute_redirects]
|
314
|
+
prefixed_redirects = no_add_script_name ? false : opts[:prefixed_redirects]
|
315
|
+
path = uri(path, absolute_redirects, prefixed_redirects) if absolute_redirects || prefixed_redirects
|
316
|
+
super(path, status)
|
317
|
+
end
|
318
|
+
|
319
|
+
# Use the contents of the file at +path+ as the response body. See plugin documentation for options.
|
320
|
+
def send_file(path, opts = OPTS)
|
321
|
+
res = response
|
322
|
+
headers = res.headers
|
323
|
+
if opts[:type] || !headers[CONTENT_TYPE]
|
324
|
+
res.content_type(opts[:type] || ::File.extname(path), :default => OCTET_STREAM)
|
325
|
+
end
|
326
|
+
|
327
|
+
disposition = opts[:disposition]
|
328
|
+
filename = opts[:filename]
|
329
|
+
if disposition || filename
|
330
|
+
disposition ||= ATTACHMENT
|
331
|
+
filename = path if filename.nil?
|
332
|
+
res.attachment(filename, disposition)
|
333
|
+
end
|
334
|
+
|
335
|
+
if lm = opts[:last_modified]
|
336
|
+
last_modified(lm)
|
337
|
+
end
|
338
|
+
|
339
|
+
file = ::Rack::File.new nil
|
340
|
+
file.path = path
|
341
|
+
s, h, b = file.serving(@env)
|
342
|
+
|
343
|
+
res.status = opts[:status] || s
|
344
|
+
headers.delete(CONTENT_LENGTH)
|
345
|
+
headers.replace(h.merge!(headers))
|
346
|
+
res.body = b
|
347
|
+
|
348
|
+
halt
|
349
|
+
rescue Errno::ENOENT
|
350
|
+
not_found
|
351
|
+
end
|
352
|
+
|
353
|
+
# Generates the absolute URI for a given path in the app.
|
354
|
+
# Takes Rack routers and reverse proxies into account.
|
355
|
+
def uri(addr = nil, absolute = true, add_script_name = true)
|
356
|
+
addr = addr.to_s if addr
|
357
|
+
return addr if addr =~ /\A[A-z][A-z0-9\+\.\-]*:/
|
358
|
+
uri = if absolute
|
359
|
+
h = if @env.has_key?(HTTP_X_FORWARDED_HOST) || port != (ssl? ? 443 : 80)
|
360
|
+
host_with_port
|
361
|
+
else
|
362
|
+
host
|
363
|
+
end
|
364
|
+
["http#{'s' if ssl?}://#{h}"]
|
365
|
+
else
|
366
|
+
[EMPTY_STRING]
|
367
|
+
end
|
368
|
+
uri << script_name.to_s if add_script_name
|
369
|
+
uri << (addr || path_info)
|
370
|
+
File.join(uri)
|
371
|
+
end
|
372
|
+
alias url uri
|
373
|
+
alias to uri
|
374
|
+
|
375
|
+
private
|
376
|
+
|
377
|
+
# Use a 303 response for non-GET responses if client uses HTTP 1.1.
|
378
|
+
def default_redirect_status
|
379
|
+
if @env[HTTP_VERSION] == HTTP11 && !is_get?
|
380
|
+
303
|
381
|
+
else
|
382
|
+
super
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
module ResponseMethods
|
388
|
+
# Set or retrieve the response status code.
|
389
|
+
def status(value = (return @status; nil))
|
390
|
+
@status = value
|
391
|
+
end
|
392
|
+
|
393
|
+
# Set or retrieve the response body. When a block is given,
|
394
|
+
# evaluation is deferred until the body is needed.
|
395
|
+
def body(value = (return @body unless block_given?; nil), &block)
|
396
|
+
if block
|
397
|
+
@body = DelayedBody.new(&block)
|
398
|
+
else
|
399
|
+
self.body = value
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Set the body to the given value.
|
404
|
+
def body=(body)
|
405
|
+
@body = DelayedBody.new{body}
|
406
|
+
end
|
407
|
+
|
408
|
+
# If the body is a DelayedBody, set the appropriate length for it.
|
409
|
+
def finish
|
410
|
+
@length = @body.length if @body.is_a?(DelayedBody) && !@headers[CONTENT_LENGTH]
|
411
|
+
super
|
412
|
+
end
|
413
|
+
|
414
|
+
# Set multiple response headers with Hash, or return the headers if no
|
415
|
+
# argument is given.
|
416
|
+
def headers(hash = (return @headers; nil))
|
417
|
+
@headers.merge!(hash)
|
418
|
+
end
|
419
|
+
|
420
|
+
# Look up a media type by file extension in Rack's mime registry.
|
421
|
+
def mime_type(type)
|
422
|
+
roda_class.mime_type(type)
|
423
|
+
end
|
424
|
+
|
425
|
+
# Set the Content-Type of the response body given a media type or file
|
426
|
+
# extension. See plugin documentation for options.
|
427
|
+
def content_type(type = (return @headers[CONTENT_TYPE]; nil), opts = OPTS)
|
428
|
+
unless (mime_type = mime_type(type) || opts[:default])
|
429
|
+
raise RodaError, "Unknown media type: #{type}"
|
430
|
+
end
|
431
|
+
|
432
|
+
unless opts.empty?
|
433
|
+
opts.each do |key, val|
|
434
|
+
next if key == :default || (key == :charset && mime_type.include?(CHARSET))
|
435
|
+
val = val.inspect if val =~ /[";,]/
|
436
|
+
mime_type += "#{mime_type.include?(SEMICOLON) ? COMMA : SEMICOLON}#{key}=#{val}"
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
@headers[CONTENT_TYPE] = mime_type
|
441
|
+
end
|
442
|
+
|
443
|
+
# Set the Content-Disposition to "attachment" with the specified filename,
|
444
|
+
# instructing the user agents to prompt to save.
|
445
|
+
def attachment(filename = nil, disposition=ATTACHMENT)
|
446
|
+
if filename
|
447
|
+
params = "; filename=#{File.basename(filename).inspect}"
|
448
|
+
unless @headers[CONTENT_TYPE]
|
449
|
+
ext = File.extname(filename)
|
450
|
+
unless ext.empty?
|
451
|
+
content_type(ext)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
@headers[CONTENT_DISPOSITION] = "#{disposition}#{params}"
|
456
|
+
end
|
457
|
+
|
458
|
+
# Whether or not the status is set to 1xx. Returns nil if status not yet set.
|
459
|
+
def informational?
|
460
|
+
@status.between?(100, 199) if @status
|
461
|
+
end
|
462
|
+
|
463
|
+
# Whether or not the status is set to 2xx. Returns nil if status not yet set.
|
464
|
+
def success?
|
465
|
+
@status.between?(200, 299) if @status
|
466
|
+
end
|
467
|
+
|
468
|
+
# Whether or not the status is set to 3xx. Returns nil if status not yet set.
|
469
|
+
def redirect?
|
470
|
+
@status.between?(300, 399) if @status
|
471
|
+
end
|
472
|
+
|
473
|
+
# Whether or not the status is set to 4xx. Returns nil if status not yet set.
|
474
|
+
def client_error?
|
475
|
+
@status.between?(400, 499) if @status
|
476
|
+
end
|
477
|
+
|
478
|
+
# Whether or not the status is set to 5xx. Returns nil if status not yet set.
|
479
|
+
def server_error?
|
480
|
+
@status.between?(500, 599) if @status
|
481
|
+
end
|
482
|
+
|
483
|
+
# Whether or not the status is set to 404. Returns nil if status not yet set.
|
484
|
+
def not_found?
|
485
|
+
@status == 404 if @status
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
module ClassMethods
|
490
|
+
# If a type and value are given, set the value in Rack's MIME registry.
|
491
|
+
# If only a type is given, lookup the type in Rack's MIME registry and
|
492
|
+
# return it.
|
493
|
+
def mime_type(type=(return; nil), value = nil)
|
494
|
+
return type.to_s if type.to_s.include?(SLASH)
|
495
|
+
type = ".#{type}" unless type.to_s[0] == ?.
|
496
|
+
if value
|
497
|
+
Rack::Mime::MIME_TYPES[type] = value
|
498
|
+
else
|
499
|
+
Rack::Mime.mime_type(type, nil)
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
module DelegateMethods
|
505
|
+
[:logger, :back].each do |meth|
|
506
|
+
define_method(meth){@_request.send(meth)}
|
507
|
+
end
|
508
|
+
[:redirect, :uri, :url, :to, :send_file, :error, :not_found].each do |meth|
|
509
|
+
define_method(meth){|*v, &block| @_request.send(meth, *v, &block)}
|
510
|
+
end
|
511
|
+
|
512
|
+
[:informational?, :success?, :redirect?, :client_error?, :server_error?, :not_found?].each do |meth|
|
513
|
+
define_method(meth){@_response.send(meth)}
|
514
|
+
end
|
515
|
+
[:status, :body, :headers, :mime_type, :content_type, :attachment].each do |meth|
|
516
|
+
define_method(meth){|*v, &block| @_response.send(meth, *v, &block)}
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
register_plugin(:sinatra_helpers, SinatraHelpers)
|
522
|
+
end
|
523
|
+
end
|