hanami-controller 2.0.0.beta4 → 2.0.0

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