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
@@ -1,56 +1,23 @@
1
- require 'rack/utils'
2
- require 'hanami/utils'
3
- require 'hanami/utils/kernel'
1
+ require "hanami/utils"
2
+ require "rack/utils"
3
+ require "rack/mime"
4
4
 
5
5
  module Hanami
6
- module Action
7
- # Mime type API
8
- #
9
- # @since 0.1.0
10
- #
11
- # @see Hanami::Action::Mime::ClassMethods#accept
6
+ class Action
12
7
  module Mime
13
- # The key that returns accepted mime types from the Rack env
14
- #
15
- # @since 0.1.0
16
- # @api private
17
- HTTP_ACCEPT = 'HTTP_ACCEPT'.freeze
18
-
19
- # The key that returns content mime type from the Rack env
20
- #
21
- # @since 1.2.0
22
- # @api private
23
- HTTP_CONTENT_TYPE = 'CONTENT_TYPE'.freeze
24
-
8
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
9
+ DEFAULT_CHARSET = 'utf-8'.freeze
25
10
  # The header key to set the mime type of the response
26
11
  #
27
12
  # @since 0.1.0
28
13
  # @api private
29
14
  CONTENT_TYPE = 'Content-Type'.freeze
30
15
 
31
- # The default mime type for an incoming HTTP request
32
- #
33
- # @since 0.1.0
34
- # @api private
35
- DEFAULT_ACCEPT = '*/*'.freeze
36
-
37
- # The default mime type that is returned in the response
38
- #
39
- # @since 0.1.0
40
- # @api private
41
- DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
42
-
43
- # The default charset that is returned in the response
44
- #
45
- # @since 0.3.0
46
- # @api private
47
- DEFAULT_CHARSET = 'utf-8'.freeze
48
-
49
16
  # Most commom MIME Types used for responses
50
17
  #
51
18
  # @since 1.0.0
52
19
  # @api private
53
- MIME_TYPES = {
20
+ TYPES = {
54
21
  txt: 'text/plain',
55
22
  html: 'text/html',
56
23
  json: 'application/json',
@@ -102,496 +69,115 @@ module Hanami
102
69
  xml: 'application/xml',
103
70
  xslt: 'application/xslt+xml',
104
71
  yml: 'text/yaml',
105
- zip: 'application/zip' }.freeze
72
+ zip: 'application/zip'
73
+ }.freeze
106
74
 
107
- # Override Ruby's hook for modules.
108
- # It includes Mime types logic
109
- #
110
- # @param base [Class] the target action
111
- #
112
- # @since 0.1.0
113
- # @api private
114
- #
115
- # @see http://www.ruby-doc.org/core-2.1.2/Module.html#method-i-included
116
- def self.included(base)
117
- base.class_eval do
118
- extend ClassMethods
119
- prepend InstanceMethods
120
- end
75
+ def self.content_type_with_charset(content_type, charset)
76
+ "#{content_type}; charset=#{charset}"
121
77
  end
122
78
 
123
- module ClassMethods
124
- # @since 0.2.0
125
- # @api private
126
- def format_to_mime_type(format)
127
- configuration.mime_type_for(format) ||
128
- MIME_TYPES[format] or
129
- raise Hanami::Controller::UnknownFormatError.new(format)
130
- end
131
-
132
- private
133
-
134
- # Restrict the access to the specified mime type symbols.
135
- #
136
- # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
137
- #
138
- # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
139
- # be converted into a mime type
140
- #
141
- # @since 0.1.0
142
- #
143
- # @see Hanami::Controller::Configuration#format
144
- #
145
- # @example
146
- # require 'hanami/controller'
147
- #
148
- # class Show
149
- # include Hanami::Action
150
- # accept :html, :json
151
- #
152
- # def call(params)
153
- # # ...
154
- # end
155
- # end
156
- #
157
- # # When called with "*/*" => 200
158
- # # When called with "text/html" => 200
159
- # # When called with "application/json" => 200
160
- # # When called with "application/xml" => 406
161
- def accept(*formats)
162
- mime_types = formats.map do |format|
163
- format_to_mime_type(format)
164
- end
165
-
166
- configuration.restrict_mime_types!(mime_types)
167
-
168
- before do
169
- unless mime_types.find {|mt| accept?(mt) }
170
- halt 406
171
- end
172
- end
79
+ # Use for setting Content-Type
80
+ # If the request has the ACCEPT header it will try to return the best Content-Type based
81
+ # on the content of the ACCEPT header taking in consideration the weights
82
+ #
83
+ # If no ACCEPT header it will check the default response_format, then the default request format and
84
+ # lastly it will fallback to DEFAULT_CONTENT_TYPE
85
+ #
86
+ # @return [String]
87
+ def self.content_type(configuration, request, accepted_mime_types)
88
+ if request.accept_header?
89
+ type = best_q_match(request.accept, accepted_mime_types)
90
+ return type if type
173
91
  end
174
92
 
175
- # Restrict the payload type to the specified mime type symbols.
176
- #
177
- # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
178
- #
179
- # @since 1.2.0
180
- #
181
- # @example
182
- # require 'hanami/controller'
183
- #
184
- # class Upload
185
- # include Hanami::Action
186
- # content_type :json
187
- #
188
- # def call(params)
189
- # # ...
190
- # end
191
- # end
192
- #
193
- # # When called with "text/html" => 415
194
- # # When called with "application/json" => 200
195
- def content_type(*formats)
196
- mime_types = formats.map { |format| format_to_mime_type(format) }
197
-
198
- before do
199
- mime_type = @_env[HTTP_CONTENT_TYPE] || default_content_type || DEFAULT_CONTENT_TYPE
200
-
201
- if mime_types.none? {|mt| ::Rack::Mime.match?(mime_type, mt) }
202
- halt 415
203
- end
204
- end
205
- end
93
+ default_response_type(configuration) || default_content_type(configuration) || DEFAULT_CONTENT_TYPE
206
94
  end
207
95
 
208
- # @since 0.7.0
209
- # @api private
210
- module InstanceMethods
211
- # @since 0.7.0
212
- # @api private
213
- def initialize(*)
214
- super
215
- @content_type = nil
216
- @charset = nil
217
- end
96
+ def self.charset(default_charset)
97
+ default_charset || DEFAULT_CHARSET
218
98
  end
219
99
 
220
- # Returns a symbol representation of the content type.
221
- #
222
- # The framework automatically detects the request mime type, and returns
223
- # the corresponding format.
224
- #
225
- # However, if this value was explicitly set by `#format=`, it will return
226
- # that value
227
- #
228
- # @return [Symbol] a symbol that corresponds to the content type
229
- #
230
- # @since 0.2.0
231
- #
232
- # @see Hanami::Action::Mime#format=
233
- # @see Hanami::Action::Mime#content_type
234
- #
235
- # @example Default scenario
236
- # require 'hanami/controller'
237
- #
238
- # class Show
239
- # include Hanami::Action
240
- #
241
- # def call(params)
242
- # end
243
- # end
244
- #
245
- # action = Show.new
246
- #
247
- # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
248
- # headers['Content-Type'] # => 'text/html'
249
- # action.format # => :html
250
- #
251
- # @example Set value
252
- # require 'hanami/controller'
253
- #
254
- # class Show
255
- # include Hanami::Action
256
- #
257
- # def call(params)
258
- # self.format = :xml
259
- # end
260
- # end
261
- #
262
- # action = Show.new
263
- #
264
- # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
265
- # headers['Content-Type'] # => 'application/xml'
266
- # action.format # => :xml
267
- def format
268
- @format ||= detect_format
100
+ def self.default_response_type(configuration)
101
+ format_to_mime_type(configuration.default_response_format, configuration)
269
102
  end
270
103
 
271
- # The content type that will be automatically set in the response.
272
- #
273
- # It prefers, in order:
274
- # * Explicit set value (see Hanami::Action::Mime#format=)
275
- # * Weighted value from Accept header based on all known MIME Types:
276
- # - Custom registered MIME Types (see Hanami::Controller::Configuration#format)
277
- # * Configured default content type (see Hanami::Controller::Configuration#default_response_format)
278
- # * Hard-coded default content type (see Hanami::Action::Mime::DEFAULT_CONTENT_TYPE)
279
- #
280
- # To override the value, use <tt>#format=</tt>
281
- #
282
- # @return [String] the content type from the request.
283
- #
284
- # @since 0.1.0
285
- #
286
- # @see Hanami::Action::Mime#format=
287
- # @see Hanami::Configuration#default_request_format
288
- # @see Hanami::Action::Mime#default_content_type
289
- # @see Hanami::Action::Mime#DEFAULT_CONTENT_TYPE
290
- # @see Hanami::Controller::Configuration#format
291
- # @see Hanami::Controller::Configuration#default_response_format
292
- #
293
- # @example
294
- # require 'hanami/controller'
295
- #
296
- # class Show
297
- # include Hanami::Action
298
- #
299
- # def call(params)
300
- # # ...
301
- # content_type # => 'text/html'
302
- # end
303
- # end
304
- def content_type
305
- return @content_type unless @content_type.nil?
306
- @content_type = content_type_from_accept_header if accept_header?
307
- @content_type || default_response_type || default_content_type || DEFAULT_CONTENT_TYPE
104
+ def self.default_content_type(configuration)
105
+ format_to_mime_type(configuration.default_request_format, configuration)
308
106
  end
309
107
 
310
- # Action charset setter, receives new charset value
311
- #
312
- # @return [String] the charset of the request.
313
- #
314
- # @since 0.3.0
315
- #
316
- # @example
317
- # require 'hanami/controller'
318
- #
319
- # class Show
320
- # include Hanami::Action
321
- #
322
- # def call(params)
323
- # # ...
324
- # self.charset = 'koi8-r'
325
- # end
326
- # end
327
- def charset=(value)
328
- @charset = value
329
- end
108
+ def self.format_to_mime_type(format, configuration)
109
+ return if format.nil?
330
110
 
331
- # The charset that will be automatically set in the response.
332
- #
333
- # It prefers, in order:
334
- # * Explicit set value (see #charset=)
335
- # * Default configuration charset
336
- # * Default content type
337
- #
338
- # To override the value, use <tt>#charset=</tt>
339
- #
340
- # @return [String] the charset of the request.
341
- #
342
- # @since 0.3.0
343
- #
344
- # @see Hanami::Action::Mime#charset=
345
- # @see Hanami::Configuration#default_charset
346
- # @see Hanami::Action::Mime#default_charset
347
- # @see Hanami::Action::Mime#DEFAULT_CHARSET
348
- #
349
- # @example
350
- # require 'hanami/controller'
351
- #
352
- # class Show
353
- # include Hanami::Action
354
- #
355
- # def call(params)
356
- # # ...
357
- # charset # => 'text/html'
358
- # end
359
- # end
360
- def charset
361
- @charset || default_charset || DEFAULT_CHARSET
111
+ configuration.mime_type_for(format) ||
112
+ TYPES.fetch(format) { raise Hanami::Controller::UnknownFormatError.new(format) }
362
113
  end
363
114
 
364
- private
365
-
366
- # Finalize the response by setting the current content type
115
+ # Transforms MIME Types to symbol
116
+ # Used for setting the format of the response
367
117
  #
368
- # @since 0.1.0
369
- # @api private
118
+ # @see Hanami::Action::Mime#finish
119
+ # @example
120
+ # detect_format("text/html; charset=utf-8", configuration) #=> :html
370
121
  #
371
- # @see Hanami::Action#finish
372
- def finish
373
- super
374
- headers[CONTENT_TYPE] ||= content_type_with_charset
122
+ # @return [Symbol, nil]
123
+ def self.detect_format(content_type, configuration)
124
+ return if content_type.nil?
125
+ ct = content_type.split(";").first
126
+ configuration.format_for(ct) || format_for(ct)
375
127
  end
376
128
 
377
- # Sets the given format and corresponding content type.
378
- #
379
- # The framework detects the `HTTP_ACCEPT` header of the request and sets
380
- # the proper `Content-Type` header in the response.
381
- # Within this default scenario, `#format` returns a symbol that
382
- # corresponds to `#content_type`.
383
- # For instance, if a client sends an `HTTP_ACCEPT` with `text/html`,
384
- # `#content_type` will return `text/html` and `#format` `:html`.
385
- #
386
- # However, it's possible to override what the framework have detected.
387
- # If a client asks for an `HTTP_ACCEPT` `*/*`, but we want to force the
388
- # response to be a `text/html` we can use this method.
389
- #
390
- # When the format is set, the framework searches for a corresponding mime
391
- # type to be set as the `Content-Type` header of the response.
392
- # This lookup is performed first in the configuration, and then in
393
- # `Hanami::Action::Mime::MIME_TYPES`. If the lookup fails, it raises an error.
394
- #
395
- # PERFORMANCE: Because `Hanami::Controller::Configuration#formats` is
396
- # smaller and looked up first than `Hanami::Action::Mime::MIME_TYPES`,
397
- # we suggest to configure the most common mime types used by your
398
- # application, **even if they are already present in that Rack constant**.
399
- #
400
- # @param format [#to_sym] the format
401
- #
402
- # @return [void]
403
- #
404
- # @raise [TypeError] if the format cannot be coerced into a Symbol
405
- # @raise [Hanami::Controller::UnknownFormatError] if the format doesn't
406
- # have a corresponding mime type
407
- #
408
- # @since 0.2.0
409
- #
410
- # @see Hanami::Action::Mime#format
411
- # @see Hanami::Action::Mime#content_type
412
- # @see Hanami::Controller::Configuration#format
413
- #
414
- # @example Default scenario
415
- # require 'hanami/controller'
416
- #
417
- # class Show
418
- # include Hanami::Action
419
- #
420
- # def call(params)
421
- # end
422
- # end
423
- #
424
- # action = Show.new
425
- #
426
- # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
427
- # headers['Content-Type'] # => 'application/octet-stream'
428
- # action.format # => :all
429
- #
430
- # _, headers, _ = action.call({ 'HTTP_ACCEPT' => 'text/html' })
431
- # headers['Content-Type'] # => 'text/html'
432
- # action.format # => :html
433
- #
434
- # @example Simple usage
435
- # require 'hanami/controller'
436
- #
437
- # class Show
438
- # include Hanami::Action
439
- #
440
- # def call(params)
441
- # # ...
442
- # self.format = :json
443
- # end
444
- # end
445
- #
446
- # action = Show.new
447
- #
448
- # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
449
- # headers['Content-Type'] # => 'application/json'
450
- # action.format # => :json
451
- #
452
- # @example Unknown format
453
- # require 'hanami/controller'
454
- #
455
- # class Show
456
- # include Hanami::Action
457
- #
458
- # def call(params)
459
- # # ...
460
- # self.format = :unknown
461
- # end
462
- # end
463
- #
464
- # action = Show.new
465
- # action.call({ 'HTTP_ACCEPT' => '*/*' })
466
- # # => raise Hanami::Controller::UnknownFormatError
467
- #
468
- # @example Custom mime type/format
469
- # require 'hanami/controller'
470
- #
471
- # Hanami::Controller.configure do
472
- # format :custom, 'application/custom'
473
- # end
474
- #
475
- # class Show
476
- # include Hanami::Action
477
- #
478
- # def call(params)
479
- # # ...
480
- # self.format = :custom
481
- # end
482
- # end
483
- #
484
- # _, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
485
- # headers['Content-Type'] # => 'application/custom'
486
- # action.format # => :custom
487
- def format=(format)
488
- @format = Utils::Kernel.Symbol(format)
489
- @content_type = self.class.format_to_mime_type(@format)
129
+ def self.format_for(content_type)
130
+ TYPES.key(content_type)
490
131
  end
491
132
 
492
- # Match the given mime type with the Accept header
493
- #
494
- # @return [Boolean] true if the given mime type matches Accept
495
- #
496
- # @since 0.1.0
497
- #
133
+ # Transforms symbols to MIME Types
498
134
  # @example
499
- # require 'hanami/controller'
500
- #
501
- # class Show
502
- # include Hanami::Action
503
- #
504
- # def call(params)
505
- # # ...
506
- # # @_env['HTTP_ACCEPT'] # => 'text/html,application/xhtml+xml,application/xml;q=0.9'
507
- #
508
- # accept?('text/html') # => true
509
- # accept?('application/xml') # => true
510
- # accept?('application/json') # => false
511
- #
135
+ # restrict_mime_types(configuration, [:json]) #=> ["application/json"]
512
136
  #
137
+ # @return [Array<String>, nil]
513
138
  #
514
- # # @_env['HTTP_ACCEPT'] # => '*/*'
515
- #
516
- # accept?('text/html') # => true
517
- # accept?('application/xml') # => true
518
- # accept?('application/json') # => true
519
- # end
520
- # end
521
- def accept?(mime_type)
522
- !!::Rack::Utils.q_values(accept).find do |mime, _|
523
- ::Rack::Mime.match?(mime_type, mime)
139
+ # @raise [Hanami::Controller::UnknownFormatError] if the format is invalid
140
+ def self.restrict_mime_types(configuration, accepted_formats)
141
+ return if accepted_formats.empty?
142
+
143
+ mime_types = accepted_formats.map do |format|
144
+ format_to_mime_type(format, configuration)
524
145
  end
525
- end
526
146
 
527
- # @since 0.1.0
528
- # @api private
529
- def accept
530
- @accept ||= @_env[HTTP_ACCEPT] || DEFAULT_ACCEPT
531
- end
147
+ accepted_mime_types = mime_types & configuration.mime_types
532
148
 
533
- # Checks if there is an Accept header for the current request.
534
- #
535
- # @return [TrueClass,FalseClass] the result of the check
536
- #
537
- # @since 0.8.0
538
- # @api private
539
- def accept_header?
540
- accept != DEFAULT_ACCEPT
149
+ return if accepted_mime_types.empty?
150
+ accepted_mime_types
541
151
  end
542
152
 
543
- # Look at the Accept header for the current request and see if it
544
- # matches any of the common MIME types (see Hanami::Action::Mime#MIME_TYPES)
545
- # or the custom registered ones (see Hanami::Controller::Configuration#format).
153
+ # Use for checking the Content-Type header to make sure is valid based
154
+ # on the accepted_mime_types
546
155
  #
547
- # @return [String,Nil] The matched MIME type for the given Accept header.
156
+ # If no Content-Type is sent in the request it will check the default_request_format
548
157
  #
549
- # @since 0.8.0
550
- # @api private
551
- #
552
- # @see Hanami::Action::Mime#MIME_TYPES
553
- # @see Hanami::Controller::Configuration#format
554
- #
555
- # @api private
556
- def content_type_from_accept_header
557
- best_q_match(accept, configuration.mime_types)
558
- end
559
-
560
- # @since 0.5.0
561
- # @api private
562
- def default_response_type
563
- self.class.format_to_mime_type(configuration.default_response_format) if configuration.default_response_format
564
- end
565
-
566
- # @since 0.2.0
567
- # @api private
568
- def default_content_type
569
- self.class.format_to_mime_type(
570
- configuration.default_request_format
571
- ) if configuration.default_request_format
572
- end
158
+ # @return [TrueClass, FalseClass]
159
+ def self.accepted_mime_type?(request, accepted_mime_types, configuration)
160
+ mime_type = request.env[CONTENT_TYPE] || default_content_type(configuration) || DEFAULT_CONTENT_TYPE
573
161
 
574
- # @since 0.2.0
575
- # @api private
576
- def detect_format
577
- configuration.format_for(content_type) || MIME_TYPES.key(content_type)
162
+ !accepted_mime_types.find { |mt| ::Rack::Mime.match?(mt, mime_type) }.nil?
578
163
  end
579
164
 
580
- # @since 0.3.0
581
- # @api private
582
- def default_charset
583
- configuration.default_charset
165
+ # Use for setting the content_type and charset if the response
166
+ #
167
+ # @see Hanami::Action::Mime#call
168
+ #
169
+ # @return [String]
170
+ def self.calculate_content_type_with_charset(configuration, request, accepted_mime_types)
171
+ charset = self.charset(configuration.default_charset)
172
+ content_type = self.content_type(configuration, request, accepted_mime_types)
173
+ content_type_with_charset(content_type, charset)
584
174
  end
585
175
 
586
- # @since 0.3.0
587
- # @api private
588
- def content_type_with_charset
589
- "#{content_type}; charset=#{charset}"
590
- end
176
+ # private
591
177
 
592
178
  # Patched version of <tt>Rack::Utils.best_q_match</tt>.
593
179
  #
594
- # @since 0.4.1
180
+ # @since 2.0.0
595
181
  # @api private
596
182
  #
597
183
  # @see http://www.rubydoc.info/gems/rack/Rack/Utils#best_q_match-class_method
@@ -599,7 +185,7 @@ module Hanami
599
185
  # @see https://github.com/hanami/controller/issues/59
600
186
  # @see https://github.com/hanami/controller/issues/104
601
187
  # @see https://github.com/hanami/controller/issues/275
602
- def best_q_match(q_value_header, available_mimes)
188
+ def self.best_q_match(q_value_header, available_mimes = TYPES.values)
603
189
  ::Rack::Utils.q_values(q_value_header).each_with_index.map do |(req_mime, quality), index|
604
190
  match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
605
191
  next unless match