hanami-controller 2.0.0.beta4 → 2.0.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.
@@ -3,14 +3,18 @@
3
3
  require "rack"
4
4
  require "rack/response"
5
5
  require "hanami/utils/kernel"
6
- require "hanami/action/halt"
7
- require "hanami/action/cookie_jar"
8
- require "hanami/action/cache/cache_control"
9
- require "hanami/action/cache/expires"
10
- require "hanami/action/cache/conditional_get"
6
+ require_relative "errors"
11
7
 
12
8
  module Hanami
13
9
  class Action
10
+ # The HTTP response for an action, given to {Action#handle}.
11
+ #
12
+ # Inherits from `Rack::Response`, providing compatibility with Rack functionality.
13
+ #
14
+ # @see http://www.rubydoc.info/gems/rack/Rack/Response
15
+ #
16
+ # @since 2.0.0
17
+ # @api private
14
18
  class Response < ::Rack::Response
15
19
  # @since 2.0.0
16
20
  # @api private
@@ -26,7 +30,7 @@ module Hanami
26
30
 
27
31
  # @since 2.0.0
28
32
  # @api private
29
- attr_reader :request, :exposures, :format, :env, :view_options
33
+ attr_reader :request, :exposures, :env, :view_options
30
34
 
31
35
  # @since 2.0.0
32
36
  # @api private
@@ -35,10 +39,10 @@ module Hanami
35
39
  # @since 2.0.0
36
40
  # @api private
37
41
  def self.build(status, env)
38
- new(config: nil, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r|
42
+ new(config: Action.config.dup, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r|
39
43
  r.status = status
40
44
  r.body = Http::Status.message_for(status)
41
- r.set_format(Mime.format_for(r.content_type))
45
+ r.set_format(Mime.detect_format(r.content_type), config)
42
46
  end
43
47
  end
44
48
 
@@ -46,7 +50,7 @@ module Hanami
46
50
  # @api private
47
51
  def initialize(request:, config:, content_type: nil, env: {}, headers: {}, view_options: nil, sessions_enabled: false) # rubocop:disable Layout/LineLength, Metrics/ParameterLists
48
52
  super([], 200, headers.dup)
49
- set_header(Action::CONTENT_TYPE, content_type)
53
+ self.content_type = content_type if content_type
50
54
 
51
55
  @request = request
52
56
  @config = config
@@ -59,6 +63,10 @@ module Hanami
59
63
  @sending_file = false
60
64
  end
61
65
 
66
+ # Sets the response body.
67
+ #
68
+ # @param str [String] the body string
69
+ #
62
70
  # @since 2.0.0
63
71
  # @api public
64
72
  def body=(str)
@@ -73,32 +81,101 @@ module Hanami
73
81
  end
74
82
  end
75
83
 
76
- # @since 2.0.0
77
- # @api public
84
+ # This is NOT RELEASED with 2.0.0
85
+ #
86
+ # @api private
78
87
  def render(view, **options)
79
88
  self.body = view.(**view_options.(request, self), **exposures.merge(options)).to_str
80
89
  end
81
90
 
91
+ # Returns the format for the response.
92
+ #
93
+ # Returns nil if a format has not been assigned and also cannot be determined from the
94
+ # response's `#content_type`.
95
+ #
96
+ # @example
97
+ # response.format # => :json
98
+ #
99
+ # @return [Symbol, nil]
100
+ #
82
101
  # @since 2.0.0
83
102
  # @api public
84
- def format=(args)
85
- @format, content_type = *args
86
- content_type = Action::Mime.content_type_with_charset(content_type, charset)
87
- set_header("Content-Type", content_type)
103
+ def format
104
+ @format ||= Mime.detect_format(content_type, @config)
88
105
  end
89
106
 
107
+ # Sets the format and associated content type for the response.
108
+ #
109
+ # Either a format name (`:json`) or a MIME type (`"application/json"`) may be given. In either
110
+ # case, the format or content type will be derived from the given value, and both will be set.
111
+ #
112
+ # Providing an unknown format name will raise an {Hanami::Action::UnknownFormatError}.
113
+ #
114
+ # Providing an unknown MIME type will set the content type and set the format as nil.
115
+ #
116
+ # @example Assigning via a format name symbol
117
+ # response.format = :json
118
+ # response.content_type # => "application/json"
119
+ # response.headers["Content-Type"] # => "application/json"
120
+ #
121
+ # @example Assigning via a content type string
122
+ # response.format = "application/json"
123
+ # response.format # => :json
124
+ # response.content_type # => "application/json"
125
+ #
126
+ # @param value [Symbol, String] the format name or content type
127
+ #
128
+ # @raise [Hanami::Action::UnknownFormatError] if an unknown format name is given
129
+ #
130
+ # @see Config#formats
131
+ #
132
+ # @since 2.0.0
133
+ # @api public
134
+ def format=(value)
135
+ format, content_type = Mime.detect_format_and_content_type(value, @config)
136
+
137
+ self.content_type = Mime.content_type_with_charset(content_type, charset)
138
+
139
+ @format = format
140
+ end
141
+
142
+ # Returns the exposure value for the given key.
143
+ #
144
+ # @param key [Object]
145
+ #
146
+ # @return [Object] the exposure value, if found
147
+ #
148
+ # @raise [KeyError] if the exposure was not found
149
+ #
90
150
  # @since 2.0.0
91
151
  # @api public
92
152
  def [](key)
93
153
  @exposures.fetch(key)
94
154
  end
95
155
 
156
+ # Sets an exposure value for the given key.
157
+ #
158
+ # @param key [Object]
159
+ # @param value [Object]
160
+ #
161
+ # @return [Object] the value
162
+ #
96
163
  # @since 2.0.0
97
164
  # @api public
98
165
  def []=(key, value)
99
166
  @exposures[key] = value
100
167
  end
101
168
 
169
+ # Returns the session for the response.
170
+ #
171
+ # This is the same session object as the {Request}.
172
+ #
173
+ # @return [Hash] the session object
174
+ #
175
+ # @raise [MissingSessionError] if sessions are not enabled
176
+ #
177
+ # @see Request#session
178
+ #
102
179
  # @since 2.0.0
103
180
  # @api public
104
181
  def session
@@ -109,6 +186,16 @@ module Hanami
109
186
  request.session
110
187
  end
111
188
 
189
+ # Returns the flash for the request.
190
+ #
191
+ # This is the same flash object as the {Request}.
192
+ #
193
+ # @return [Flash]
194
+ #
195
+ # @raise [MissingSessionError] if sessions are not enabled
196
+ #
197
+ # @see Request#flash
198
+ #
112
199
  # @since 2.0.0
113
200
  # @api public
114
201
  def flash
@@ -119,12 +206,21 @@ module Hanami
119
206
  request.flash
120
207
  end
121
208
 
209
+ # Returns the set of cookies to be included in the response.
210
+ #
211
+ # @return [CookieJar]
212
+ #
122
213
  # @since 2.0.0
123
214
  # @api public
124
215
  def cookies
125
216
  @cookies ||= CookieJar.new(env.dup, headers, @config.cookies)
126
217
  end
127
218
 
219
+ # Sets the response to redirect to the given URL and halts further handling.
220
+ #
221
+ # @param url [String]
222
+ # @param status [Integer] the HTTP status to use for the redirect
223
+ #
128
224
  # @since 2.0.0
129
225
  # @api public
130
226
  def redirect_to(url, status: 302)
@@ -134,6 +230,22 @@ module Hanami
134
230
  Halt.call(status)
135
231
  end
136
232
 
233
+ # Sends the file at the given path as the response, for any file within the configured
234
+ # `public_directory`.
235
+ #
236
+ # Handles the following aspects for file responses:
237
+ #
238
+ # - Setting `Content-Type` and `Content-Length` headers
239
+ # - File Not Found responses (returns a 404)
240
+ # - Conditional GET (via `If-Modified-Since` header)
241
+ # - Range requests (via `Range` header)
242
+ #
243
+ # @param path [String] the file path
244
+ #
245
+ # @return [void]
246
+ #
247
+ # @see Config#public_directory
248
+ #
137
249
  # @since 2.0.0
138
250
  # @api public
139
251
  def send_file(path)
@@ -142,6 +254,14 @@ module Hanami
142
254
  )
143
255
  end
144
256
 
257
+ # Send the file at the given path as the response, for a file anywhere in the file system.
258
+ #
259
+ # @see #send_file
260
+ #
261
+ # @param path [String, Pathname] path to the file to be sent
262
+ #
263
+ # @return [void]
264
+ #
145
265
  # @since 2.0.0
146
266
  # @api public
147
267
  def unsafe_send_file(path)
@@ -156,6 +276,37 @@ module Hanami
156
276
  )
157
277
  end
158
278
 
279
+ # Specifies the response freshness policy for HTTP caches using the `Cache-Control` header.
280
+ #
281
+ # Any number of non-value directives (`:public`, `:private`, `:no_cache`, `:no_store`,
282
+ # `:must_revalidate`, `:proxy_revalidate`) may be passed along with a Hash of value directives
283
+ # (`:max_age`, `:min_stale`, `:s_max_age`).
284
+ #
285
+ # See [RFC 2616 / 14.9](http://tools.ietf.org/html/rfc2616#section-14.9.1) for more on
286
+ # standard cache control directives.
287
+ #
288
+ # @example
289
+ # # Set Cache-Control directives
290
+ # response.cache_control :public, max_age: 900, s_maxage: 86400
291
+ #
292
+ # # Overwrite previous Cache-Control directives
293
+ # response.cache_control :private, :no_cache, :no_store
294
+ #
295
+ # response.get_header("Cache-Control") # => "private, no-store, max-age=900"
296
+ #
297
+ # @param values [Array<Symbol, Hash>] values to map to `Cache-Control` directives
298
+ # @option values [Symbol] :public
299
+ # @option values [Symbol] :private
300
+ # @option values [Symbol] :no_cache
301
+ # @option values [Symbol] :no_store
302
+ # @option values [Symbol] :must_validate
303
+ # @option values [Symbol] :proxy_revalidate
304
+ # @option values [Hash] :max_age
305
+ # @option values [Hash] :min_stale
306
+ # @option values [Hash] :s_max_age
307
+ #
308
+ # @return void
309
+ #
159
310
  # @since 2.0.0
160
311
  # @api public
161
312
  def cache_control(*values)
@@ -163,6 +314,28 @@ module Hanami
163
314
  headers.merge!(directives.headers)
164
315
  end
165
316
 
317
+ # Sets the `Expires` header and `Cache-Control`/`max-age` directive for the response.
318
+ #
319
+ # You can provide an integer number of seconds in the future, or a Time object indicating when
320
+ # the response should be considered "stale". The remaining arguments are passed to
321
+ # {#cache_control}.
322
+ #
323
+ # @example
324
+ # # Set Cache-Control directives and Expires
325
+ # response.expires 900, :public
326
+ #
327
+ # # Overwrite Cache-Control directives and Expires
328
+ # response.expires 300, :private, :no_cache, :no_store
329
+ #
330
+ # response.get_header("Expires") # => "Thu, 26 Jun 2014 12:00:00 GMT"
331
+ # response.get_header("Cache-Control") # => "private, no-cache, no-store max-age=300"
332
+ #
333
+ # @param amount [Integer, Time] number of seconds or point in time
334
+ # @param values [Array<Symbols>] values to map to `Cache-Control` directives via
335
+ # {#cache_control}
336
+ #
337
+ # @return void
338
+ #
166
339
  # @since 2.0.0
167
340
  # @api public
168
341
  def expires(amount, *values)
@@ -170,6 +343,26 @@ module Hanami
170
343
  headers.merge!(directives.headers)
171
344
  end
172
345
 
346
+ # Sets the `etag` and/or `last_modified` headers on the response and halts with a `304 Not
347
+ # Modified` response if the request is still fresh according to the `IfNoneMatch` and
348
+ # `IfModifiedSince` request headers.
349
+ #
350
+ # @example
351
+ # # Set etag header and halt 304 if request matches IF_NONE_MATCH header
352
+ # response.fresh etag: some_resource.updated_at.to_i
353
+ #
354
+ # # Set last_modified header and halt 304 if request matches IF_MODIFIED_SINCE
355
+ # response.fresh last_modified: some_resource.updated_at
356
+ #
357
+ # # Set etag and last_modified header and halt 304 if request matches IF_MODIFIED_SINCE and IF_NONE_MATCH
358
+ # response.fresh last_modified: some_resource.updated_at
359
+ #
360
+ # @param options [Hash]
361
+ # @option options [Integer] :etag for testing IfNoneMatch conditions
362
+ # @option options [Date] :last_modified for testing IfModifiedSince conditions
363
+ #
364
+ # @return void
365
+ #
173
366
  # @since 2.0.0
174
367
  # @api public
175
368
  def fresh(options)
@@ -183,7 +376,7 @@ module Hanami
183
376
  end
184
377
 
185
378
  # @since 2.0.0
186
- # @api public
379
+ # @api private
187
380
  def set_format(value) # rubocop:disable Naming/AccessorMethodName
188
381
  @format = value
189
382
  end
@@ -209,7 +402,7 @@ module Hanami
209
402
  alias_method :to_ary, :to_a
210
403
 
211
404
  # @since 2.0.0
212
- # @api public
405
+ # @api private
213
406
  def head?
214
407
  env[Action::REQUEST_METHOD] == Action::HEAD
215
408
  end
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/action/flash"
4
-
5
3
  module Hanami
6
4
  class Action
7
- # Session API
5
+ # Session support for actions.
8
6
  #
9
- # This module isn't included by default.
7
+ # Not included by default; you should include this module manually to enable session support.
8
+ # For actions within an Hanami app, this module will be included automatically if sessions are
9
+ # configured in the app config.
10
10
  #
11
+ # @api public
11
12
  # @since 0.1.0
12
13
  module Session
14
+ # @api private
15
+ # @since 0.1.0
13
16
  def self.included(base)
14
17
  base.class_eval do
15
18
  before { |req, _| req.id }
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/action/params"
3
+ require_relative "params"
4
4
 
5
5
  module Hanami
6
6
  class Action
7
+ # Support for validating params when calling actions.
8
+ #
9
+ # Included only when hanami-validations (and its dependencies) are bundled.
10
+ #
11
+ # @api private
12
+ # @since 0.1.0
7
13
  module Validatable
8
14
  # Defines the class name for anonymous params
9
15
  #
@@ -34,13 +40,10 @@ module Hanami
34
40
  # Once whitelisted, the params are available as an Hash with symbols
35
41
  # as keys.
36
42
  #
37
- #
38
- #
39
43
  # It accepts an anonymous block where all the params can be listed.
40
44
  # It internally creates an inner class which inherits from
41
45
  # Hanami::Action::Params.
42
46
  #
43
- #
44
47
  # Alternatively, it accepts an concrete class that should inherit from
45
48
  # Hanami::Action::Params.
46
49
  #
@@ -49,8 +52,6 @@ module Hanami
49
52
  #
50
53
  # @return void
51
54
  #
52
- # @since 0.3.0
53
- #
54
55
  # @see Hanami::Action::Params
55
56
  # @see https://guides.hanamirb.org//validations/overview
56
57
  #
@@ -93,6 +94,9 @@ module Hanami
93
94
  # req.params[:admin] # => nil
94
95
  # end
95
96
  # end
97
+ #
98
+ # @since 0.3.0
99
+ # @api public
96
100
  def params(klass = nil, &blk)
97
101
  if klass.nil?
98
102
  klass = const_set(PARAMS_CLASS_NAME, Class.new(Params))