hanami-controller 2.2.0 → 2.3.0.beta2

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.
@@ -7,37 +7,39 @@ require_relative "errors"
7
7
 
8
8
  module Hanami
9
9
  class Action
10
+ # @api private
10
11
  module Mime # rubocop:disable Metrics/ModuleLength
11
- # Most commom MIME Types used for responses
12
+ # Most commom media types used for responses
12
13
  #
13
14
  # @since 1.0.0
14
- # @api private
15
+ # @api public
15
16
  TYPES = {
16
- txt: "text/plain",
17
- html: "text/html",
18
- json: "application/json",
19
- manifest: "text/cache-manifest",
20
17
  atom: "application/atom+xml",
21
18
  avi: "video/x-msvideo",
22
19
  bmp: "image/bmp",
23
- bz: "application/x-bzip",
24
20
  bz2: "application/x-bzip2",
21
+ bz: "application/x-bzip",
25
22
  chm: "application/vnd.ms-htmlhelp",
26
23
  css: "text/css",
27
24
  csv: "text/csv",
28
25
  flv: "video/x-flv",
26
+ form: "application/x-www-form-urlencoded",
29
27
  gif: "image/gif",
30
28
  gz: "application/x-gzip",
31
29
  h264: "video/h264",
30
+ html: "text/html",
32
31
  ico: "image/vnd.microsoft.icon",
33
32
  ics: "text/calendar",
34
33
  jpg: "image/jpeg",
35
34
  js: "application/javascript",
36
- mp4: "video/mp4",
35
+ json: "application/json",
36
+ manifest: "text/cache-manifest",
37
37
  mov: "video/quicktime",
38
38
  mp3: "audio/mpeg",
39
+ mp4: "video/mp4",
39
40
  mp4a: "audio/mp4",
40
41
  mpg: "video/mpeg",
42
+ multipart: "multipart/form-data",
41
43
  oga: "audio/ogg",
42
44
  ogg: "application/ogg",
43
45
  ogv: "video/ogg",
@@ -53,13 +55,14 @@ module Hanami
53
55
  tar: "application/x-tar",
54
56
  torrent: "application/x-bittorrent",
55
57
  tsv: "text/tab-separated-values",
58
+ txt: "text/plain",
56
59
  uri: "text/uri-list",
57
60
  vcs: "text/x-vcalendar",
58
61
  wav: "audio/x-wav",
59
62
  webm: "video/webm",
60
63
  wmv: "video/x-ms-wmv",
61
- woff: "application/font-woff",
62
64
  woff2: "application/font-woff2",
65
+ woff: "application/font-woff",
63
66
  wsdl: "application/wsdl+xml",
64
67
  xhtml: "application/xhtml+xml",
65
68
  xml: "application/xml",
@@ -68,9 +71,109 @@ module Hanami
68
71
  zip: "application/zip"
69
72
  }.freeze
70
73
 
74
+ # @api private
71
75
  ANY_TYPE = "*/*"
72
76
 
77
+ # @api private
78
+ Format = Data.define(:name, :media_type, :accept_types, :content_types) do
79
+ def initialize(name:, media_type:, accept_types: [media_type], content_types: [media_type])
80
+ super
81
+ end
82
+ end
83
+
84
+ # @api private
85
+ FORMATS = TYPES
86
+ .to_h { |name, media_type| [name, Format.new(name:, media_type:)] }
87
+ .update(
88
+ all: Format.new(
89
+ name: :all,
90
+ media_type: "application/octet-stream",
91
+ accept_types: ["*/*"],
92
+ content_types: ["*/*"]
93
+ ),
94
+ html: Format.new(
95
+ name: :html,
96
+ media_type: "text/html",
97
+ content_types: ["application/x-www-form-urlencoded", "multipart/form-data"]
98
+ )
99
+ )
100
+ .freeze
101
+ private_constant :FORMATS
102
+
103
+ # @api private
104
+ MEDIA_TYPES_TO_FORMATS = FORMATS.each_with_object({}) { |(_name, format), hsh|
105
+ hsh[format.media_type] = format
106
+ }.freeze
107
+ private_constant :MEDIA_TYPES_TO_FORMATS
108
+
109
+ # @api private
110
+ ACCEPT_TYPES_TO_FORMATS = FORMATS.each_with_object({}) { |(_name, format), hsh|
111
+ format.accept_types.each { |type| hsh[type] = format }
112
+ }.freeze
113
+ private_constant :ACCEPT_TYPES_TO_FORMATS
114
+
73
115
  class << self
116
+ # Yields if an action is configured with `formats`, the request has an `Accept` header, and
117
+ # none of the Accept types matches the accepted formats. The given block is expected to halt
118
+ # the request handling.
119
+ #
120
+ # If any of these conditions are not met, then the request is acceptable and the method
121
+ # returns without yielding.
122
+ #
123
+ # @see Action#enforce_accepted_media_types
124
+ # @see Config#formats
125
+ #
126
+ # @api private
127
+ def enforce_accept(request, config)
128
+ return unless request.accept_header?
129
+
130
+ accept_types = ::Rack::Utils.q_values(request.accept).map(&:first)
131
+ return if accept_types.any? { |type| accepted_type?(type, config) }
132
+
133
+ yield
134
+ end
135
+
136
+ # Yields if an action is configured with `formats`, the request has a `Content-Type` header,
137
+ # and the content type does not match the accepted formats. The given block is expected to
138
+ # halt the request handling.
139
+ #
140
+ # If any of these conditions are not met, then the request is acceptable and the method
141
+ # returns without yielding.
142
+ #
143
+ # @see Action#enforce_accepted_media_types
144
+ # @see Config#formats
145
+ #
146
+ # @api private
147
+ def enforce_content_type(request, config)
148
+ # Compare media type (without parameters) instead of full Content-Type header to avoid
149
+ # false negatives (e.g., multipart/form-data; boundary=...)
150
+ media_type = request.media_type
151
+
152
+ return if media_type.nil?
153
+
154
+ return if accepted_content_type?(media_type, config)
155
+
156
+ yield
157
+ end
158
+
159
+ # Returns a string combining a media type and charset, intended for setting as the
160
+ # `Content-Type` header for the response to the given request.
161
+ #
162
+ # This uses the request's `Accept` header (if present) along with the configured formats to
163
+ # determine the best content type to return.
164
+ #
165
+ # @return [String]
166
+ #
167
+ # @see Action#call
168
+ #
169
+ # @api private
170
+ def response_content_type_with_charset(request, config)
171
+ content_type_with_charset(
172
+ response_content_type(request, config),
173
+ config.default_charset || Action::DEFAULT_CHARSET
174
+ )
175
+ end
176
+
74
177
  # Returns a format name for the given content type.
75
178
  #
76
179
  # The format name will come from the configured formats, if such a format is configured
@@ -81,52 +184,50 @@ module Hanami
81
184
  # This is used to return the format name a {Response}.
82
185
  #
83
186
  # @example
84
- # detect_format("application/jsonl charset=utf-8", config) # => :json
187
+ # format_from_media_type("application/json;charset=utf-8", config) # => :json
85
188
  #
86
189
  # @return [Symbol, nil]
87
190
  #
88
191
  # @see Response#format
89
192
  # @see Action#finish
90
193
  #
91
- # @since 2.0.0
92
194
  # @api private
93
- def detect_format(content_type, config)
94
- return if content_type.nil?
195
+ def format_from_media_type(media_type, config)
196
+ return if media_type.nil?
95
197
 
96
- ct = content_type.split(";").first
97
- config.formats.format_for(ct) || TYPES.key(ct)
198
+ mt = media_type.split(";").first
199
+ config.formats.format_for(mt) || MEDIA_TYPES_TO_FORMATS[mt]&.name
98
200
  end
99
201
 
100
202
  # Returns a format name and content type pair for a given format name or content type
101
203
  # string.
102
204
  #
103
205
  # @example
104
- # detect_format_and_content_type(:json, config)
206
+ # format_and_media_type(:json, config)
105
207
  # # => [:json, "application/json"]
106
208
  #
107
- # detect_format_and_content_type("application/json", config)
209
+ # format_and_media_type("application/json", config)
108
210
  # # => [:json, "application/json"]
109
211
  #
110
212
  # @example Unknown format name
111
- # detect_format_and_content_type(:unknown, config)
213
+ # format_and_media_type(:unknown, config)
112
214
  # # raises Hanami::Action::UnknownFormatError
113
215
  #
114
216
  # @example Unknown content type
115
- # detect_format_and_content_type("application/unknown", config)
217
+ # format_and_media_type("application/unknown", config)
116
218
  # # => [nil, "application/unknown"]
117
219
  #
118
220
  # @return [Array<(Symbol, String)>]
119
221
  #
120
222
  # @raise [Hanami::Action::UnknownFormatError] if an unknown format name is given
121
223
  #
122
- # @since 2.0.0
123
224
  # @api private
124
- def detect_format_and_content_type(value, config)
225
+ def format_and_media_type(value, config)
125
226
  case value
126
227
  when Symbol
127
- [value, format_to_mime_type(value, config)]
228
+ [value, format_to_media_type(value, config)]
128
229
  when String
129
- [detect_format(value, config), value]
230
+ [format_from_media_type(value, config), value]
130
231
  else
131
232
  raise UnknownFormatError.new(value)
132
233
  end
@@ -144,34 +245,13 @@ module Hanami
144
245
  #
145
246
  # @return [String]
146
247
  #
147
- # @since 2.0.0
148
248
  # @api private
149
249
  def content_type_with_charset(content_type, charset)
150
250
  "#{content_type}; charset=#{charset}"
151
251
  end
152
252
 
153
- # Returns a string combining a MIME type and charset, intended for setting as the
154
- # `Content-Type` header for the response to the given request.
155
- #
156
- # This uses the request's `Accept` header (if present) along with the configured formats to
157
- # determine the best content type to return.
158
- #
159
- # @return [String]
160
- #
161
- # @see Action#call
162
- #
163
- # @since 2.0.0
164
- # @api private
165
- def response_content_type_with_charset(request, config)
166
- content_type_with_charset(
167
- response_content_type(request, config),
168
- config.default_charset || Action::DEFAULT_CHARSET
169
- )
170
- end
171
-
172
253
  # Patched version of <tt>Rack::Utils.best_q_match</tt>.
173
254
  #
174
- # @since 2.0.0
175
255
  # @api private
176
256
  #
177
257
  # @see http://www.rubydoc.info/gems/rack/Rack/Utils#best_q_match-class_method
@@ -179,7 +259,7 @@ module Hanami
179
259
  # @see https://github.com/hanami/controller/issues/59
180
260
  # @see https://github.com/hanami/controller/issues/104
181
261
  # @see https://github.com/hanami/controller/issues/275
182
- def best_q_match(q_value_header, available_mimes = TYPES.values)
262
+ def best_q_match(q_value_header, available_mimes)
183
263
  ::Rack::Utils.q_values(q_value_header).each_with_index.map { |(req_mime, quality), index|
184
264
  match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
185
265
  next unless match
@@ -188,97 +268,118 @@ module Hanami
188
268
  }.compact.max&.format
189
269
  end
190
270
 
191
- # Yields if an action is configured with `formats`, the request has an `Accept` header, an
192
- # none of the Accept types matches the accepted formats. The given block is expected to halt
193
- # the request handling.
194
- #
195
- # If any of these conditions are not met, then the request is acceptable and the method
196
- # returns without yielding.
197
- #
198
- # @see Action#enforce_accepted_mime_types
199
- # @see Config#formats
200
- #
201
- # @since 2.0.0
202
- # @api private
203
- def enforce_accept(request, config)
204
- return unless request.accept_header?
205
-
206
- accept_types = ::Rack::Utils.q_values(request.accept).map(&:first)
207
- return if accept_types.any? { |mime_type| accepted_mime_type?(mime_type, config) }
271
+ private
208
272
 
209
- yield
273
+ # @api private
274
+ def accepted_type?(media_type, config)
275
+ accepted_types(config).any? { |accepted_type|
276
+ ::Rack::Mime.match?(media_type, accepted_type)
277
+ }
210
278
  end
211
279
 
212
- # Yields if an action is configured with `formats`, the request has a `Content-Type` header
213
- # (or a `default_requst_format` is configured), and the content type does not match the
214
- # accepted formats. The given block is expected to halt the request handling.
215
- #
216
- # If any of these conditions are not met, then the request is acceptable and the method
217
- # returns without yielding.
218
- #
219
- # @see Action#enforce_accepted_mime_types
220
- # @see Config#formats
221
- #
222
- # @since 2.0.0
223
280
  # @api private
224
- def enforce_content_type(request, config)
225
- content_type = request.content_type
281
+ def accepted_types(config)
282
+ return [ANY_TYPE] if config.formats.empty?
226
283
 
227
- return if content_type.nil?
284
+ config.formats.map { |format| format_to_accept_types(format, config) }.flatten(1)
285
+ end
228
286
 
229
- return if accepted_mime_type?(content_type, config)
287
+ def format_to_accept_types(format, config)
288
+ configured_types = config.formats.accept_types_for(format)
289
+ return configured_types if configured_types.any?
230
290
 
231
- yield
291
+ FORMATS
292
+ .fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
293
+ .accept_types
232
294
  end
233
295
 
234
- private
235
-
236
- # @since 2.0.0
237
296
  # @api private
238
- def accepted_mime_type?(mime_type, config)
239
- accepted_mime_types(config).any? { |accepted_mime_type|
240
- ::Rack::Mime.match?(mime_type, accepted_mime_type)
297
+ def accepted_content_type?(content_type, config)
298
+ accepted_content_types(config).any? { |accepted_content_type|
299
+ ::Rack::Mime.match?(content_type, accepted_content_type)
241
300
  }
242
301
  end
243
302
 
244
- # @since 2.0.0
245
303
  # @api private
246
- def accepted_mime_types(config)
304
+ def accepted_content_types(config)
247
305
  return [ANY_TYPE] if config.formats.empty?
248
306
 
249
- config.formats.map { |format| format_to_mime_types(format, config) }.flatten(1)
307
+ config.formats.map { |format| format_to_content_types(format, config) }.flatten(1)
308
+ end
309
+
310
+ # @api private
311
+ def format_to_content_types(format, config)
312
+ configured_types = config.formats.content_types_for(format)
313
+ return configured_types if configured_types.any?
314
+
315
+ FORMATS
316
+ .fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
317
+ .content_types
250
318
  end
251
319
 
252
- # @since 2.0.0
253
320
  # @api private
254
321
  def response_content_type(request, config)
322
+ # This method prepares the default `Content-Type` for the response. Importantly, it only
323
+ # does this after `#enforce_accept` and `#enforce_content_type` have already passed the
324
+ # request. So by the time we get here, the request has been deemed acceptable to the
325
+ # action, so we can try to be as helpful as possible in setting an appropriate content
326
+ # type for the response.
327
+
255
328
  if request.accept_header?
256
- all_mime_types = TYPES.values + config.formats.mapping.keys
257
- content_type = best_q_match(request.accept, all_mime_types)
329
+ content_type =
330
+ if config.formats.empty? || config.formats.accepted.include?(:all)
331
+ permissive_response_content_type(request, config)
332
+ else
333
+ restrictive_response_content_type(request, config)
334
+ end
258
335
 
259
336
  return content_type if content_type
260
337
  end
261
338
 
262
339
  if config.formats.default
263
- return format_to_mime_type(config.formats.default, config)
340
+ return format_to_media_type(config.formats.default, config)
264
341
  end
265
342
 
266
343
  Action::DEFAULT_CONTENT_TYPE
267
344
  end
268
345
 
269
- # @since 2.0.0
270
346
  # @api private
271
- def format_to_mime_type(format, config)
272
- config.formats.mime_type_for(format) ||
273
- TYPES.fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
347
+ def permissive_response_content_type(request, config)
348
+ # If no accepted formats are configured, or if the formats include :all, then we're
349
+ # working with a "permissive" action. In this case we simply want a response content type
350
+ # that corresponds to the request's accept header as closely as possible. This means we
351
+ # work from _all_ the media types we know of.
352
+
353
+ all_media_types =
354
+ (ACCEPT_TYPES_TO_FORMATS.keys | MEDIA_TYPES_TO_FORMATS.keys) +
355
+ config.formats.accept_types
356
+
357
+ best_q_match(request.accept, all_media_types)
274
358
  end
275
359
 
276
- # @since 2.0.0
277
360
  # @api private
278
- def format_to_mime_types(format, config)
279
- config.formats.mime_types_for(format).tap { |types|
280
- types << TYPES[format] if TYPES.key?(format)
281
- }
361
+ def restrictive_response_content_type(request, config)
362
+ # When specific formats are configured, this is a "resitrctive" action. Here we want to
363
+ # match against the configured accept types only, and work back from those to the
364
+ # configured format, so we can use its canonical media type for the content type.
365
+
366
+ accept_types_to_formats = config.formats.accepted_formats(FORMATS)
367
+ .each_with_object({}) { |(_, format), hsh|
368
+ format.accept_types.each { hsh[_1] = format }
369
+ }
370
+
371
+ accept_type = best_q_match(request.accept, accept_types_to_formats.keys)
372
+ accept_types_to_formats[accept_type].media_type if accept_type
373
+ end
374
+
375
+ # @api private
376
+ def format_to_media_type(format, config)
377
+ configured_type = config.formats.media_type_for(format)
378
+ return configured_type if configured_type
379
+
380
+ FORMATS
381
+ .fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
382
+ .media_type
282
383
  end
283
384
  end
284
385
  end
@@ -362,13 +362,7 @@ module Hanami
362
362
  # @since 0.7.0
363
363
  # @api private
364
364
  def _router_params(fallback = {})
365
- env.fetch(ROUTER_PARAMS) do
366
- if session = fallback.delete(Action::RACK_SESSION)
367
- fallback[Action::RACK_SESSION] = Utils::Hash.deep_symbolize(session)
368
- end
369
-
370
- fallback
371
- end
365
+ env.fetch(ROUTER_PARAMS, fallback)
372
366
  end
373
367
  end
374
368
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack/file"
3
+ require "rack/files"
4
4
 
5
5
  module Hanami
6
6
  class Action
@@ -21,7 +21,7 @@ module Hanami
21
21
  # @since 0.4.3
22
22
  # @api private
23
23
  def initialize(path, root)
24
- @file = ::Rack::File.new(root.to_s)
24
+ @file = ::Rack::Files.new(root.to_s)
25
25
  @path = path.to_s
26
26
  end
27
27
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ # @since 2.2.0
6
+ # @api private
7
+ def self.rack_3?
8
+ defined?(::Rack::Headers)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Hanami
6
+ class Action
7
+ class Request < ::Rack::Request
8
+ # Wrapper for Rack-provided sessions, allowing access using symbol keys.
9
+ #
10
+ # @since 2.3.0
11
+ # @api public
12
+ class Session
13
+ extend Forwardable
14
+
15
+ def_delegators \
16
+ :@session,
17
+ :clear,
18
+ :delete,
19
+ :empty?,
20
+ :size,
21
+ :length,
22
+ :each,
23
+ :to_h,
24
+ :inspect,
25
+ :keys,
26
+ :values
27
+
28
+ def initialize(session)
29
+ @session = session
30
+ end
31
+
32
+ def [](key)
33
+ @session[key.to_s]
34
+ end
35
+
36
+ def []=(key, value)
37
+ @session[key.to_s] = value
38
+ end
39
+
40
+ def key?(key)
41
+ @session.key?(key.to_s)
42
+ end
43
+
44
+ alias_method :has_key?, :key?
45
+ alias_method :include?, :key?
46
+
47
+ def ==(other)
48
+ Utils::Hash.deep_symbolize(@session) == Utils::Hash.deep_symbolize(other)
49
+ end
50
+
51
+ private
52
+
53
+ # Provides a fallback for any methods not handled by the def_delegators.
54
+ def method_missing(method_name, *args, &block)
55
+ if @session.respond_to?(method_name)
56
+ @session.send(method_name, *args, &block)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def respond_to_missing?(method_name, include_private = false)
63
+ @session.respond_to?(method_name, include_private) || super
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -26,11 +26,12 @@ module Hanami
26
26
 
27
27
  # @since 2.0.0
28
28
  # @api private
29
- def initialize(env:, params:, session_enabled: false)
29
+ def initialize(env:, params:, default_tld_length: 1, session_enabled: false)
30
30
  super(env)
31
31
 
32
32
  @params = params
33
33
  @session_enabled = session_enabled
34
+ @default_tld_length = default_tld_length
34
35
  end
35
36
 
36
37
  # Returns the request's ID
@@ -56,7 +57,7 @@ module Hanami
56
57
 
57
58
  # Returns the session for the request.
58
59
  #
59
- # @return [Hash] the session object
60
+ # @return [Hanami::Request::Session] the session object
60
61
  #
61
62
  # @raise [MissingSessionError] if the session is not enabled
62
63
  #
@@ -70,7 +71,7 @@ module Hanami
70
71
  raise Hanami::Action::MissingSessionError.new("Hanami::Action::Request#session")
71
72
  end
72
73
 
73
- super
74
+ @session ||= Session.new(super)
74
75
  end
75
76
 
76
77
  # Returns the flash for the request.
@@ -91,6 +92,31 @@ module Hanami
91
92
  @flash ||= Flash.new(session[Flash::KEY])
92
93
  end
93
94
 
95
+ # Returns the subdomains for the current host.
96
+ #
97
+ # @return [Array<String>]
98
+ #
99
+ # @api public
100
+ # @since 2.3.0
101
+ def subdomains(tld_length = @default_tld_length)
102
+ return [] if IP_ADDRESS_HOST_REGEXP.match?(host)
103
+
104
+ host.split(".")[0..-(tld_length + 2)]
105
+ end
106
+
107
+ IP_ADDRESS_HOST_REGEXP = /\A\d+\.\d+\.\d+\.\d+\z/
108
+ private_constant :IP_ADDRESS_HOST_REGEXP
109
+
110
+ # Returns the subdomain for the current host.
111
+ #
112
+ # @return [String]
113
+ #
114
+ # @api public
115
+ # @since 2.3.0
116
+ def subdomain(tld_length = @default_tld_length)
117
+ subdomains(tld_length).join(".")
118
+ end
119
+
94
120
  # @since 2.0.0
95
121
  # @api private
96
122
  def accept?(mime_type)
@@ -42,7 +42,7 @@ module Hanami
42
42
  new(config: Action.config.dup, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r|
43
43
  r.status = status
44
44
  r.body = Http::Status.message_for(status)
45
- r.set_format(Mime.detect_format(r.content_type), config)
45
+ r.set_format(Mime.format_from_media_type(r.content_type), config)
46
46
  end
47
47
  end
48
48
 
@@ -71,12 +71,15 @@ module Hanami
71
71
  # @api public
72
72
  def body=(str)
73
73
  @length = 0
74
- @body = EMPTY_BODY.dup
74
+ @body = EMPTY_BODY.dup
75
+
76
+ return if str.nil? || str == EMPTY_BODY
75
77
 
76
78
  if str.is_a?(::Rack::Files::BaseIterator)
77
79
  @body = str
80
+ buffered_body! # Ensure appropriate content-length is set
78
81
  else
79
- write(str) unless str.nil? || str == EMPTY_BODY
82
+ write(str)
80
83
  end
81
84
  end
82
85
 
@@ -131,7 +134,7 @@ module Hanami
131
134
  # @since 2.0.0
132
135
  # @api public
133
136
  def format
134
- @format ||= Mime.detect_format(content_type, @config)
137
+ @format ||= Mime.format_from_media_type(content_type, @config)
135
138
  end
136
139
 
137
140
  # Sets the format and associated content type for the response.
@@ -162,7 +165,7 @@ module Hanami
162
165
  # @since 2.0.0
163
166
  # @api public
164
167
  def format=(value)
165
- format, content_type = Mime.detect_format_and_content_type(value, @config)
168
+ format, content_type = Mime.format_and_media_type(value, @config)
166
169
 
167
170
  self.content_type = Mime.content_type_with_charset(content_type, charset)
168
171