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
@@ -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