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.
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