roda 1.1.0 → 1.2.0
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 +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
|