hanami-controller 2.0.0.beta4 → 2.0.0

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.
@@ -3,6 +3,7 @@
3
3
  require "hanami/utils"
4
4
  require "rack/utils"
5
5
  require "rack/mime"
6
+ require_relative "errors"
6
7
 
7
8
  module Hanami
8
9
  class Action
@@ -67,249 +68,217 @@ module Hanami
67
68
  zip: "application/zip"
68
69
  }.freeze
69
70
 
70
- # @since 2.0.0
71
- # @api private
72
- def self.content_type_with_charset(content_type, charset)
73
- "#{content_type}; charset=#{charset}"
74
- end
71
+ ANY_TYPE = "*/*"
72
+
73
+ class << self
74
+ # Returns a format name for the given content type.
75
+ #
76
+ # The format name will come from the configured formats, if such a format is configured
77
+ # there, or instead from the default list of formats in `Mime::TYPES`.
78
+ #
79
+ # Returns nil if no matching format can be found.
80
+ #
81
+ # This is used to return the format name a {Response}.
82
+ #
83
+ # @example
84
+ # detect_format("application/jsonl charset=utf-8", config) # => :json
85
+ #
86
+ # @return [Symbol, nil]
87
+ #
88
+ # @see Response#format
89
+ # @see Action#finish
90
+ #
91
+ # @since 2.0.0
92
+ # @api private
93
+ def detect_format(content_type, config)
94
+ return if content_type.nil?
75
95
 
76
- # Use for setting Content-Type
77
- # If the request has the ACCEPT header it will try to return the best Content-Type based
78
- # on the content of the ACCEPT header taking in consideration the weights
79
- #
80
- # If no ACCEPT header it will check the default response_format, then the default request format and
81
- # lastly it will fallback to DEFAULT_CONTENT_TYPE
82
- #
83
- # @return [String]
84
- #
85
- # @since 2.0.0
86
- # @api private
87
- def self.content_type(config, request, accepted_mime_types)
88
- if request.accept_header?
89
- type = best_q_match(request.accept, accepted_mime_types)
90
- return type if type
96
+ ct = content_type.split(";").first
97
+ config.formats.format_for(ct) || TYPES.key(ct)
91
98
  end
92
99
 
93
- default_response_type(config) || default_content_type(config) || Action::DEFAULT_CONTENT_TYPE
94
- end
95
-
96
- # @since 2.0.0
97
- # @api private
98
- def self.charset(default_charset)
99
- default_charset || Action::DEFAULT_CHARSET
100
- end
101
-
102
- # @since 2.0.0
103
- # @api private
104
- def self.default_response_type(config)
105
- format_to_mime_type(config.default_response_format, config)
106
- end
107
-
108
- # @since 2.0.0
109
- # @api private
110
- def self.default_content_type(config)
111
- format_to_mime_type(config.default_request_format, config)
112
- end
113
-
114
- # @since 2.0.0
115
- # @api private
116
- def self.format_to_mime_type(format, config)
117
- return if format.nil?
118
-
119
- config.mime_type_for(format) ||
120
- TYPES.fetch(format) { raise Hanami::Controller::UnknownFormatError.new(format) }
121
- end
122
-
123
- # Transforms MIME Types to symbol
124
- # Used for setting the format of the response
125
- #
126
- # @see Hanami::Action::Mime#finish
127
- # @example
128
- # detect_format("text/html; charset=utf-8", config) #=> :html
129
- #
130
- # @return [Symbol, nil]
131
- #
132
- # @since 2.0.0
133
- # @api private
134
- def self.detect_format(content_type, config)
135
- return if content_type.nil?
136
-
137
- ct = content_type.split(";").first
138
- config.format_for(ct) || format_for(ct)
139
- end
140
-
141
- # @since 2.0.0
142
- # @api private
143
- def self.format_for(content_type)
144
- TYPES.key(content_type)
145
- end
146
-
147
- # Transforms symbols to MIME Types
148
- # @example
149
- # restrict_mime_types(config, [:json]) #=> ["application/json"]
150
- #
151
- # @return [Array<String>, nil]
152
- #
153
- # @raise [Hanami::Controller::UnknownFormatError] if the format is invalid
154
- #
155
- # @since 2.0.0
156
- # @api private
157
- def self.restrict_mime_types(config)
158
- return if config.accepted_formats.empty?
159
-
160
- mime_types = config.accepted_formats.map do |format|
161
- format_to_mime_type(format, config)
100
+ # Returns a format name and content type pair for a given format name or content type
101
+ # string.
102
+ #
103
+ # @example
104
+ # detect_format_and_content_type(:json, config)
105
+ # # => [:json, "application/json"]
106
+ #
107
+ # detect_format_and_content_type("application/json", config)
108
+ # # => [:json, "application/json"]
109
+ #
110
+ # @example Unknown format name
111
+ # detect_format_and_content_type(:unknown, config)
112
+ # # raises Hanami::Action::UnknownFormatError
113
+ #
114
+ # @example Unknown content type
115
+ # detect_format_and_content_type("application/unknown", config)
116
+ # # => [nil, "application/unknown"]
117
+ #
118
+ # @return [Array<(Symbol, String)>]
119
+ #
120
+ # @raise [Hanami::Action::UnknownFormatError] if an unknown format name is given
121
+ #
122
+ # @since 2.0.0
123
+ # @api private
124
+ def detect_format_and_content_type(value, config)
125
+ case value
126
+ when Symbol
127
+ [value, format_to_mime_type(value, config)]
128
+ when String
129
+ [detect_format(value, config), value]
130
+ else
131
+ raise UnknownFormatError.new(value)
132
+ end
162
133
  end
163
134
 
164
- accepted_mime_types = mime_types & config.mime_types
165
-
166
- return if accepted_mime_types.empty?
167
-
168
- accepted_mime_types
169
- end
170
-
171
- # Yields if an action is configured with `accepted_formats`, the request has an `Accept`
172
- # header, and none of the Accept types matches the accepted formats. The given block is
173
- # expected to halt the request handling.
174
- #
175
- # If any of these conditions are not met, then the request is acceptable and the method
176
- # returns without yielding.
177
- #
178
- # @see Action#enforce_accepted_mime_types
179
- # @see Action.accept
180
- # @see Config#accepted_formats
181
- #
182
- # @since 2.0.0
183
- # @api private
184
- def self.enforce_accept(request, config)
185
- return unless request.accept_header?
135
+ # Returns a string combining the given content type and charset, intended for setting as a
136
+ # `Content-Type` header.
137
+ #
138
+ # @example
139
+ # Mime.content_type_with_charset("application/json", "utf-8")
140
+ # # => "application/json; charset=utf-8"
141
+ #
142
+ # @param content_type [String]
143
+ # @param charset [String]
144
+ #
145
+ # @return [String]
146
+ #
147
+ # @since 2.0.0
148
+ # @api private
149
+ def content_type_with_charset(content_type, charset)
150
+ "#{content_type}; charset=#{charset}"
151
+ end
186
152
 
187
- accept_types = ::Rack::Utils.q_values(request.accept).map(&:first)
188
- return if accept_types.any? { |mime_type| accepted_mime_type?(mime_type, config) }
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
189
171
 
190
- yield
191
- end
172
+ # Patched version of <tt>Rack::Utils.best_q_match</tt>.
173
+ #
174
+ # @since 2.0.0
175
+ # @api private
176
+ #
177
+ # @see http://www.rubydoc.info/gems/rack/Rack/Utils#best_q_match-class_method
178
+ # @see https://github.com/rack/rack/pull/659
179
+ # @see https://github.com/hanami/controller/issues/59
180
+ # @see https://github.com/hanami/controller/issues/104
181
+ # @see https://github.com/hanami/controller/issues/275
182
+ def best_q_match(q_value_header, available_mimes = TYPES.values)
183
+ ::Rack::Utils.q_values(q_value_header).each_with_index.map { |(req_mime, quality), index|
184
+ match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
185
+ next unless match
186
+
187
+ RequestMimeWeight.new(req_mime, quality, index, match)
188
+ }.compact.max&.format
189
+ end
192
190
 
193
- # Yields if an action is configured with `accepted_formats`, the request has a `Content-Type`
194
- # header (or a `default_requst_format` is configured), and the content type does not match the
195
- # accepted formats. The given block is expected to halt the request handling.
196
- #
197
- # If any of these conditions are not met, then the request is acceptable and the method
198
- # returns without yielding.
199
- #
200
- # @see Action#enforce_accepted_mime_types
201
- # @see Action.accept
202
- # @see Config#accepted_formats
203
- #
204
- # @since 2.0.0
205
- # @api private
206
- def self.enforce_content_type(request, config)
207
- content_type = request.content_type || default_content_type(config)
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?
208
205
 
209
- return if content_type.nil?
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) }
210
208
 
211
- return if accepted_mime_type?(content_type, config)
209
+ yield
210
+ end
212
211
 
213
- yield
214
- end
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
+ # @api private
224
+ def enforce_content_type(request, config)
225
+ content_type = request.content_type
215
226
 
216
- # @since 2.0.0
217
- # @api private
218
- def self.accepted_mime_type?(mime_type, config)
219
- config.accepted_mime_types.any? { |accepted_mime_type|
220
- ::Rack::Mime.match?(accepted_mime_type, mime_type)
221
- }
222
- end
227
+ return if content_type.nil?
223
228
 
224
- # Use for setting the content_type and charset if the response
225
- #
226
- # @see Hanami::Action::Mime#call
227
- #
228
- # @return [String]
229
- #
230
- # @since 2.0.0
231
- # @api private
232
- def self.calculate_content_type_with_charset(config, request, accepted_mime_types)
233
- charset = self.charset(config.default_charset)
234
- content_type = self.content_type(config, request, accepted_mime_types)
235
- content_type_with_charset(content_type, charset)
236
- end
229
+ return if accepted_mime_type?(content_type, config)
237
230
 
238
- # Patched version of <tt>Rack::Utils.best_q_match</tt>.
239
- #
240
- # @since 2.0.0
241
- # @api private
242
- #
243
- # @see http://www.rubydoc.info/gems/rack/Rack/Utils#best_q_match-class_method
244
- # @see https://github.com/rack/rack/pull/659
245
- # @see https://github.com/hanami/controller/issues/59
246
- # @see https://github.com/hanami/controller/issues/104
247
- # @see https://github.com/hanami/controller/issues/275
248
- def self.best_q_match(q_value_header, available_mimes = TYPES.values)
249
- ::Rack::Utils.q_values(q_value_header).each_with_index.map do |(req_mime, quality), index|
250
- match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
251
- next unless match
231
+ yield
232
+ end
252
233
 
253
- RequestMimeWeight.new(req_mime, quality, index, match)
254
- end.compact.max&.format
255
- end
234
+ private
256
235
 
257
- # @since 1.0.1
258
- # @api private
259
- class RequestMimeWeight
260
236
  # @since 2.0.0
261
237
  # @api private
262
- MIME_SEPARATOR = "/"
263
- private_constant :MIME_SEPARATOR
238
+ def accepted_mime_type?(mime_type, config)
239
+ accepted_mime_types(config).any? { |accepted_mime_type|
240
+ ::Rack::Mime.match?(accepted_mime_type, mime_type)
241
+ }
242
+ end
264
243
 
265
244
  # @since 2.0.0
266
245
  # @api private
267
- MIME_WILDCARD = "*"
268
- private_constant :MIME_WILDCARD
269
-
270
- include Comparable
271
-
272
- # @since 1.0.1
273
- # @api private
274
- attr_reader :quality
246
+ def accepted_mime_types(config)
247
+ return [ANY_TYPE] if config.formats.empty?
275
248
 
276
- # @since 1.0.1
277
- # @api private
278
- attr_reader :index
249
+ config.formats.map { |format| format_to_mime_types(format, config) }.flatten(1)
250
+ end
279
251
 
280
- # @since 1.0.1
252
+ # @since 2.0.0
281
253
  # @api private
282
- attr_reader :mime
254
+ def response_content_type(request, config)
255
+ 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)
283
258
 
284
- # @since 1.0.1
285
- # @api private
286
- attr_reader :format
259
+ return content_type if content_type
260
+ end
287
261
 
288
- # @since 1.0.1
289
- # @api private
290
- attr_reader :priority
262
+ if config.formats.default
263
+ return format_to_mime_type(config.formats.default, config)
264
+ end
291
265
 
292
- # @since 1.0.1
293
- # @api private
294
- def initialize(mime, quality, index, format = mime)
295
- @quality, @index, @format = quality, index, format
296
- calculate_priority(mime)
266
+ Action::DEFAULT_CONTENT_TYPE
297
267
  end
298
268
 
299
- # @since 1.0.1
269
+ # @since 2.0.0
300
270
  # @api private
301
- def <=>(other)
302
- return priority <=> other.priority unless priority == other.priority
303
-
304
- other.index <=> index
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) }
305
274
  end
306
275
 
307
- private
308
-
309
- # @since 1.0.1
276
+ # @since 2.0.0
310
277
  # @api private
311
- def calculate_priority(mime)
312
- @priority ||= (mime.split(MIME_SEPARATOR, 2).count(MIME_WILDCARD) * -10) + quality
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
+ }
313
282
  end
314
283
  end
315
284
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/action/base_params"
4
3
  require "hanami/validations/form"
5
4
 
6
5
  module Hanami
@@ -4,13 +4,17 @@ require "rack/file"
4
4
 
5
5
  module Hanami
6
6
  class Action
7
+ # Rack extensions for actions.
8
+ #
9
+ # @api private
10
+ # @since 0.4.3
7
11
  module Rack
8
12
  # File to be sent
9
13
  #
14
+ # @see Hanami::Action::Response#send_file
15
+ #
10
16
  # @since 0.4.3
11
17
  # @api private
12
- #
13
- # @see Hanami::Action::Rack#send_file
14
18
  class File
15
19
  # @param path [String,Pathname] file path
16
20
  #
@@ -1,22 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/action/flash"
4
3
  require "rack/mime"
5
4
  require "rack/request"
6
5
  require "rack/utils"
7
6
  require "securerandom"
7
+ require_relative "errors"
8
8
 
9
9
  module Hanami
10
10
  class Action
11
- # An HTTP request based on top of Rack::Request.
12
- # This guarantees backwards compatibility with with Rack.
11
+ # The HTTP request for an action, given to {Action#handle}.
13
12
  #
14
- # @since 0.3.1
13
+ # Inherits from `Rack::Request`, providing compatibility with Rack functionality.
15
14
  #
16
15
  # @see http://www.rubydoc.info/gems/rack/Rack/Request
16
+ #
17
+ # @since 0.3.1
17
18
  class Request < ::Rack::Request
19
+ # Returns the request's params.
20
+ #
21
+ # For an action with {Validatable} included, this will be a {Params} instance, otherwise a
22
+ # {BaseParams}.
23
+ #
24
+ # @return [BaseParams,Params]
25
+ #
26
+ # @since 2.0.0
27
+ # @api public
18
28
  attr_reader :params
19
29
 
30
+ # @since 2.0.0
31
+ # @api private
20
32
  def initialize(env:, params:, sessions_enabled: false)
21
33
  super(env)
22
34
 
@@ -24,11 +36,27 @@ module Hanami
24
36
  @sessions_enabled = sessions_enabled
25
37
  end
26
38
 
39
+ # Returns the request's ID
40
+ #
41
+ # @return [String]
42
+ #
43
+ # @since 2.0.0
44
+ # @api public
27
45
  def id
28
46
  # FIXME: make this number configurable and document the probabilities of clashes
29
47
  @id ||= @env[Action::REQUEST_ID] = SecureRandom.hex(Action::DEFAULT_ID_LENGTH)
30
48
  end
31
49
 
50
+ # Returns the session for the request.
51
+ #
52
+ # @return [Hash] the session object
53
+ #
54
+ # @raise [MissingSessionError] if sessions are not enabled
55
+ #
56
+ # @see Response#session
57
+ #
58
+ # @since 2.0.0
59
+ # @api public
32
60
  def session
33
61
  unless @sessions_enabled
34
62
  raise Hanami::Action::MissingSessionError.new("Hanami::Action::Request#session")
@@ -37,6 +65,16 @@ module Hanami
37
65
  super
38
66
  end
39
67
 
68
+ # Returns the flash for the request.
69
+ #
70
+ # @return [Flash]
71
+ #
72
+ # @raise [MissingSessionError] if sessions are not enabled
73
+ #
74
+ # @see Response#flash
75
+ #
76
+ # @since 2.0.0
77
+ # @api public
40
78
  def flash
41
79
  unless @sessions_enabled
42
80
  raise Hanami::Action::MissingSessionError.new("Hanami::Action::Request#flash")
@@ -45,12 +83,16 @@ module Hanami
45
83
  @flash ||= Flash.new(session[Flash::KEY])
46
84
  end
47
85
 
86
+ # @since 2.0.0
87
+ # @api private
48
88
  def accept?(mime_type)
49
89
  !!::Rack::Utils.q_values(accept).find do |mime, _|
50
90
  ::Rack::Mime.match?(mime_type, mime)
51
91
  end
52
92
  end
53
93
 
94
+ # @since 2.0.0
95
+ # @api private
54
96
  def accept_header?
55
97
  accept != Action::DEFAULT_ACCEPT
56
98
  end