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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +3 -9
- data/hanami-controller.gemspec +5 -3
- data/lib/hanami/action/base_params.rb +39 -16
- data/lib/hanami/action/cache/cache_control.rb +0 -2
- data/lib/hanami/action/cache/expires.rb +0 -2
- data/lib/hanami/action/cache.rb +0 -4
- data/lib/hanami/action/config/formats.rb +216 -0
- data/lib/hanami/action/config.rb +24 -113
- data/lib/hanami/action/constants.rb +0 -6
- data/lib/hanami/action/csrf_protection.rb +1 -7
- data/lib/hanami/action/{error.rb → errors.rb} +38 -3
- data/lib/hanami/action/flash.rb +29 -22
- data/lib/hanami/action/halt.rb +3 -1
- data/lib/hanami/action/mime/request_mime_weight.rb +66 -0
- data/lib/hanami/action/mime.rb +179 -210
- data/lib/hanami/action/params.rb +0 -1
- data/lib/hanami/action/rack/file.rb +6 -2
- data/lib/hanami/action/request.rb +46 -4
- data/lib/hanami/action/response.rb +210 -17
- data/lib/hanami/action/session.rb +7 -4
- data/lib/hanami/action/validatable.rb +10 -6
- data/lib/hanami/action.rb +116 -143
- data/lib/hanami/controller/version.rb +5 -2
- data/lib/hanami/controller.rb +2 -30
- data/lib/hanami/http/status.rb +2 -0
- data/lib/hanami-controller.rb +3 -0
- metadata +44 -14
- data/lib/hanami/controller/error.rb +0 -9
data/lib/hanami/action/mime.rb
CHANGED
@@ -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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
188
|
-
|
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
|
-
|
191
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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
|
-
|
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
|
-
|
209
|
+
yield
|
210
|
+
end
|
212
211
|
|
213
|
-
|
214
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
263
|
-
|
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
|
-
|
268
|
-
|
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
|
-
|
277
|
-
|
278
|
-
attr_reader :index
|
249
|
+
config.formats.map { |format| format_to_mime_types(format, config) }.flatten(1)
|
250
|
+
end
|
279
251
|
|
280
|
-
# @since
|
252
|
+
# @since 2.0.0
|
281
253
|
# @api private
|
282
|
-
|
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
|
-
|
285
|
-
|
286
|
-
attr_reader :format
|
259
|
+
return content_type if content_type
|
260
|
+
end
|
287
261
|
|
288
|
-
|
289
|
-
|
290
|
-
|
262
|
+
if config.formats.default
|
263
|
+
return format_to_mime_type(config.formats.default, config)
|
264
|
+
end
|
291
265
|
|
292
|
-
|
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
|
269
|
+
# @since 2.0.0
|
300
270
|
# @api private
|
301
|
-
def
|
302
|
-
|
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
|
-
|
308
|
-
|
309
|
-
# @since 1.0.1
|
276
|
+
# @since 2.0.0
|
310
277
|
# @api private
|
311
|
-
def
|
312
|
-
|
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
|
data/lib/hanami/action/params.rb
CHANGED
@@ -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
|
-
#
|
12
|
-
# This guarantees backwards compatibility with with Rack.
|
11
|
+
# The HTTP request for an action, given to {Action#handle}.
|
13
12
|
#
|
14
|
-
#
|
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
|