hanami-controller 1.3.3 → 2.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
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