hanami-controller 1.3.3 → 2.0.0.alpha1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -7
  3. data/README.md +295 -537
  4. data/hanami-controller.gemspec +3 -3
  5. data/lib/hanami/action.rb +653 -38
  6. data/lib/hanami/action/base_params.rb +2 -2
  7. data/lib/hanami/action/cache.rb +1 -139
  8. data/lib/hanami/action/cache/cache_control.rb +4 -4
  9. data/lib/hanami/action/cache/conditional_get.rb +4 -5
  10. data/lib/hanami/action/cache/directives.rb +1 -1
  11. data/lib/hanami/action/cache/expires.rb +3 -3
  12. data/lib/hanami/action/cookie_jar.rb +3 -3
  13. data/lib/hanami/action/cookies.rb +3 -62
  14. data/lib/hanami/action/flash.rb +2 -2
  15. data/lib/hanami/action/glue.rb +5 -31
  16. data/lib/hanami/action/halt.rb +12 -0
  17. data/lib/hanami/action/mime.rb +77 -491
  18. data/lib/hanami/action/params.rb +3 -3
  19. data/lib/hanami/action/rack/file.rb +1 -1
  20. data/lib/hanami/action/request.rb +30 -20
  21. data/lib/hanami/action/response.rb +174 -0
  22. data/lib/hanami/action/session.rb +8 -117
  23. data/lib/hanami/action/validatable.rb +2 -2
  24. data/lib/hanami/controller.rb +0 -210
  25. data/lib/hanami/controller/configuration.rb +51 -506
  26. data/lib/hanami/controller/version.rb +1 -1
  27. metadata +12 -21
  28. data/lib/hanami/action/callable.rb +0 -92
  29. data/lib/hanami/action/callbacks.rb +0 -214
  30. data/lib/hanami/action/configurable.rb +0 -50
  31. data/lib/hanami/action/exposable.rb +0 -126
  32. data/lib/hanami/action/exposable/guard.rb +0 -104
  33. data/lib/hanami/action/head.rb +0 -121
  34. data/lib/hanami/action/rack.rb +0 -411
  35. data/lib/hanami/action/rack/callable.rb +0 -47
  36. data/lib/hanami/action/rack/errors.rb +0 -53
  37. data/lib/hanami/action/redirect.rb +0 -59
  38. data/lib/hanami/action/throwable.rb +0 -169
@@ -17,13 +17,13 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = []
18
18
  spec.test_files = spec.files.grep(%r{^(spec)/})
19
19
  spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.3.0'
20
+ spec.required_ruby_version = '>= 2.5.0'
21
21
 
22
22
  spec.add_dependency 'rack', '~> 2.0'
23
- spec.add_dependency 'hanami-utils', '~> 1.3'
23
+ spec.add_dependency 'hanami-utils', '~> 2.0.alpha'
24
24
 
25
25
  spec.add_development_dependency 'bundler', '>= 1.6', '< 3'
26
26
  spec.add_development_dependency 'rack-test', '~> 1.0'
27
- spec.add_development_dependency 'rake', '~> 13'
27
+ spec.add_development_dependency 'rake', '~> 12'
28
28
  spec.add_development_dependency 'rspec', '~> 3.7'
29
29
  end
@@ -1,17 +1,21 @@
1
- require 'hanami/action/configurable'
2
- require 'hanami/action/rack'
3
- require 'hanami/action/mime'
4
- require 'hanami/action/redirect'
5
- require 'hanami/action/exposable'
6
- require 'hanami/action/throwable'
7
- require 'hanami/action/callbacks'
8
1
  begin
9
2
  require 'hanami/validations'
10
3
  require 'hanami/action/validatable'
11
4
  rescue LoadError
12
5
  end
13
- require 'hanami/action/head'
14
- require 'hanami/action/callable'
6
+
7
+ require 'hanami/action/request'
8
+ require 'hanami/action/response'
9
+ require 'hanami/action/base_params'
10
+ require 'hanami/action/rack/file'
11
+
12
+ require 'rack/utils'
13
+ require 'hanami/utils'
14
+ require 'hanami/utils/kernel'
15
+
16
+ require 'hanami/utils/class_attribute'
17
+
18
+ require 'hanami/utils/callbacks'
15
19
 
16
20
  module Hanami
17
21
  # An HTTP endpoint
@@ -28,7 +32,137 @@ module Hanami
28
32
  # # ...
29
33
  # end
30
34
  # end
31
- module Action
35
+ class Action
36
+ require "hanami/action/mime"
37
+ require "hanami/action/halt"
38
+
39
+ # Rack SPEC response code
40
+ #
41
+ # @since 1.0.0
42
+ # @api private
43
+ RESPONSE_CODE = 0
44
+
45
+ # Rack SPEC response headers
46
+ #
47
+ # @since 1.0.0
48
+ # @api private
49
+ RESPONSE_HEADERS = 1
50
+
51
+ # Rack SPEC response body
52
+ #
53
+ # @since 1.0.0
54
+ # @api private
55
+ RESPONSE_BODY = 2
56
+
57
+ DEFAULT_ERROR_CODE = 500
58
+
59
+ # Status codes that by RFC must not include a message body
60
+ #
61
+ # @since 0.3.2
62
+ # @api private
63
+ HTTP_STATUSES_WITHOUT_BODY = Set.new((100..199).to_a << 204 << 205 << 304).freeze
64
+
65
+ # Not Found
66
+ #
67
+ # @since 1.0.0
68
+ # @api private
69
+ NOT_FOUND = 404
70
+
71
+ # Entity headers allowed in blank body responses, according to
72
+ # RFC 2616 - Section 10 (HTTP 1.1).
73
+ #
74
+ # "The response MAY include new or updated metainformation in the form
75
+ # of entity-headers".
76
+ #
77
+ # @since 0.4.0
78
+ # @api private
79
+ #
80
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
81
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html
82
+ ENTITY_HEADERS = {
83
+ 'Allow' => true,
84
+ 'Content-Encoding' => true,
85
+ 'Content-Language' => true,
86
+ 'Content-Location' => true,
87
+ 'Content-MD5' => true,
88
+ 'Content-Range' => true,
89
+ 'Expires' => true,
90
+ 'Last-Modified' => true,
91
+ 'extension-header' => true
92
+ }.freeze
93
+
94
+ # The request method
95
+ #
96
+ # @since 0.3.2
97
+ # @api private
98
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
99
+
100
+ # The Content-Length HTTP header
101
+ #
102
+ # @since 1.0.0
103
+ # @api private
104
+ CONTENT_LENGTH = 'Content-Length'.freeze
105
+
106
+ # The non-standard HTTP header to pass the control over when a resource
107
+ # cannot be found by the current endpoint
108
+ #
109
+ # @since 1.0.0
110
+ # @api private
111
+ X_CASCADE = 'X-Cascade'.freeze
112
+
113
+ # HEAD request
114
+ #
115
+ # @since 0.3.2
116
+ # @api private
117
+ HEAD = 'HEAD'.freeze
118
+
119
+ # The key that returns accepted mime types from the Rack env
120
+ #
121
+ # @since 0.1.0
122
+ # @api private
123
+ HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
124
+
125
+ # The header key to set the mime type of the response
126
+ #
127
+ # @since 0.1.0
128
+ # @api private
129
+ CONTENT_TYPE = 'Content-Type'.freeze
130
+
131
+ # The default mime type for an incoming HTTP request
132
+ #
133
+ # @since 0.1.0
134
+ # @api private
135
+ DEFAULT_ACCEPT = '*/*'.freeze
136
+
137
+ # The default mime type that is returned in the response
138
+ #
139
+ # @since 0.1.0
140
+ # @api private
141
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
142
+
143
+ # @since 0.2.0
144
+ # @api private
145
+ RACK_ERRORS = 'rack.errors'.freeze
146
+
147
+ # This isn't part of Rack SPEC
148
+ #
149
+ # Exception notifiers use <tt>rack.exception</tt> instead of
150
+ # <tt>rack.errors</tt>, so we need to support it.
151
+ #
152
+ # @since 0.5.0
153
+ # @api private
154
+ #
155
+ # @see Hanami::Action::Throwable::RACK_ERRORS
156
+ # @see http://www.rubydoc.info/github/rack/rack/file/SPEC#The_Error_Stream
157
+ # @see https://github.com/hanami/controller/issues/133
158
+ RACK_EXCEPTION = 'rack.exception'.freeze
159
+
160
+ # The HTTP header for redirects
161
+ #
162
+ # @since 0.2.0
163
+ # @api private
164
+ LOCATION = 'Location'.freeze
165
+
32
166
  # Override Ruby's hook for modules.
33
167
  # It includes basic Hanami::Action modules to the given class.
34
168
  #
@@ -36,36 +170,512 @@ module Hanami
36
170
  #
37
171
  # @since 0.1.0
38
172
  # @api private
173
+ def self.inherited(base)
174
+ if base.superclass == Hanami::Action
175
+ base.class_eval do
176
+ include Utils::ClassAttribute
177
+
178
+ class_attribute :before_callbacks
179
+ self.before_callbacks = Utils::Callbacks::Chain.new
180
+
181
+ class_attribute :after_callbacks
182
+ self.after_callbacks = Utils::Callbacks::Chain.new
183
+
184
+ include Validatable if defined?(Validatable)
185
+ end
186
+ end
187
+ end
188
+
189
+ # Returns the class which defines the params
190
+ #
191
+ # Returns the class which has been provided to define the
192
+ # params. By default this will be Hanami::Action::Params.
193
+ #
194
+ # @return [Class] A params class (when whitelisted) or
195
+ # Hanami::Action::Params
196
+ #
197
+ # @api private
198
+ # @since 0.7.0
199
+ def self.params_class
200
+ @params_class ||= BaseParams
201
+ end
202
+
203
+ # FIXME: make this thread-safe
204
+ def self.accepted_formats
205
+ @accepted_formats ||= []
206
+ end
207
+
208
+ # FIXME: make this thread-safe
209
+ def self.handled_exceptions
210
+ @handled_exceptions ||= {}
211
+ end
212
+
213
+ # Define a callback for an Action.
214
+ # The callback will be executed **before** the action is called, in the
215
+ # order they are added.
216
+ #
217
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
218
+ # each of them is representing a name of a method available in the
219
+ # context of the Action.
220
+ #
221
+ # @param blk [Proc] an anonymous function to be executed
222
+ #
223
+ # @return [void]
224
+ #
225
+ # @since 0.3.2
226
+ #
227
+ # @see Hanami::Action::Callbacks::ClassMethods#append_after
228
+ #
229
+ # @example Method names (symbols)
230
+ # require 'hanami/controller'
231
+ #
232
+ # class Show
233
+ # include Hanami::Action
234
+ #
235
+ # before :authenticate, :set_article
236
+ #
237
+ # def call(params)
238
+ # end
239
+ #
240
+ # private
241
+ # def authenticate
242
+ # # ...
243
+ # end
244
+ #
245
+ # # `params` in the method signature is optional
246
+ # def set_article(params)
247
+ # @article = Article.find params[:id]
248
+ # end
249
+ # end
250
+ #
251
+ # # The order of execution will be:
252
+ # #
253
+ # # 1. #authenticate
254
+ # # 2. #set_article
255
+ # # 3. #call
256
+ #
257
+ # @example Anonymous functions (Procs)
258
+ # require 'hanami/controller'
259
+ #
260
+ # class Show
261
+ # include Hanami::Action
262
+ #
263
+ # before { ... } # 1 do some authentication stuff
264
+ # before {|params| @article = Article.find params[:id] } # 2
265
+ #
266
+ # def call(params)
267
+ # end
268
+ # end
269
+ #
270
+ # # The order of execution will be:
271
+ # #
272
+ # # 1. authentication
273
+ # # 2. set the article
274
+ # # 3. #call
275
+ def self.append_before(*callbacks, &blk)
276
+ before_callbacks.append(*callbacks, &blk)
277
+ end
278
+
279
+ class << self
280
+ # @since 0.1.0
281
+ alias before append_before
282
+ end
283
+
284
+ # Define a callback for an Action.
285
+ # The callback will be executed **after** the action is called, in the
286
+ # order they are added.
287
+ #
288
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
289
+ # each of them is representing a name of a method available in the
290
+ # context of the Action.
291
+ #
292
+ # @param blk [Proc] an anonymous function to be executed
293
+ #
294
+ # @return [void]
295
+ #
296
+ # @since 0.3.2
297
+ #
298
+ # @see Hanami::Action::Callbacks::ClassMethods#append_before
299
+ def self.append_after(*callbacks, &blk)
300
+ after_callbacks.append(*callbacks, &blk)
301
+ end
302
+
303
+ class << self
304
+ # @since 0.1.0
305
+ alias after append_after
306
+ end
307
+
308
+ # Define a callback for an Action.
309
+ # The callback will be executed **before** the action is called.
310
+ # It will add the callback at the beginning of the callbacks' chain.
311
+ #
312
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
313
+ # each of them is representing a name of a method available in the
314
+ # context of the Action.
315
+ #
316
+ # @param blk [Proc] an anonymous function to be executed
317
+ #
318
+ # @return [void]
319
+ #
320
+ # @since 0.3.2
321
+ #
322
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
323
+ def self.prepend_before(*callbacks, &blk)
324
+ before_callbacks.prepend(*callbacks, &blk)
325
+ end
326
+
327
+ # Define a callback for an Action.
328
+ # The callback will be executed **after** the action is called.
329
+ # It will add the callback at the beginning of the callbacks' chain.
330
+ #
331
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
332
+ # each of them is representing a name of a method available in the
333
+ # context of the Action.
334
+ #
335
+ # @param blk [Proc] an anonymous function to be executed
336
+ #
337
+ # @return [void]
338
+ #
339
+ # @since 0.3.2
340
+ #
341
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
342
+ def self.prepend_after(*callbacks, &blk)
343
+ after_callbacks.prepend(*callbacks, &blk)
344
+ end
345
+
346
+ # Restrict the access to the specified mime type symbols.
347
+ #
348
+ # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
349
+ #
350
+ # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
351
+ # be converted into a mime type
352
+ #
353
+ # @since 0.1.0
354
+ #
355
+ # @see Hanami::Controller::Configuration#format
356
+ #
357
+ # @example
358
+ # require 'hanami/controller'
359
+ #
360
+ # class Show
361
+ # include Hanami::Action
362
+ # accept :html, :json
363
+ #
364
+ # def call(params)
365
+ # # ...
366
+ # end
367
+ # end
368
+ #
369
+ # # When called with "*/*" => 200
370
+ # # When called with "text/html" => 200
371
+ # # When called with "application/json" => 200
372
+ # # When called with "application/xml" => 406
373
+ def self.accept(*formats)
374
+ @accepted_formats = *formats
375
+ before :enforce_accepted_mime_types
376
+ end
377
+
378
+ # Handle the given exception with an HTTP status code.
379
+ #
380
+ # When the exception is raise during #call execution, it will be
381
+ # translated into the associated HTTP status.
382
+ #
383
+ # This is a fine grained control, for a global configuration see
384
+ # Hanami::Action.handled_exceptions
385
+ #
386
+ # @param exception [Hash] the exception class must be the key and the
387
+ # HTTP status the value of the hash
388
+ #
389
+ # @since 0.1.0
390
+ #
391
+ # @see Hanami::Action.handled_exceptions
392
+ #
393
+ # @example
394
+ # require 'hanami/controller'
395
+ #
396
+ # class Show
397
+ # include Hanami::Action
398
+ # handle_exception RecordNotFound => 404
399
+ #
400
+ # def call(params)
401
+ # # ...
402
+ # raise RecordNotFound.new
403
+ # end
404
+ # end
405
+ #
406
+ # Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
407
+ def self.handle_exception(exception)
408
+ handled_exceptions.merge!(exception)
409
+ end
410
+
411
+ # Callbacks API instance methods
412
+ #
413
+ # @since 0.1.0
414
+ # @api private
415
+ def self.new(configuration:, **args)
416
+ allocate.tap do |obj|
417
+ obj.instance_variable_set(:@configuration, configuration)
418
+ obj.instance_variable_set(:@accepted_mime_types, Mime.restrict_mime_types(configuration, accepted_formats))
419
+ obj.instance_variable_set(
420
+ :@handled_exceptions,
421
+ ::Hash[
422
+ configuration
423
+ .handled_exceptions
424
+ .merge(handled_exceptions)
425
+ .sort{ |(ex1,_),(ex2,_)| ex1.ancestors.include?(ex2) ? -1 : 1 }
426
+ ]
427
+ )
428
+ obj.send(:initialize, **args)
429
+ obj.freeze
430
+ end
431
+ end
432
+
433
+ # Implements the Rack/Hanami::Action protocol
39
434
  #
40
- # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
41
- #
42
- # @see Hanami::Action::Rack
43
- # @see Hanami::Action::Mime
44
- # @see Hanami::Action::Http
45
- # @see Hanami::Action::Redirect
46
- # @see Hanami::Action::Exposable
47
- # @see Hanami::Action::Throwable
48
- # @see Hanami::Action::Callbacks
49
- # @see Hanami::Action::Validatable
50
- # @see Hanami::Action::Configurable
51
- # @see Hanami::Action::Callable
52
- def self.included(base)
53
- base.class_eval do
54
- include Rack
55
- include Mime
56
- include Redirect
57
- include Exposable
58
- include Throwable
59
- include Callbacks
60
- include Validatable if defined?(Validatable)
61
- include Configurable
62
- include Head
63
- prepend Callable
435
+ # @since 0.1.0
436
+ # @api private
437
+ def call(env)
438
+ request = nil
439
+ response = nil
440
+
441
+ halted = catch :halt do
442
+ begin
443
+ params = self.class.params_class.new(env)
444
+ request = Hanami::Action::Request.new(env, params)
445
+ response = Hanami::Action::Response.new(action: self.class.name, configuration: configuration, content_type: Mime.calculate_content_type_with_charset(configuration, request, accepted_mime_types), env: env, header: configuration.default_headers)
446
+ _run_before_callbacks(request, response)
447
+ handle(request, response)
448
+ _run_after_callbacks(request, response)
449
+ rescue => exception
450
+ _handle_exception(request, response, exception)
451
+ end
64
452
  end
453
+
454
+ finish(request, response, halted)
455
+ end
456
+
457
+ def initialize(**)
458
+ end
459
+
460
+ protected
461
+
462
+ def handle(request, response)
463
+ end
464
+
465
+ # Halt the action execution with the given HTTP status code and message.
466
+ #
467
+ # When used, the execution of a callback or of an action is interrupted
468
+ # and the control returns to the framework, that decides how to handle
469
+ # the event.
470
+ #
471
+ # If a message is provided, it sets the response body with the message.
472
+ # Otherwise, it sets the response body with the default message associated
473
+ # to the code (eg 404 will set `"Not Found"`).
474
+ #
475
+ # @param status [Fixnum] a valid HTTP status code
476
+ # @param body [String] the response body
477
+ #
478
+ # @raise [StandardError] if the code isn't valid
479
+ #
480
+ # @since 0.2.0
481
+ #
482
+ # @see Hanami::Controller#handled_exceptions
483
+ # @see Hanami::Action::Throwable#handle_exception
484
+ # @see Hanami::Http::Status:ALL
485
+ #
486
+ # @example Basic usage
487
+ # require 'hanami/controller'
488
+ #
489
+ # class Show
490
+ # def call(params)
491
+ # halt 404
492
+ # end
493
+ # end
494
+ #
495
+ # # => [404, {}, ["Not Found"]]
496
+ #
497
+ # @example Custom message
498
+ # require 'hanami/controller'
499
+ #
500
+ # class Show
501
+ # def call(params)
502
+ # halt 404, "This is not the droid you're looking for."
503
+ # end
504
+ # end
505
+ #
506
+ # # => [404, {}, ["This is not the droid you're looking for."]]
507
+ def halt(status, body = nil)
508
+ Halt.call(status, body)
509
+ end
510
+
511
+ # @since 0.3.2
512
+ # @api private
513
+ def _requires_no_body?(res)
514
+ HTTP_STATUSES_WITHOUT_BODY.include?(res.status)
515
+ end
516
+
517
+ # @since 2.0.0
518
+ # @api private
519
+ def _requires_empty_headers?(res)
520
+ _requires_no_body?(res) || res.head?
65
521
  end
66
522
 
67
523
  private
68
524
 
525
+ attr_reader :configuration
526
+
527
+ def accepted_mime_types
528
+ @accepted_mime_types || configuration.mime_types
529
+ end
530
+
531
+ def enforce_accepted_mime_types(req, *)
532
+ Mime.accepted_mime_type?(req, accepted_mime_types, configuration) or halt 406
533
+ end
534
+
535
+ attr_reader :handled_exceptions
536
+
537
+ def exception_handler(exception)
538
+ handled_exceptions.each do |exception_class, handler|
539
+ return handler if exception.kind_of?(exception_class)
540
+ end
541
+
542
+ nil
543
+ end
544
+
545
+ # @since 0.2.0
546
+ # @api private
547
+ def _reference_in_rack_errors(req, exception)
548
+ req.env[RACK_EXCEPTION] = exception
549
+
550
+ if errors = req.env[RACK_ERRORS]
551
+ errors.write(_dump_exception(exception))
552
+ errors.flush
553
+ end
554
+ end
555
+
556
+ # @since 0.2.0
557
+ # @api private
558
+ def _dump_exception(exception)
559
+ [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
560
+ end
561
+
562
+ # @since 0.1.0
563
+ # @api private
564
+ def _handle_exception(req, res, exception)
565
+ handler = exception_handler(exception)
566
+
567
+ if handler.nil?
568
+ _reference_in_rack_errors(req, exception)
569
+ raise exception
570
+ end
571
+
572
+ instance_exec(
573
+ req,
574
+ res,
575
+ exception,
576
+ &_exception_handler(handler)
577
+ )
578
+
579
+ nil
580
+ end
581
+
582
+ # @since 0.3.0
583
+ # @api private
584
+ def _exception_handler(handler)
585
+ if respond_to?(handler.to_s, true)
586
+ method(handler)
587
+ else
588
+ ->(*) { halt handler }
589
+ end
590
+ end
591
+
592
+ # @since 0.1.0
593
+ # @api private
594
+ def _run_before_callbacks(*args)
595
+ self.class.before_callbacks.run(self, *args)
596
+ nil
597
+ end
598
+
599
+ # @since 0.1.0
600
+ # @api private
601
+ def _run_after_callbacks(*args)
602
+ self.class.after_callbacks.run(self, *args)
603
+ nil
604
+ end
605
+
606
+ # According to RFC 2616, when a response MUST have an empty body, it only
607
+ # allows Entity Headers.
608
+ #
609
+ # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
610
+ # other custom header.
611
+ #
612
+ # This restriction is enforced by <tt>Hanami::Action#_requires_no_body?</tt>.
613
+ #
614
+ # However, there are cases that demand to bypass this rule to set meta
615
+ # informations via headers.
616
+ #
617
+ # An example is a <tt>DELETE</tt> request for a JSON API application.
618
+ # It returns a <tt>204</tt> but still wants to specify the rate limit
619
+ # quota via <tt>X-Rate-Limit</tt>.
620
+ #
621
+ # @since 0.5.0
622
+ #
623
+ # @see Hanami::Action#_requires_no_body?
624
+ #
625
+ # @example
626
+ # require 'hanami/controller'
627
+ #
628
+ # module Books
629
+ # class Destroy
630
+ # include Hanami::Action
631
+ #
632
+ # def call(params)
633
+ # # ...
634
+ # self.headers.merge!(
635
+ # 'Last-Modified' => 'Fri, 27 Nov 2015 13:32:36 GMT',
636
+ # 'X-Rate-Limit' => '4000',
637
+ # 'Content-Type' => 'application/json',
638
+ # 'X-No-Pass' => 'true'
639
+ # )
640
+ #
641
+ # self.status = 204
642
+ # end
643
+ #
644
+ # private
645
+ #
646
+ # def keep_response_header?(header)
647
+ # super || header == 'X-Rate-Limit'
648
+ # end
649
+ # end
650
+ # end
651
+ #
652
+ # # Only the following headers will be sent:
653
+ # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
654
+ # # * X-Rate-Limit - because we explicitely allow it
655
+ #
656
+ # # Both Content-Type and X-No-Pass are removed because they're not allowed
657
+ def keep_response_header?(header)
658
+ ENTITY_HEADERS.include?(header)
659
+ end
660
+
661
+ # @since 2.0.0
662
+ # @api private
663
+ def _empty_headers(res)
664
+ res.headers.select! { |header, _| keep_response_header?(header) }
665
+ end
666
+
667
+ def format(value)
668
+ case value
669
+ when Symbol
670
+ format = Utils::Kernel.Symbol(value)
671
+ [format, Action::Mime.format_to_mime_type(format, configuration)]
672
+ when String
673
+ [Action::Mime.detect_format(value, configuration), value]
674
+ else
675
+ raise Hanami::Controller::UnknownFormatError.new(value)
676
+ end
677
+ end
678
+
69
679
  # Raise error when `Hanami::Action::Session` isn't included.
70
680
  #
71
681
  # To use `session`, include `Hanami::Action::Session`.
@@ -98,14 +708,19 @@ module Hanami
98
708
  # @api private
99
709
  # @abstract
100
710
  #
101
- # @see Hanami::Action::Mime#finish
102
- # @see Hanami::Action::Exposable#finish
103
711
  # @see Hanami::Action::Callable#finish
104
712
  # @see Hanami::Action::Session#finish
105
713
  # @see Hanami::Action::Cookies#finish
106
714
  # @see Hanami::Action::Cache#finish
107
- # @see Hanami::Action::Head#finish
108
- def finish
715
+ def finish(req, res, halted)
716
+ res.status, res.body = *halted unless halted.nil?
717
+
718
+ _empty_headers(res) if _requires_empty_headers?(res)
719
+
720
+ res.set_format(Action::Mime.detect_format(res.content_type, configuration))
721
+ res[:params] = req.params
722
+ res[:format] = res.format
723
+ res
109
724
  end
110
725
  end
111
726
  end