roda 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +70 -0
  3. data/README.rdoc +261 -302
  4. data/Rakefile +1 -1
  5. data/doc/release_notes/1.2.0.txt +406 -0
  6. data/lib/roda.rb +206 -124
  7. data/lib/roda/plugins/all_verbs.rb +11 -10
  8. data/lib/roda/plugins/assets.rb +5 -5
  9. data/lib/roda/plugins/backtracking_array.rb +12 -5
  10. data/lib/roda/plugins/caching.rb +10 -8
  11. data/lib/roda/plugins/class_level_routing.rb +94 -0
  12. data/lib/roda/plugins/content_for.rb +6 -0
  13. data/lib/roda/plugins/default_headers.rb +4 -11
  14. data/lib/roda/plugins/delay_build.rb +42 -0
  15. data/lib/roda/plugins/delegate.rb +64 -0
  16. data/lib/roda/plugins/drop_body.rb +33 -0
  17. data/lib/roda/plugins/empty_root.rb +48 -0
  18. data/lib/roda/plugins/environments.rb +68 -0
  19. data/lib/roda/plugins/error_email.rb +1 -2
  20. data/lib/roda/plugins/error_handler.rb +1 -1
  21. data/lib/roda/plugins/halt.rb +7 -5
  22. data/lib/roda/plugins/head.rb +4 -2
  23. data/lib/roda/plugins/header_matchers.rb +17 -9
  24. data/lib/roda/plugins/hooks.rb +16 -32
  25. data/lib/roda/plugins/json.rb +4 -10
  26. data/lib/roda/plugins/mailer.rb +233 -0
  27. data/lib/roda/plugins/match_affix.rb +48 -0
  28. data/lib/roda/plugins/multi_route.rb +9 -11
  29. data/lib/roda/plugins/multi_run.rb +81 -0
  30. data/lib/roda/plugins/named_templates.rb +93 -0
  31. data/lib/roda/plugins/not_allowed.rb +43 -48
  32. data/lib/roda/plugins/path.rb +63 -2
  33. data/lib/roda/plugins/render.rb +79 -48
  34. data/lib/roda/plugins/render_each.rb +6 -0
  35. data/lib/roda/plugins/sinatra_helpers.rb +523 -0
  36. data/lib/roda/plugins/slash_path_empty.rb +25 -0
  37. data/lib/roda/plugins/static_path_info.rb +64 -0
  38. data/lib/roda/plugins/streaming.rb +1 -1
  39. data/lib/roda/plugins/view_subdirs.rb +12 -8
  40. data/lib/roda/version.rb +1 -1
  41. data/spec/integration_spec.rb +33 -0
  42. data/spec/plugin/backtracking_array_spec.rb +24 -18
  43. data/spec/plugin/class_level_routing_spec.rb +138 -0
  44. data/spec/plugin/delay_build_spec.rb +23 -0
  45. data/spec/plugin/delegate_spec.rb +20 -0
  46. data/spec/plugin/drop_body_spec.rb +20 -0
  47. data/spec/plugin/empty_root_spec.rb +14 -0
  48. data/spec/plugin/environments_spec.rb +31 -0
  49. data/spec/plugin/h_spec.rb +1 -3
  50. data/spec/plugin/header_matchers_spec.rb +14 -0
  51. data/spec/plugin/hooks_spec.rb +3 -5
  52. data/spec/plugin/mailer_spec.rb +191 -0
  53. data/spec/plugin/match_affix_spec.rb +22 -0
  54. data/spec/plugin/multi_run_spec.rb +31 -0
  55. data/spec/plugin/named_templates_spec.rb +65 -0
  56. data/spec/plugin/path_spec.rb +66 -2
  57. data/spec/plugin/render_spec.rb +46 -1
  58. data/spec/plugin/sinatra_helpers_spec.rb +534 -0
  59. data/spec/plugin/slash_path_empty_spec.rb +22 -0
  60. data/spec/plugin/static_path_info_spec.rb +50 -0
  61. data/spec/request_spec.rb +23 -0
  62. data/spec/response_spec.rb +12 -1
  63. metadata +48 -6
@@ -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 default template cache.
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
- # <%== %> not escape output, and handles postfix conditions inside
32
- # <%= %> tags.
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] ||= Encoding.default_external
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, true)
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] = render_opts.dup
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
- if template.is_a?(Hash)
125
- if opts.empty?
126
- opts = template
127
- else
128
- opts = opts.merge(template)
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
- if template.is_a?(Hash)
161
- if opts.empty?
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
- content = opts[:content] || render(template, opts)
169
-
170
- if layout = opts.fetch(:layout, render_opts[:layout])
171
- if layout_opts = opts[:layout_opts]
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||OPTS){content}
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(path, &block)
176
+ def cached_template(opts, &block)
186
177
  if cache = render_opts[:cache]
187
- unless template = cache[path]
188
- template = cache[path] = yield
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(template, opts)
228
+ def template_path(opts)
198
229
  render_opts = render_opts()
199
- "#{opts[:views] || render_opts[:views]}/#{template}.#{opts[:ext] || render_opts[:ext] || render_opts[:engine]}"
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