hanami-controller 2.0.0.beta1 → 2.0.0.rc1

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,15 +3,23 @@
3
3
  require "rack"
4
4
  require "rack/response"
5
5
  require "hanami/utils/kernel"
6
- require "hanami/action/flash"
7
6
  require "hanami/action/halt"
8
7
  require "hanami/action/cookie_jar"
9
8
  require "hanami/action/cache/cache_control"
10
9
  require "hanami/action/cache/expires"
11
10
  require "hanami/action/cache/conditional_get"
11
+ require_relative "errors"
12
12
 
13
13
  module Hanami
14
14
  class Action
15
+ # The HTTP response for an action, given to {Action#handle}.
16
+ #
17
+ # Inherits from `Rack::Response`, providing compatibility with Rack functionality.
18
+ #
19
+ # @see http://www.rubydoc.info/gems/rack/Rack/Response
20
+ #
21
+ # @since 2.0.0
22
+ # @api private
15
23
  class Response < ::Rack::Response
16
24
  # @since 2.0.0
17
25
  # @api private
@@ -27,7 +35,7 @@ module Hanami
27
35
 
28
36
  # @since 2.0.0
29
37
  # @api private
30
- attr_reader :request, :action, :exposures, :format, :env, :view_options
38
+ attr_reader :request, :exposures, :env, :view_options
31
39
 
32
40
  # @since 2.0.0
33
41
  # @api private
@@ -36,7 +44,7 @@ module Hanami
36
44
  # @since 2.0.0
37
45
  # @api private
38
46
  def self.build(status, env)
39
- new(action: "", configuration: nil, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r| # rubocop:disable Layout/LineLength
47
+ new(config: nil, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r|
40
48
  r.status = status
41
49
  r.body = Http::Status.message_for(status)
42
50
  r.set_format(Mime.format_for(r.content_type))
@@ -45,21 +53,25 @@ module Hanami
45
53
 
46
54
  # @since 2.0.0
47
55
  # @api private
48
- def initialize(request:, action:, configuration:, content_type: nil, env: {}, headers: {}, view_options: nil) # rubocop:disable Metrics/ParameterLists
56
+ def initialize(request:, config:, content_type: nil, env: {}, headers: {}, view_options: nil, sessions_enabled: false) # rubocop:disable Layout/LineLength, Metrics/ParameterLists
49
57
  super([], 200, headers.dup)
50
- set_header(Action::CONTENT_TYPE, content_type)
58
+ self.content_type = content_type if content_type
51
59
 
52
60
  @request = request
53
- @action = action
54
- @configuration = configuration
61
+ @config = config
55
62
  @charset = ::Rack::MediaType.params(content_type).fetch("charset", nil)
56
63
  @exposures = {}
57
64
  @env = env
58
65
  @view_options = view_options || DEFAULT_VIEW_OPTIONS
59
66
 
67
+ @sessions_enabled = sessions_enabled
60
68
  @sending_file = false
61
69
  end
62
70
 
71
+ # Sets the response body.
72
+ #
73
+ # @param str [String] the body string
74
+ #
63
75
  # @since 2.0.0
64
76
  # @api public
65
77
  def body=(str)
@@ -74,50 +86,141 @@ module Hanami
74
86
  end
75
87
  end
76
88
 
77
- # @since 2.0.0
78
- # @api public
89
+ # This is NOT RELEASED with 2.0.0
90
+ #
91
+ # @api private
79
92
  def render(view, **options)
80
93
  self.body = view.(**view_options.(request, self), **exposures.merge(options)).to_str
81
94
  end
82
95
 
96
+ # Returns the format for the response.
97
+ #
98
+ # Returns nil if a format has not been assigned and also cannot be determined from the
99
+ # response's `#content_type`.
100
+ #
101
+ # @example
102
+ # response.format # => :json
103
+ #
104
+ # @return [Symbol, nil]
105
+ #
106
+ # @since 2.0.0
107
+ # @api public
108
+ def format
109
+ @format ||= Mime.detect_format(content_type, @config)
110
+ end
111
+
112
+ # Sets the format and associated content type for the response.
113
+ #
114
+ # Either a format name (`:json`) or a content type string (`"application/json"`) may be given.
115
+ # In either case, the format or content type will be derived from the given value, and both
116
+ # will be set.
117
+ #
118
+ # @example Assigning via a format name symbol
119
+ # response.format = :json
120
+ # response.content_type # => "application/json"
121
+ # response.headers["Content-Type"] # => "application/json"
122
+ #
123
+ # @example Assigning via a content type string
124
+ # response.format = "application/json"
125
+ # response.format # => :json
126
+ # response.content_type # => "application/json"
127
+ #
128
+ # @param value [Symbol, String] the format name or content type
129
+ #
130
+ # @see Config#formats
131
+ #
83
132
  # @since 2.0.0
84
133
  # @api public
85
- def format=(args)
86
- @format, content_type = *args
87
- content_type = Action::Mime.content_type_with_charset(content_type, charset)
88
- set_header("Content-Type", content_type)
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
89
140
  end
90
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
+ #
91
150
  # @since 2.0.0
92
151
  # @api public
93
152
  def [](key)
94
153
  @exposures.fetch(key)
95
154
  end
96
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
+ #
97
163
  # @since 2.0.0
98
164
  # @api public
99
165
  def []=(key, value)
100
166
  @exposures[key] = value
101
167
  end
102
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
+ #
103
179
  # @since 2.0.0
104
180
  # @api public
105
181
  def session
106
- env[Action::RACK_SESSION] ||= {}
182
+ unless @sessions_enabled
183
+ raise Hanami::Action::MissingSessionError.new("Hanami::Action::Response#session")
184
+ end
185
+
186
+ request.session
107
187
  end
108
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
+ #
109
199
  # @since 2.0.0
110
200
  # @api public
111
- def cookies
112
- @cookies ||= CookieJar.new(env.dup, headers, @configuration.cookies)
201
+ def flash
202
+ unless @sessions_enabled
203
+ raise Hanami::Action::MissingSessionError.new("Hanami::Action::Response#flash")
204
+ end
205
+
206
+ request.flash
113
207
  end
114
208
 
209
+ # Returns the set of cookies to be included in the response.
210
+ #
211
+ # @return [CookieJar]
212
+ #
115
213
  # @since 2.0.0
116
214
  # @api public
117
- def flash
118
- @flash ||= Flash.new(session[Flash::KEY])
215
+ def cookies
216
+ @cookies ||= CookieJar.new(env.dup, headers, @config.cookies)
119
217
  end
120
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
+ #
121
224
  # @since 2.0.0
122
225
  # @api public
123
226
  def redirect_to(url, status: 302)
@@ -127,19 +230,43 @@ module Hanami
127
230
  Halt.call(status)
128
231
  end
129
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
+ #
130
249
  # @since 2.0.0
131
250
  # @api public
132
251
  def send_file(path)
133
252
  _send_file(
134
- Rack::File.new(path, @configuration.public_directory).call(env)
253
+ Rack::File.new(path, @config.public_directory).call(env)
135
254
  )
136
255
  end
137
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
+ #
138
265
  # @since 2.0.0
139
266
  # @api public
140
267
  def unsafe_send_file(path)
141
268
  directory = if Pathname.new(path).relative?
142
- @configuration.root_directory
269
+ @config.root_directory
143
270
  else
144
271
  FILE_SYSTEM_ROOT
145
272
  end
@@ -149,6 +276,37 @@ module Hanami
149
276
  )
150
277
  end
151
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
+ #
152
310
  # @since 2.0.0
153
311
  # @api public
154
312
  def cache_control(*values)
@@ -156,6 +314,28 @@ module Hanami
156
314
  headers.merge!(directives.headers)
157
315
  end
158
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
+ #
159
339
  # @since 2.0.0
160
340
  # @api public
161
341
  def expires(amount, *values)
@@ -163,6 +343,26 @@ module Hanami
163
343
  headers.merge!(directives.headers)
164
344
  end
165
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
+ #
166
366
  # @since 2.0.0
167
367
  # @api public
168
368
  def fresh(options)
@@ -177,15 +377,6 @@ module Hanami
177
377
 
178
378
  # @since 2.0.0
179
379
  # @api private
180
- def request_id
181
- env.fetch(Action::REQUEST_ID) do
182
- # FIXME: raise a meaningful error, by inviting devs to include Hanami::Action::Session
183
- raise "Can't find request ID"
184
- end
185
- end
186
-
187
- # @since 2.0.0
188
- # @api public
189
380
  def set_format(value) # rubocop:disable Naming/AccessorMethodName
190
381
  @format = value
191
382
  end
@@ -211,7 +402,7 @@ module Hanami
211
402
  alias_method :to_ary, :to_a
212
403
 
213
404
  # @since 2.0.0
214
- # @api public
405
+ # @api private
215
406
  def head?
216
407
  env[Action::REQUEST_METHOD] == Action::HEAD
217
408
  end
@@ -4,12 +4,17 @@ require "hanami/action/flash"
4
4
 
5
5
  module Hanami
6
6
  class Action
7
- # Session API
7
+ # Session support for actions.
8
8
  #
9
- # This module isn't included by default.
9
+ # Not included by default; you should include this module manually to enable session support.
10
+ # For actions within an Hanami app, this module will be included automatically if sessions are
11
+ # configured in the app config.
10
12
  #
13
+ # @api public
11
14
  # @since 0.1.0
12
15
  module Session
16
+ # @api private
17
+ # @since 0.1.0
13
18
  def self.included(base)
14
19
  base.class_eval do
15
20
  before { |req, _| req.id }
@@ -18,6 +23,10 @@ module Hanami
18
23
 
19
24
  private
20
25
 
26
+ def sessions_enabled?
27
+ true
28
+ end
29
+
21
30
  # Finalize the response
22
31
  #
23
32
  # @return [void]
@@ -4,6 +4,12 @@ require "hanami/action/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))