hanami-controller 2.3.0.beta1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +4 -4
- data/hanami-controller.gemspec +1 -1
- data/lib/hanami/action/config/formats.rb +153 -78
- data/lib/hanami/action/config.rb +12 -1
- data/lib/hanami/action/mime.rb +202 -103
- data/lib/hanami/action/response.rb +3 -3
- data/lib/hanami/action.rb +12 -3
- data/lib/hanami/controller/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 194fc2615c1034ce507513f72e4f218a45fafe60ad53823e0b04e07cae0469f6
|
4
|
+
data.tar.gz: fff5370bd197844f24e83d88490a950fac8cb44c1683c489da94cbb8b5d92402
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f61ceab1aba8a83b459e1c07698802f45fc27f5852454578a4cb135cf520bcf6c12a9a70ae90763dbdb0f1e0dde927da3eb67f071045344c44bb32bc86c8981a
|
7
|
+
data.tar.gz: f2d5905a9d93d09b4a328dbc27d564226293f89a54bb23155b5bd2ececfece6fe768a58d9e3473a228659e790c83018ee90b9f0b1ce1d783bba82679351abc1c
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,79 @@
|
|
2
2
|
|
3
3
|
Complete, fast and testable actions for Rack
|
4
4
|
|
5
|
+
## v2.3.0.beta2 - 2025-10-17
|
6
|
+
|
7
|
+
### Added
|
8
|
+
|
9
|
+
- [Tim Riley] Make format config more flexible. (#485)
|
10
|
+
|
11
|
+
**Use `config.formats.register` to register a new format and its media types.**
|
12
|
+
|
13
|
+
This replaces `config.formats.add`. Unlike `.add` it does _not_ set the format as being one of the accpeted formats at the same time.
|
14
|
+
|
15
|
+
This change makes it easier to `register` your custom formats in app config or a base action class, without inadvertently causing format restrictions in descendent action classes.
|
16
|
+
|
17
|
+
A simple registration looks like this:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
config.formats.register(:json, "application/json")
|
21
|
+
```
|
22
|
+
|
23
|
+
`.register` also allows you to register one or more media types for the distinct stages of request processing:
|
24
|
+
|
25
|
+
- If you want to accept requests based on different/additional media types in `Accept` request headers, provide them as `accept_types:`
|
26
|
+
- If you want to accept requests based on different/additional media types in `Content-Type` request headers, provide them as `content_types:`
|
27
|
+
- If you do not provide these options, then the _default_ media type (the required second argument, after the format name) is used for each of the above
|
28
|
+
- This default media type is also set as the default `Content-Type` _response_ header for requests that match the format
|
29
|
+
|
30
|
+
Together, these allow you to register a format like this:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
config.formats.register(
|
34
|
+
:jsonapi,
|
35
|
+
"application/vnd.api+json",
|
36
|
+
accept_types: ["application/vnd.api+json", "application/json"],
|
37
|
+
content_types: ["application/vnd.api+json", "application/json"],
|
38
|
+
)
|
39
|
+
```
|
40
|
+
|
41
|
+
**Use `config.formats.accept` to accept specific formats from an action.**
|
42
|
+
|
43
|
+
`formats.accept` replaces `Action.format` and `config.format`. You can access your accepted formats via `formats.accepted`, which replaces `config.formats.values`.
|
44
|
+
|
45
|
+
To accept a format:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
config.formats.accept :html, :json
|
49
|
+
config.formats.accepted # => [:html, :json]
|
50
|
+
|
51
|
+
config.formats.accept :csv # it is additive
|
52
|
+
config.formats.accepted # => [:html, :json, :csv]
|
53
|
+
```
|
54
|
+
|
55
|
+
The first format you give to `accept` will also become the _default format_ for responses from your action.
|
56
|
+
|
57
|
+
**Use config.formats.default=` to set an action's default format.**
|
58
|
+
|
59
|
+
This is a new capability. Assign an action's default format using `config.formats.default=`.
|
60
|
+
|
61
|
+
The default format is used to set the response `Content-Type` header when the request does not specify a format via `Accept`.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
config.formats.accept :html, :json
|
65
|
+
|
66
|
+
# When no default is already set, the first accepted format becomes default
|
67
|
+
config.formats.default # => :html
|
68
|
+
|
69
|
+
# But you can now configure this directly
|
70
|
+
config.formats.default = :json
|
71
|
+
```
|
72
|
+
|
73
|
+
### Changed
|
74
|
+
|
75
|
+
- [Tim Riley] `Action.format`, `config.format`, `config.formats.add`, `config.formats.values`, and `config.formats.values=` are deprecated and will be removed in Hanami 2.4. (#485)
|
76
|
+
- [Tim Riley] Drop support for Ruby 3.1.
|
77
|
+
|
5
78
|
## v2.3.0.beta1 - 2025-10-03
|
6
79
|
|
7
80
|
### Fixed
|
data/README.md
CHANGED
@@ -135,10 +135,10 @@ action = Show.new(configuration: configuration)
|
|
135
135
|
response = action.call(id: 23, key: "value")
|
136
136
|
```
|
137
137
|
|
138
|
-
####
|
138
|
+
#### Allowlisting
|
139
139
|
|
140
140
|
Params represent an untrusted input.
|
141
|
-
For security reasons it's recommended to
|
141
|
+
For security reasons it's recommended to allowlist them.
|
142
142
|
|
143
143
|
```ruby
|
144
144
|
require "hanami/validations"
|
@@ -162,11 +162,11 @@ class Signup < Hanami::Action
|
|
162
162
|
puts request.params.class # => Signup::Params
|
163
163
|
puts request.params.class.superclass # => Hanami::Action::Params
|
164
164
|
|
165
|
-
#
|
165
|
+
# Allowlist :first_name, but not :admin
|
166
166
|
puts request.params[:first_name] # => "Luca"
|
167
167
|
puts request.params[:admin] # => nil
|
168
168
|
|
169
|
-
#
|
169
|
+
# Allowlist nested params [:address][:line_one], not [:address][:line_two]
|
170
170
|
puts request.params[:address][:line_one] # => "69 Tender St"
|
171
171
|
puts request.params[:address][:line_two] # => nil
|
172
172
|
end
|
data/hanami-controller.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = []
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
spec.metadata["rubygems_mfa_required"] = "true"
|
21
|
-
spec.required_ruby_version = ">= 3.
|
21
|
+
spec.required_ruby_version = ">= 3.2"
|
22
22
|
|
23
23
|
spec.add_dependency "rack", ">= 2.1"
|
24
24
|
spec.add_dependency "hanami-utils", "~> 2.3.0.beta1"
|
@@ -11,35 +11,57 @@ module Hanami
|
|
11
11
|
# @since 2.0.0
|
12
12
|
# @api private
|
13
13
|
class Formats
|
14
|
-
include Dry.Equalizer(:
|
14
|
+
include Dry.Equalizer(:accepted, :mapping)
|
15
15
|
|
16
|
-
# Default MIME type to format mapping
|
17
|
-
#
|
18
16
|
# @since 2.0.0
|
19
17
|
# @api private
|
20
|
-
|
21
|
-
"application/octet-stream" => :all,
|
22
|
-
"*/*" => :all
|
23
|
-
}.freeze
|
18
|
+
attr_reader :mapping
|
24
19
|
|
20
|
+
# The array of formats to accept requests by.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# config.formats.accepted = [:html, :json]
|
24
|
+
# config.formats.accepted # => [:html, :json]
|
25
|
+
#
|
25
26
|
# @since 2.0.0
|
26
|
-
# @api
|
27
|
-
attr_reader :
|
27
|
+
# @api public
|
28
|
+
attr_reader :accepted
|
29
|
+
|
30
|
+
# @see #accepted
|
31
|
+
#
|
32
|
+
# @since 2.0.0
|
33
|
+
# @api public
|
34
|
+
def values
|
35
|
+
msg = <<~TEXT
|
36
|
+
Hanami::Action `config.formats.values` is deprecated and will be removed in Hanami 2.4.
|
37
|
+
|
38
|
+
Please use `config.formats.accepted` instead.
|
39
|
+
|
40
|
+
See https://guides.hanamirb.org/v2.3/actions/formats-and-mime-types/ for details.
|
41
|
+
TEXT
|
42
|
+
warn(msg, category: :deprecated)
|
43
|
+
|
44
|
+
accepted
|
45
|
+
end
|
28
46
|
|
29
|
-
#
|
47
|
+
# Returns the default format name.
|
48
|
+
#
|
49
|
+
# When a request is received that cannot
|
50
|
+
#
|
51
|
+
# @return [Symbol, nil] the default format name, if any
|
30
52
|
#
|
31
53
|
# @example
|
32
|
-
# config.formats.
|
33
|
-
# config.formats.values # => [:html, :json]
|
54
|
+
# @config.formats.default # => :json
|
34
55
|
#
|
35
56
|
# @since 2.0.0
|
36
57
|
# @api public
|
37
|
-
attr_reader :
|
58
|
+
attr_reader :default
|
38
59
|
|
39
60
|
# @since 2.0.0
|
40
61
|
# @api private
|
41
|
-
def initialize(
|
42
|
-
@
|
62
|
+
def initialize(accepted: [], default: nil, mapping: {})
|
63
|
+
@accepted = accepted
|
64
|
+
@default = default
|
43
65
|
@mapping = mapping
|
44
66
|
end
|
45
67
|
|
@@ -47,15 +69,74 @@ module Hanami
|
|
47
69
|
# @api private
|
48
70
|
private def initialize_copy(original) # rubocop:disable Style/AccessModifierDeclarations
|
49
71
|
super
|
50
|
-
@
|
72
|
+
@accepted = original.accepted.dup
|
73
|
+
@default = original.default
|
51
74
|
@mapping = original.mapping.dup
|
52
75
|
end
|
53
76
|
|
77
|
+
# !@attribute [w] accepted
|
78
|
+
# @since 2.3.0
|
79
|
+
# @api public
|
80
|
+
def accepted=(formats)
|
81
|
+
@accepted = formats.map { |f| Hanami::Utils::Kernel.Symbol(f) }
|
82
|
+
end
|
83
|
+
|
54
84
|
# !@attribute [w] values
|
55
85
|
# @since 2.0.0
|
56
86
|
# @api public
|
57
|
-
|
58
|
-
|
87
|
+
alias_method :values=, :accepted=
|
88
|
+
|
89
|
+
# @since 2.3.0
|
90
|
+
def accept(*formats)
|
91
|
+
self.default = formats.first if default.nil?
|
92
|
+
self.accepted = accepted | formats
|
93
|
+
end
|
94
|
+
|
95
|
+
# @api private
|
96
|
+
def accepted_formats(standard_formats = {})
|
97
|
+
accepted.to_h { |format|
|
98
|
+
[
|
99
|
+
format,
|
100
|
+
mapping.fetch(format) { standard_formats[format] }
|
101
|
+
]
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
# @since 2.3.0
|
106
|
+
def default=(format)
|
107
|
+
@default = format.to_sym
|
108
|
+
end
|
109
|
+
|
110
|
+
# Registers a format and its associated media types.
|
111
|
+
#
|
112
|
+
# @param format [Symbol] the format name
|
113
|
+
# @param media_type [String] the format's media type
|
114
|
+
# @param accept_types [Array<String>] media types to accept in request `Accept` headers
|
115
|
+
# @param content_types [Array<String>] media types to accept in request `Content-Type` headers
|
116
|
+
#
|
117
|
+
# @example
|
118
|
+
# config.formats.register(:scim, media_type: "application/json+scim")
|
119
|
+
#
|
120
|
+
# config.formats.register(
|
121
|
+
# :jsonapi,
|
122
|
+
# "application/vnd.api+json",
|
123
|
+
# accept_types: ["application/vnd.api+json", "application/json"],
|
124
|
+
# content_types: ["application/vnd.api+json", "application/json"]
|
125
|
+
# )
|
126
|
+
#
|
127
|
+
# @return [self]
|
128
|
+
#
|
129
|
+
# @since 2.3.0
|
130
|
+
# @api public
|
131
|
+
def register(format, media_type, accept_types: [media_type], content_types: [media_type])
|
132
|
+
mapping[format] = Mime::Format.new(
|
133
|
+
name: format.to_sym,
|
134
|
+
media_type: media_type,
|
135
|
+
accept_types: accept_types,
|
136
|
+
content_types: content_types
|
137
|
+
)
|
138
|
+
|
139
|
+
self
|
59
140
|
end
|
60
141
|
|
61
142
|
# @overload add(format)
|
@@ -83,20 +164,30 @@ module Hanami
|
|
83
164
|
# @param mime_types [Array<String>]
|
84
165
|
#
|
85
166
|
# @example
|
86
|
-
# config.formats.add(:json, ["application/json+scim"
|
167
|
+
# config.formats.add(:json, ["application/json+scim"])
|
87
168
|
#
|
88
169
|
# @return [self]
|
89
170
|
#
|
90
171
|
# @since 2.0.0
|
91
172
|
# @api public
|
92
|
-
def add(format, mime_types
|
93
|
-
|
173
|
+
def add(format, mime_types)
|
174
|
+
msg = <<~TEXT
|
175
|
+
Hanami::Action `config.formats.add` is deprecated and will be removed in Hanami 2.4.
|
176
|
+
|
177
|
+
Please use `config.formats.register` instead.
|
178
|
+
|
179
|
+
See https://guides.hanamirb.org/v2.3/actions/formats-and-mime-types/ for details.
|
180
|
+
TEXT
|
181
|
+
warn(msg, category: :deprecated)
|
182
|
+
|
183
|
+
mime_type = Array(mime_types).first
|
94
184
|
|
95
|
-
|
96
|
-
|
97
|
-
end
|
185
|
+
# The old behaviour would have subsequent mime types _replacing_ previous ones
|
186
|
+
mapping.reject! { |_, format| format.media_type == mime_type }
|
98
187
|
|
99
|
-
|
188
|
+
register(format, Array(mime_types).first, accept_types: mime_types)
|
189
|
+
|
190
|
+
accept(format) unless @accepted.include?(format)
|
100
191
|
|
101
192
|
self
|
102
193
|
end
|
@@ -104,31 +195,19 @@ module Hanami
|
|
104
195
|
# @since 2.0.0
|
105
196
|
# @api private
|
106
197
|
def empty?
|
107
|
-
|
198
|
+
accepted.empty?
|
108
199
|
end
|
109
200
|
|
110
201
|
# @since 2.0.0
|
111
202
|
# @api private
|
112
203
|
def any?
|
113
|
-
@
|
204
|
+
@accepted.any?
|
114
205
|
end
|
115
206
|
|
116
207
|
# @since 2.0.0
|
117
208
|
# @api private
|
118
209
|
def map(&blk)
|
119
|
-
@
|
120
|
-
end
|
121
|
-
|
122
|
-
# @since 2.0.0
|
123
|
-
# @api private
|
124
|
-
def mapping=(mappings)
|
125
|
-
@mapping = {}
|
126
|
-
|
127
|
-
mappings.each do |format_name, mime_types|
|
128
|
-
Array(mime_types).each do |mime_type|
|
129
|
-
add(format_name, mime_type)
|
130
|
-
end
|
131
|
-
end
|
210
|
+
@accepted.map(&blk)
|
132
211
|
end
|
133
212
|
|
134
213
|
# Clears any previously added mappings and format values.
|
@@ -138,15 +217,24 @@ module Hanami
|
|
138
217
|
# @since 2.0.0
|
139
218
|
# @api public
|
140
219
|
def clear
|
141
|
-
@
|
142
|
-
@
|
220
|
+
@accepted = []
|
221
|
+
@default = nil
|
222
|
+
@mapping = {}
|
143
223
|
|
144
224
|
self
|
145
225
|
end
|
146
226
|
|
147
|
-
#
|
227
|
+
# Returns an array of all accepted media types.
|
228
|
+
#
|
229
|
+
# @since 2.3.0
|
230
|
+
# @api public
|
231
|
+
def accept_types
|
232
|
+
accepted.map { |format| mapping[format]&.accept_types }.flatten(1).compact
|
233
|
+
end
|
234
|
+
|
235
|
+
# Retrieve the format name associated with the given media type
|
148
236
|
#
|
149
|
-
# @param
|
237
|
+
# @param media_type [String] the media Type
|
150
238
|
#
|
151
239
|
# @return [Symbol,NilClass] the associated format name, if any
|
152
240
|
#
|
@@ -157,59 +245,46 @@ module Hanami
|
|
157
245
|
#
|
158
246
|
# @since 2.0.0
|
159
247
|
# @api public
|
160
|
-
def format_for(
|
161
|
-
|
248
|
+
def format_for(media_type)
|
249
|
+
mapping.values.reverse.find { |format| format.media_type == media_type }&.name
|
162
250
|
end
|
163
251
|
|
164
|
-
# Returns the
|
252
|
+
# Returns the media type associated with the given format.
|
165
253
|
#
|
166
254
|
# @param format [Symbol] the format name
|
167
255
|
#
|
168
|
-
# @return [String, nil] the associated
|
256
|
+
# @return [String, nil] the associated media type, if any
|
169
257
|
#
|
170
258
|
# @example
|
171
|
-
# @config.formats.
|
259
|
+
# @config.formats.media_type_for(:json) # => "application/json"
|
172
260
|
#
|
173
261
|
# @see #format_for
|
174
262
|
#
|
175
|
-
# @since 2.
|
263
|
+
# @since 2.3.0
|
176
264
|
# @api public
|
177
|
-
def
|
178
|
-
|
265
|
+
def media_type_for(format)
|
266
|
+
mapping[format]&.media_type
|
179
267
|
end
|
180
268
|
|
181
|
-
#
|
182
|
-
|
183
|
-
|
184
|
-
#
|
185
|
-
# @param format [Symbol] the format name
|
186
|
-
#
|
187
|
-
# @return [Array<String>] the associated MIME types
|
188
|
-
#
|
189
|
-
# @since 2.0.0
|
190
|
-
# @api public
|
191
|
-
def mime_types_for(format)
|
192
|
-
@mapping.each_with_object([]) { |(mime_type, f), arr| arr << mime_type if format == f }
|
269
|
+
# @api private
|
270
|
+
def accept_types_for(format)
|
271
|
+
mapping[format]&.accept_types || []
|
193
272
|
end
|
194
273
|
|
195
|
-
#
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
#
|
201
|
-
#
|
274
|
+
# @api private
|
275
|
+
def content_types_for(format)
|
276
|
+
mapping[format]&.content_types || []
|
277
|
+
end
|
278
|
+
|
279
|
+
# @see #media_type_for
|
202
280
|
# @since 2.0.0
|
203
281
|
# @api public
|
204
|
-
|
205
|
-
@values.first
|
206
|
-
end
|
282
|
+
alias_method :mime_type_for, :media_type_for
|
207
283
|
|
284
|
+
# @see #media_type_for
|
208
285
|
# @since 2.0.0
|
209
|
-
# @api
|
210
|
-
|
211
|
-
@mapping.keys
|
212
|
-
end
|
286
|
+
# @api public
|
287
|
+
alias_method :mime_types_for, :accept_types_for
|
213
288
|
end
|
214
289
|
end
|
215
290
|
end
|
data/lib/hanami/action/config.rb
CHANGED
@@ -68,7 +68,8 @@ module Hanami
|
|
68
68
|
|
69
69
|
# Sets the format (or formats) for the action.
|
70
70
|
#
|
71
|
-
# To configure custom formats and MIME type mappings, call {Formats#
|
71
|
+
# To configure custom formats and MIME type mappings, call {Formats#register formats.register}
|
72
|
+
# first.
|
72
73
|
#
|
73
74
|
# @example
|
74
75
|
# config.format :html, :json
|
@@ -82,10 +83,20 @@ module Hanami
|
|
82
83
|
# @since 2.0.0
|
83
84
|
# @api public
|
84
85
|
def format(*formats)
|
86
|
+
msg = <<~TEXT
|
87
|
+
Hanami::Action `config.format` is deprecated and will be removed in Hanami 2.4.
|
88
|
+
|
89
|
+
Please use `config.formats.register` and/or `config.formats.accept` instead.
|
90
|
+
|
91
|
+
See https://guides.hanamirb.org/v2.3/actions/formats-and-mime-types/ for details.
|
92
|
+
TEXT
|
93
|
+
warn(msg, category: :deprecated)
|
94
|
+
|
85
95
|
if formats.empty?
|
86
96
|
self.formats.values
|
87
97
|
else
|
88
98
|
self.formats.values = formats
|
99
|
+
self.formats.default = formats.first
|
89
100
|
end
|
90
101
|
end
|
91
102
|
|
data/lib/hanami/action/mime.rb
CHANGED
@@ -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
|
12
|
+
# Most commom media types used for responses
|
12
13
|
#
|
13
14
|
# @since 1.0.0
|
14
|
-
# @api
|
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
|
-
|
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
|
-
#
|
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
|
94
|
-
return if
|
195
|
+
def format_from_media_type(media_type, config)
|
196
|
+
return if media_type.nil?
|
95
197
|
|
96
|
-
|
97
|
-
config.formats.format_for(
|
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
|
-
#
|
206
|
+
# format_and_media_type(:json, config)
|
105
207
|
# # => [:json, "application/json"]
|
106
208
|
#
|
107
|
-
#
|
209
|
+
# format_and_media_type("application/json", config)
|
108
210
|
# # => [:json, "application/json"]
|
109
211
|
#
|
110
212
|
# @example Unknown format name
|
111
|
-
#
|
213
|
+
# format_and_media_type(:unknown, config)
|
112
214
|
# # raises Hanami::Action::UnknownFormatError
|
113
215
|
#
|
114
216
|
# @example Unknown content type
|
115
|
-
#
|
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
|
225
|
+
def format_and_media_type(value, config)
|
125
226
|
case value
|
126
227
|
when Symbol
|
127
|
-
[value,
|
228
|
+
[value, format_to_media_type(value, config)]
|
128
229
|
when String
|
129
|
-
[
|
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
|
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,99 +268,118 @@ module Hanami
|
|
188
268
|
}.compact.max&.format
|
189
269
|
end
|
190
270
|
|
191
|
-
|
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
|
-
|
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
|
225
|
-
|
226
|
-
# to avoid false negatives (e.g., multipart/form-data; boundary=...)
|
227
|
-
media_type = request.media_type
|
281
|
+
def accepted_types(config)
|
282
|
+
return [ANY_TYPE] if config.formats.empty?
|
228
283
|
|
229
|
-
|
284
|
+
config.formats.map { |format| format_to_accept_types(format, config) }.flatten(1)
|
285
|
+
end
|
230
286
|
|
231
|
-
|
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?
|
232
290
|
|
233
|
-
|
291
|
+
FORMATS
|
292
|
+
.fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
|
293
|
+
.accept_types
|
234
294
|
end
|
235
295
|
|
236
|
-
private
|
237
|
-
|
238
|
-
# @since 2.0.0
|
239
296
|
# @api private
|
240
|
-
def
|
241
|
-
|
242
|
-
::Rack::Mime.match?(
|
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)
|
243
300
|
}
|
244
301
|
end
|
245
302
|
|
246
|
-
# @since 2.0.0
|
247
303
|
# @api private
|
248
|
-
def
|
304
|
+
def accepted_content_types(config)
|
249
305
|
return [ANY_TYPE] if config.formats.empty?
|
250
306
|
|
251
|
-
config.formats.map { |format|
|
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
|
252
318
|
end
|
253
319
|
|
254
|
-
# @since 2.0.0
|
255
320
|
# @api private
|
256
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
|
+
|
257
328
|
if request.accept_header?
|
258
|
-
|
259
|
-
|
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
|
260
335
|
|
261
336
|
return content_type if content_type
|
262
337
|
end
|
263
338
|
|
264
339
|
if config.formats.default
|
265
|
-
return
|
340
|
+
return format_to_media_type(config.formats.default, config)
|
266
341
|
end
|
267
342
|
|
268
343
|
Action::DEFAULT_CONTENT_TYPE
|
269
344
|
end
|
270
345
|
|
271
|
-
# @since 2.0.0
|
272
346
|
# @api private
|
273
|
-
def
|
274
|
-
|
275
|
-
|
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)
|
276
358
|
end
|
277
359
|
|
278
|
-
# @since 2.0.0
|
279
360
|
# @api private
|
280
|
-
def
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
284
383
|
end
|
285
384
|
end
|
286
385
|
end
|
@@ -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.
|
45
|
+
r.set_format(Mime.format_from_media_type(r.content_type), config)
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
@@ -134,7 +134,7 @@ module Hanami
|
|
134
134
|
# @since 2.0.0
|
135
135
|
# @api public
|
136
136
|
def format
|
137
|
-
@format ||= Mime.
|
137
|
+
@format ||= Mime.format_from_media_type(content_type, @config)
|
138
138
|
end
|
139
139
|
|
140
140
|
# Sets the format and associated content type for the response.
|
@@ -165,7 +165,7 @@ module Hanami
|
|
165
165
|
# @since 2.0.0
|
166
166
|
# @api public
|
167
167
|
def format=(value)
|
168
|
-
format, content_type = Mime.
|
168
|
+
format, content_type = Mime.format_and_media_type(value, @config)
|
169
169
|
|
170
170
|
self.content_type = Mime.content_type_with_charset(content_type, charset)
|
171
171
|
|
data/lib/hanami/action.rb
CHANGED
@@ -272,6 +272,15 @@ module Hanami
|
|
272
272
|
# @since 2.0.0
|
273
273
|
# @api public
|
274
274
|
def self.format(...)
|
275
|
+
msg = <<~TEXT
|
276
|
+
Hanami::Action `format` is deprecated and will be removed in Hanami 2.4.
|
277
|
+
|
278
|
+
Please use `config.formats.accept` instead.
|
279
|
+
|
280
|
+
See https://guides.hanamirb.org/v2.3/actions/formats-and-mime-types/ for details.
|
281
|
+
TEXT
|
282
|
+
warn(msg, category: :deprecated)
|
283
|
+
|
275
284
|
config.format(...)
|
276
285
|
end
|
277
286
|
|
@@ -326,7 +335,7 @@ module Hanami
|
|
326
335
|
session_enabled: session_enabled?
|
327
336
|
)
|
328
337
|
|
329
|
-
|
338
|
+
enforce_accepted_media_types(request)
|
330
339
|
|
331
340
|
_run_before_callbacks(request, response)
|
332
341
|
handle(request, response)
|
@@ -413,7 +422,7 @@ module Hanami
|
|
413
422
|
|
414
423
|
# @since 2.0.0
|
415
424
|
# @api private
|
416
|
-
def
|
425
|
+
def enforce_accepted_media_types(request)
|
417
426
|
return if config.formats.empty?
|
418
427
|
|
419
428
|
Mime.enforce_accept(request, config) { return halt 406 }
|
@@ -596,7 +605,7 @@ module Hanami
|
|
596
605
|
_empty_headers(res) if _requires_empty_headers?(res)
|
597
606
|
_empty_body(res) if res.head?
|
598
607
|
|
599
|
-
res.set_format(
|
608
|
+
res.set_format(Mime.format_from_media_type(res.content_type, config))
|
600
609
|
res[:params] = req.params
|
601
610
|
res[:format] = res.format
|
602
611
|
res
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hanami-controller
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.3.0.
|
4
|
+
version: 2.3.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luca Guidi
|
@@ -214,7 +214,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
214
214
|
requirements:
|
215
215
|
- - ">="
|
216
216
|
- !ruby/object:Gem::Version
|
217
|
-
version: '3.
|
217
|
+
version: '3.2'
|
218
218
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
219
219
|
requirements:
|
220
220
|
- - ">="
|