hanami-controller 2.0.0.alpha1 → 2.0.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,6 +7,13 @@ module Hanami
7
7
  module Mime
8
8
  DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
9
9
  DEFAULT_CHARSET = 'utf-8'.freeze
10
+
11
+ # The key that returns content mime type from the Rack env
12
+ #
13
+ # @since 2.0.0
14
+ # @api private
15
+ HTTP_CONTENT_TYPE = 'CONTENT_TYPE'.freeze
16
+
10
17
  # The header key to set the mime type of the response
11
18
  #
12
19
  # @since 0.1.0
@@ -157,7 +164,7 @@ module Hanami
157
164
  #
158
165
  # @return [TrueClass, FalseClass]
159
166
  def self.accepted_mime_type?(request, accepted_mime_types, configuration)
160
- mime_type = request.env[CONTENT_TYPE] || default_content_type(configuration) || DEFAULT_CONTENT_TYPE
167
+ mime_type = request.env[HTTP_CONTENT_TYPE] || default_content_type(configuration) || DEFAULT_CONTENT_TYPE
161
168
 
162
169
  !accepted_mime_types.find { |mt| ::Rack::Mime.match?(mt, mime_type) }.nil?
163
170
  end
@@ -126,7 +126,7 @@ module Hanami
126
126
  #
127
127
  # @since 0.7.0
128
128
  #
129
- # @see http://hanamirb.org/guides/validations/overview/
129
+ # @see https://guides.hanamirb.org/validations/overview
130
130
  #
131
131
  # @example
132
132
  # class Signup
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rack'
2
4
  require 'rack/response'
3
5
  require 'hanami/utils/kernel'
@@ -11,44 +13,52 @@ require 'hanami/action/cache/conditional_get'
11
13
  module Hanami
12
14
  class Action
13
15
  class Response < ::Rack::Response
14
- REQUEST_METHOD = "REQUEST_METHOD".freeze
15
- HTTP_ACCEPT = "HTTP_ACCEPT".freeze
16
- SESSION_KEY = "rack.session".freeze
17
- REQUEST_ID = "hanami.request_id".freeze
18
- LOCATION = "Location".freeze
19
-
20
- X_CASCADE = "X-Cascade".freeze
21
- CONTENT_LENGTH = "Content-Length".freeze
16
+ DEFAULT_VIEW_OPTIONS = -> * { {} }.freeze
17
+
18
+ REQUEST_METHOD = "REQUEST_METHOD"
19
+ HTTP_ACCEPT = "HTTP_ACCEPT"
20
+ SESSION_KEY = "rack.session"
21
+ REQUEST_ID = "hanami.request_id"
22
+ LOCATION = "Location"
23
+
24
+ X_CASCADE = "X-Cascade"
25
+ CONTENT_LENGTH = "Content-Length"
22
26
  NOT_FOUND = 404
23
27
 
24
28
  RACK_STATUS = 0
25
29
  RACK_HEADERS = 1
26
30
  RACK_BODY = 2
27
31
 
28
- HEAD = "HEAD".freeze
32
+ HEAD = "HEAD"
33
+
34
+ FLASH_SESSION_KEY = "_flash"
29
35
 
30
36
  EMPTY_BODY = [].freeze
31
37
 
32
- attr_reader :action, :exposures, :format, :env
38
+ FILE_SYSTEM_ROOT = Pathname.new("/").freeze
39
+
40
+ attr_reader :request, :action, :exposures, :format, :env, :view_options
33
41
  attr_accessor :charset
34
42
 
35
- def self.build(response, env)
36
- new(action: "", configuration: nil, content_type: Mime.best_q_match(env[HTTP_ACCEPT]), env: env, header: response[RACK_HEADERS]).tap do |r|
37
- r.status = response[RACK_STATUS]
38
- r.body = response[RACK_BODY].first
43
+ def self.build(status, env)
44
+ new(action: "", configuration: nil, content_type: Mime.best_q_match(env[HTTP_ACCEPT]), env: env).tap do |r|
45
+ r.status = status
46
+ r.body = Http::Status.message_for(status)
39
47
  r.set_format(Mime.format_for(r.content_type))
40
48
  end
41
49
  end
42
50
 
43
- def initialize(action:, configuration:, content_type: nil, env: {}, header: {})
44
- super([], 200, header.dup)
51
+ def initialize(request:, action:, configuration:, content_type: nil, env: {}, headers: {}, view_options: nil)
52
+ super([], 200, headers.dup)
45
53
  set_header("Content-Type", content_type)
46
54
 
47
- @action = action
55
+ @request = request
56
+ @action = action
48
57
  @configuration = configuration
49
- @charset = ::Rack::MediaType.params(content_type).fetch('charset', nil)
58
+ @charset = ::Rack::MediaType.params(content_type).fetch('charset', nil)
50
59
  @exposures = {}
51
- @env = env
60
+ @env = env
61
+ @view_options = view_options || DEFAULT_VIEW_OPTIONS
52
62
 
53
63
  @sending_file = false
54
64
  end
@@ -65,6 +75,10 @@ module Hanami
65
75
  end
66
76
  end
67
77
 
78
+ def render(view, **options)
79
+ self.body = view.(**view_options.(request, self), **options).to_str
80
+ end
81
+
68
82
  def format=(args)
69
83
  @format, content_type = *args
70
84
  content_type = Action::Mime.content_type_with_charset(content_type, charset)
@@ -88,15 +102,12 @@ module Hanami
88
102
  end
89
103
 
90
104
  def flash
91
- @flash ||= Flash.new(session)
105
+ @flash ||= Flash.new(session[FLASH_SESSION_KEY])
92
106
  end
93
107
 
94
108
  def redirect_to(url, status: 302)
95
109
  return unless renderable?
96
110
 
97
- # This trick avoids to instantiate `flash` if it wasn't already.
98
- flash.keep! if defined?(@flash)
99
-
100
111
  redirect(::String.new(url), status)
101
112
  Halt.call(status)
102
113
  end
@@ -108,7 +119,11 @@ module Hanami
108
119
  end
109
120
 
110
121
  def unsafe_send_file(path)
111
- directory = @configuration.root_directory if Pathname.new(path).relative?
122
+ directory = if Pathname.new(path).relative?
123
+ @configuration.root_directory
124
+ else
125
+ FILE_SYSTEM_ROOT
126
+ end
112
127
 
113
128
  _send_file(
114
129
  Rack::File.new(path, directory).call(env)
@@ -148,9 +163,13 @@ module Hanami
148
163
  end
149
164
 
150
165
  def renderable?
166
+ return !head? && body.empty? if body.respond_to?(:empty?)
167
+
151
168
  !@sending_file && !head?
152
169
  end
153
170
 
171
+ alias to_ary to_a
172
+
154
173
  def head?
155
174
  env[REQUEST_METHOD] == HEAD
156
175
  end
@@ -16,17 +16,6 @@ module Hanami
16
16
 
17
17
  private
18
18
 
19
- # Container useful to transport data with the HTTP session
20
- #
21
- # @return [Hanami::Action::Flash] a Flash instance
22
- #
23
- # @since 0.3.0
24
- #
25
- # @see Hanami::Action::Flash
26
- def flash
27
- @flash ||= Flash.new(session)
28
- end
29
-
30
19
  # Finalize the response
31
20
  #
32
21
  # @return [void]
@@ -36,9 +25,12 @@ module Hanami
36
25
  #
37
26
  # @see Hanami::Action#finish
38
27
  def finish(req, res, *)
39
- res.flash.clear
40
- res[:session] = res.session
41
- res[:flash] = res.flash
28
+ if (next_flash = res.flash.next).any?
29
+ res.session['_flash'] = next_flash
30
+ else
31
+ res.session.delete('_flash')
32
+ end
33
+
42
34
  super
43
35
  end
44
36
  end
@@ -0,0 +1,581 @@
1
+ begin
2
+ require 'hanami/validations'
3
+ require 'hanami/action/validatable'
4
+ rescue LoadError
5
+ end
6
+
7
+ require 'hanami/utils/class_attribute'
8
+ require 'hanami/utils/callbacks'
9
+ require 'hanami/utils'
10
+ require 'hanami/utils/string'
11
+ require 'hanami/utils/kernel'
12
+ require 'rack/utils'
13
+
14
+ require_relative 'base_params'
15
+ require_relative 'configuration'
16
+ require_relative 'halt'
17
+ require_relative 'mime'
18
+ require_relative 'rack/file'
19
+ require_relative 'request'
20
+ require_relative 'response'
21
+
22
+ module Hanami
23
+ class Action
24
+ module StandaloneAction
25
+ def self.included(klass)
26
+ klass.include InstanceMethods
27
+ klass.extend ClassMethods
28
+ end
29
+
30
+ module ClassMethods
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ alias_method :config, :configuration
36
+
37
+ # Override Ruby's hook for modules.
38
+ # It includes basic Hanami::Action modules to the given class.
39
+ #
40
+ # @param subclass [Class] the target action
41
+ #
42
+ # @since 0.1.0
43
+ # @api private
44
+ def inherited(subclass)
45
+ if subclass.superclass == Action
46
+ subclass.class_eval do
47
+ include Utils::ClassAttribute
48
+
49
+ class_attribute :before_callbacks
50
+ self.before_callbacks = Utils::Callbacks::Chain.new
51
+
52
+ class_attribute :after_callbacks
53
+ self.after_callbacks = Utils::Callbacks::Chain.new
54
+
55
+ include Validatable if defined?(Validatable)
56
+ end
57
+ end
58
+
59
+ subclass.instance_variable_set '@configuration', configuration.dup
60
+ end
61
+
62
+ # Returns the class which defines the params
63
+ #
64
+ # Returns the class which has been provided to define the
65
+ # params. By default this will be Hanami::Action::Params.
66
+ #
67
+ # @return [Class] A params class (when whitelisted) or
68
+ # Hanami::Action::Params
69
+ #
70
+ # @api private
71
+ # @since 0.7.0
72
+ def params_class
73
+ @params_class ||= BaseParams
74
+ end
75
+
76
+ # FIXME: make this thread-safe
77
+ def accepted_formats
78
+ @accepted_formats ||= []
79
+ end
80
+
81
+ # Define a callback for an Action.
82
+ # The callback will be executed **before** the action is called, in the
83
+ # order they are added.
84
+ #
85
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
86
+ # each of them is representing a name of a method available in the
87
+ # context of the Action.
88
+ #
89
+ # @param blk [Proc] an anonymous function to be executed
90
+ #
91
+ # @return [void]
92
+ #
93
+ # @since 0.3.2
94
+ #
95
+ # @see Hanami::Action::Callbacks::ClassMethods#append_after
96
+ #
97
+ # @example Method names (symbols)
98
+ # require 'hanami/controller'
99
+ #
100
+ # class Show
101
+ # include Hanami::Action
102
+ #
103
+ # before :authenticate, :set_article
104
+ #
105
+ # def call(params)
106
+ # end
107
+ #
108
+ # private
109
+ # def authenticate
110
+ # # ...
111
+ # end
112
+ #
113
+ # # `params` in the method signature is optional
114
+ # def set_article(params)
115
+ # @article = Article.find params[:id]
116
+ # end
117
+ # end
118
+ #
119
+ # # The order of execution will be:
120
+ # #
121
+ # # 1. #authenticate
122
+ # # 2. #set_article
123
+ # # 3. #call
124
+ #
125
+ # @example Anonymous functions (Procs)
126
+ # require 'hanami/controller'
127
+ #
128
+ # class Show
129
+ # include Hanami::Action
130
+ #
131
+ # before { ... } # 1 do some authentication stuff
132
+ # before {|params| @article = Article.find params[:id] } # 2
133
+ #
134
+ # def call(params)
135
+ # end
136
+ # end
137
+ #
138
+ # # The order of execution will be:
139
+ # #
140
+ # # 1. authentication
141
+ # # 2. set the article
142
+ # # 3. #call
143
+ def append_before(*callbacks, &blk)
144
+ before_callbacks.append(*callbacks, &blk)
145
+ end
146
+
147
+ alias_method :before, :append_before
148
+
149
+ # class << self
150
+ # # @since 0.1.0
151
+ # alias before append_before
152
+ # end
153
+
154
+ # Define a callback for an Action.
155
+ # The callback will be executed **after** the action is called, in the
156
+ # order they are added.
157
+ #
158
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
159
+ # each of them is representing a name of a method available in the
160
+ # context of the Action.
161
+ #
162
+ # @param blk [Proc] an anonymous function to be executed
163
+ #
164
+ # @return [void]
165
+ #
166
+ # @since 0.3.2
167
+ #
168
+ # @see Hanami::Action::Callbacks::ClassMethods#append_before
169
+ def append_after(*callbacks, &blk)
170
+ after_callbacks.append(*callbacks, &blk)
171
+ end
172
+
173
+ alias_method :after, :append_after
174
+
175
+ # class << self
176
+ # # @since 0.1.0
177
+ # alias after append_after
178
+ # end
179
+
180
+ # Define a callback for an Action.
181
+ # The callback will be executed **before** the action is called.
182
+ # It will add the callback at the beginning of the callbacks' chain.
183
+ #
184
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
185
+ # each of them is representing a name of a method available in the
186
+ # context of the Action.
187
+ #
188
+ # @param blk [Proc] an anonymous function to be executed
189
+ #
190
+ # @return [void]
191
+ #
192
+ # @since 0.3.2
193
+ #
194
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
195
+ def prepend_before(*callbacks, &blk)
196
+ before_callbacks.prepend(*callbacks, &blk)
197
+ end
198
+
199
+ # Define a callback for an Action.
200
+ # The callback will be executed **after** the action is called.
201
+ # It will add the callback at the beginning of the callbacks' chain.
202
+ #
203
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
204
+ # each of them is representing a name of a method available in the
205
+ # context of the Action.
206
+ #
207
+ # @param blk [Proc] an anonymous function to be executed
208
+ #
209
+ # @return [void]
210
+ #
211
+ # @since 0.3.2
212
+ #
213
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
214
+ def prepend_after(*callbacks, &blk)
215
+ after_callbacks.prepend(*callbacks, &blk)
216
+ end
217
+
218
+ # Restrict the access to the specified mime type symbols.
219
+ #
220
+ # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
221
+ #
222
+ # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
223
+ # be converted into a mime type
224
+ #
225
+ # @since 0.1.0
226
+ #
227
+ # @see Hanami::Controller::Configuration#format
228
+ #
229
+ # @example
230
+ # require 'hanami/controller'
231
+ #
232
+ # class Show
233
+ # include Hanami::Action
234
+ # accept :html, :json
235
+ #
236
+ # def call(params)
237
+ # # ...
238
+ # end
239
+ # end
240
+ #
241
+ # # When called with "*/*" => 200
242
+ # # When called with "text/html" => 200
243
+ # # When called with "application/json" => 200
244
+ # # When called with "application/xml" => 406
245
+ def accept(*formats)
246
+ @accepted_formats = *formats
247
+ before :enforce_accepted_mime_types
248
+ end
249
+
250
+ # Returns a new action
251
+ #
252
+ # @overload new(**deps, ...)
253
+ # @param deps [Hash] action dependencies
254
+ #
255
+ # @overload new(configuration:, **deps, ...)
256
+ # @param configuration [Hanami::Controller::Configuration] action configuration
257
+ # @param deps [Hash] action dependencies
258
+ #
259
+ # @return [Hanami::Action] Action object
260
+ #
261
+ # @since 2.0.0
262
+ def new(*args, configuration: self.configuration, **kwargs, &block)
263
+ allocate.tap do |obj|
264
+ obj.instance_variable_set(:@name, Name[name])
265
+ obj.instance_variable_set(:@configuration, configuration.dup.finalize!)
266
+ obj.instance_variable_set(:@accepted_mime_types, Mime.restrict_mime_types(configuration, accepted_formats))
267
+ obj.send(:initialize, *args, **kwargs, &block)
268
+ obj.freeze
269
+ end
270
+ end
271
+
272
+ module Name
273
+ MODULE_SEPARATOR_TRANSFORMER = [:gsub, "::", "."].freeze
274
+
275
+ def self.call(name)
276
+ Utils::String.transform(name, MODULE_SEPARATOR_TRANSFORMER, :underscore) unless name.nil?
277
+ end
278
+
279
+ class << self
280
+ alias_method :[], :call
281
+ end
282
+ end
283
+ end
284
+
285
+ module InstanceMethods
286
+ attr_reader :name
287
+
288
+ # Implements the Rack/Hanami::Action protocol
289
+ #
290
+ # @since 0.1.0
291
+ # @api private
292
+ def call(env)
293
+ request = nil
294
+ response = nil
295
+
296
+ halted = catch :halt do
297
+ begin
298
+ params = self.class.params_class.new(env)
299
+ request = build_request(env, params)
300
+ response = build_response(
301
+ request: request,
302
+ action: name,
303
+ configuration: configuration,
304
+ content_type: Mime.calculate_content_type_with_charset(configuration, request, accepted_mime_types),
305
+ env: env,
306
+ headers: configuration.default_headers
307
+ )
308
+
309
+ _run_before_callbacks(request, response)
310
+ handle(request, response)
311
+ _run_after_callbacks(request, response)
312
+ rescue => exception
313
+ _handle_exception(request, response, exception)
314
+ end
315
+ end
316
+
317
+ finish(request, response, halted)
318
+ end
319
+
320
+ def initialize(**deps)
321
+ @_deps = deps
322
+ end
323
+
324
+ protected
325
+
326
+ # Hook for subclasses to apply behavior as part of action invocation
327
+ #
328
+ # @param request [Hanami::Action::Request]
329
+ # @param response [Hanami::Action::Response]
330
+ #
331
+ # @since 2.0.0
332
+ def handle(request, response)
333
+ end
334
+
335
+ # Halt the action execution with the given HTTP status code and message.
336
+ #
337
+ # When used, the execution of a callback or of an action is interrupted
338
+ # and the control returns to the framework, that decides how to handle
339
+ # the event.
340
+ #
341
+ # If a message is provided, it sets the response body with the message.
342
+ # Otherwise, it sets the response body with the default message associated
343
+ # to the code (eg 404 will set `"Not Found"`).
344
+ #
345
+ # @param status [Fixnum] a valid HTTP status code
346
+ # @param body [String] the response body
347
+ #
348
+ # @raise [StandardError] if the code isn't valid
349
+ #
350
+ # @since 0.2.0
351
+ #
352
+ # @see Hanami::Action::Throwable#handle_exception
353
+ # @see Hanami::Http::Status:ALL
354
+ #
355
+ # @example Basic usage
356
+ # require 'hanami/controller'
357
+ #
358
+ # class Show
359
+ # def call(params)
360
+ # halt 404
361
+ # end
362
+ # end
363
+ #
364
+ # # => [404, {}, ["Not Found"]]
365
+ #
366
+ # @example Custom message
367
+ # require 'hanami/controller'
368
+ #
369
+ # class Show
370
+ # def call(params)
371
+ # halt 404, "This is not the droid you're looking for."
372
+ # end
373
+ # end
374
+ #
375
+ # # => [404, {}, ["This is not the droid you're looking for."]]
376
+ def halt(status, body = nil)
377
+ Halt.call(status, body)
378
+ end
379
+
380
+ # @since 0.3.2
381
+ # @api private
382
+ def _requires_no_body?(res)
383
+ HTTP_STATUSES_WITHOUT_BODY.include?(res.status)
384
+ end
385
+
386
+ # @since 2.0.0
387
+ # @api private
388
+ def _requires_empty_headers?(res)
389
+ _requires_no_body?(res) || res.head?
390
+ end
391
+
392
+ private
393
+
394
+ attr_reader :configuration
395
+
396
+ def accepted_mime_types
397
+ @accepted_mime_types || configuration.mime_types
398
+ end
399
+
400
+ def enforce_accepted_mime_types(req, *)
401
+ Mime.accepted_mime_type?(req, accepted_mime_types, configuration) or halt 406
402
+ end
403
+
404
+ def exception_handler(exception)
405
+ configuration.handled_exceptions.each do |exception_class, handler|
406
+ return handler if exception.kind_of?(exception_class)
407
+ end
408
+
409
+ nil
410
+ end
411
+
412
+ def build_request(env, params)
413
+ Request.new(env, params)
414
+ end
415
+
416
+ def build_response(**options)
417
+ Response.new(**options)
418
+ end
419
+
420
+ # @since 0.2.0
421
+ # @api private
422
+ def _reference_in_rack_errors(req, exception)
423
+ req.env[RACK_EXCEPTION] = exception
424
+
425
+ if errors = req.env[RACK_ERRORS]
426
+ errors.write(_dump_exception(exception))
427
+ errors.flush
428
+ end
429
+ end
430
+
431
+ # @since 0.2.0
432
+ # @api private
433
+ def _dump_exception(exception)
434
+ [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
435
+ end
436
+
437
+ # @since 0.1.0
438
+ # @api private
439
+ def _handle_exception(req, res, exception)
440
+ handler = exception_handler(exception)
441
+
442
+ if handler.nil?
443
+ _reference_in_rack_errors(req, exception)
444
+ raise exception
445
+ end
446
+
447
+ instance_exec(
448
+ req,
449
+ res,
450
+ exception,
451
+ &_exception_handler(handler)
452
+ )
453
+
454
+ nil
455
+ end
456
+
457
+ # @since 0.3.0
458
+ # @api private
459
+ def _exception_handler(handler)
460
+ if respond_to?(handler.to_s, true)
461
+ method(handler)
462
+ else
463
+ ->(*) { halt handler }
464
+ end
465
+ end
466
+
467
+ # @since 0.1.0
468
+ # @api private
469
+ def _run_before_callbacks(*args)
470
+ self.class.before_callbacks.run(self, *args)
471
+ nil
472
+ end
473
+
474
+ # @since 0.1.0
475
+ # @api private
476
+ def _run_after_callbacks(*args)
477
+ self.class.after_callbacks.run(self, *args)
478
+ nil
479
+ end
480
+
481
+ # According to RFC 2616, when a response MUST have an empty body, it only
482
+ # allows Entity Headers.
483
+ #
484
+ # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
485
+ # other custom header.
486
+ #
487
+ # This restriction is enforced by <tt>Hanami::Action#_requires_no_body?</tt>.
488
+ #
489
+ # However, there are cases that demand to bypass this rule to set meta
490
+ # informations via headers.
491
+ #
492
+ # An example is a <tt>DELETE</tt> request for a JSON API application.
493
+ # It returns a <tt>204</tt> but still wants to specify the rate limit
494
+ # quota via <tt>X-Rate-Limit</tt>.
495
+ #
496
+ # @since 0.5.0
497
+ #
498
+ # @see Hanami::Action#_requires_no_body?
499
+ #
500
+ # @example
501
+ # require 'hanami/controller'
502
+ #
503
+ # module Books
504
+ # class Destroy
505
+ # include Hanami::Action
506
+ #
507
+ # def call(params)
508
+ # # ...
509
+ # self.headers.merge!(
510
+ # 'Last-Modified' => 'Fri, 27 Nov 2015 13:32:36 GMT',
511
+ # 'X-Rate-Limit' => '4000',
512
+ # 'Content-Type' => 'application/json',
513
+ # 'X-No-Pass' => 'true'
514
+ # )
515
+ #
516
+ # self.status = 204
517
+ # end
518
+ #
519
+ # private
520
+ #
521
+ # def keep_response_header?(header)
522
+ # super || header == 'X-Rate-Limit'
523
+ # end
524
+ # end
525
+ # end
526
+ #
527
+ # # Only the following headers will be sent:
528
+ # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
529
+ # # * X-Rate-Limit - because we explicitely allow it
530
+ #
531
+ # # Both Content-Type and X-No-Pass are removed because they're not allowed
532
+ def keep_response_header?(header)
533
+ ENTITY_HEADERS.include?(header)
534
+ end
535
+
536
+ # @since 2.0.0
537
+ # @api private
538
+ def _empty_headers(res)
539
+ res.headers.select! { |header, _| keep_response_header?(header) }
540
+ end
541
+
542
+ def format(value)
543
+ case value
544
+ when Symbol
545
+ format = Utils::Kernel.Symbol(value)
546
+ [format, Action::Mime.format_to_mime_type(format, configuration)]
547
+ when String
548
+ [Action::Mime.detect_format(value, configuration), value]
549
+ else
550
+ raise Hanami::Controller::UnknownFormatError.new(value)
551
+ end
552
+ end
553
+
554
+ # Finalize the response
555
+ #
556
+ # This method is abstract and COULD be implemented by included modules in
557
+ # order to prepare their data before the response will be returned to the
558
+ # webserver.
559
+ #
560
+ # @since 0.1.0
561
+ # @api private
562
+ # @abstract
563
+ #
564
+ # @see Hanami::Action::Callable#finish
565
+ # @see Hanami::Action::Session#finish
566
+ # @see Hanami::Action::Cookies#finish
567
+ # @see Hanami::Action::Cache#finish
568
+ def finish(req, res, halted)
569
+ res.status, res.body = *halted unless halted.nil?
570
+
571
+ _empty_headers(res) if _requires_empty_headers?(res)
572
+
573
+ res.set_format(Action::Mime.detect_format(res.content_type, configuration))
574
+ res[:params] = req.params
575
+ res[:format] = res.format
576
+ res
577
+ end
578
+ end
579
+ end
580
+ end
581
+ end