hanami-controller 2.2.0 → 2.3.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -0
- data/README.md +87 -88
- data/hanami-controller.gemspec +3 -3
- data/lib/hanami/action/cache/cache_control.rb +1 -0
- data/lib/hanami/action/cache/conditional_get.rb +1 -1
- data/lib/hanami/action/cache/expires.rb +1 -0
- data/lib/hanami/action/config/formats.rb +153 -78
- data/lib/hanami/action/config.rb +30 -1
- data/lib/hanami/action/constants.rb +8 -8
- data/lib/hanami/action/errors.rb +1 -1
- data/lib/hanami/action/mime.rb +202 -101
- data/lib/hanami/action/params.rb +1 -7
- data/lib/hanami/action/rack/file.rb +2 -2
- data/lib/hanami/action/rack_utils.rb +11 -0
- data/lib/hanami/action/request/session.rb +68 -0
- data/lib/hanami/action/request.rb +29 -3
- data/lib/hanami/action/response.rb +8 -5
- data/lib/hanami/action.rb +17 -5
- data/lib/hanami/controller/version.rb +1 -1
- metadata +12 -13
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,97 +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
|
-
|
|
281
|
+
def accepted_types(config)
|
|
282
|
+
return [ANY_TYPE] if config.formats.empty?
|
|
226
283
|
|
|
227
|
-
|
|
284
|
+
config.formats.map { |format| format_to_accept_types(format, config) }.flatten(1)
|
|
285
|
+
end
|
|
228
286
|
|
|
229
|
-
|
|
287
|
+
def format_to_accept_types(format, config)
|
|
288
|
+
configured_types = config.formats.accept_types_for(format)
|
|
289
|
+
return configured_types if configured_types.any?
|
|
230
290
|
|
|
231
|
-
|
|
291
|
+
FORMATS
|
|
292
|
+
.fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
|
|
293
|
+
.accept_types
|
|
232
294
|
end
|
|
233
295
|
|
|
234
|
-
private
|
|
235
|
-
|
|
236
|
-
# @since 2.0.0
|
|
237
296
|
# @api private
|
|
238
|
-
def
|
|
239
|
-
|
|
240
|
-
::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)
|
|
241
300
|
}
|
|
242
301
|
end
|
|
243
302
|
|
|
244
|
-
# @since 2.0.0
|
|
245
303
|
# @api private
|
|
246
|
-
def
|
|
304
|
+
def accepted_content_types(config)
|
|
247
305
|
return [ANY_TYPE] if config.formats.empty?
|
|
248
306
|
|
|
249
|
-
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
|
|
250
318
|
end
|
|
251
319
|
|
|
252
|
-
# @since 2.0.0
|
|
253
320
|
# @api private
|
|
254
321
|
def response_content_type(request, config)
|
|
322
|
+
# This method prepares the default `Content-Type` for the response. Importantly, it only
|
|
323
|
+
# does this after `#enforce_accept` and `#enforce_content_type` have already passed the
|
|
324
|
+
# request. So by the time we get here, the request has been deemed acceptable to the
|
|
325
|
+
# action, so we can try to be as helpful as possible in setting an appropriate content
|
|
326
|
+
# type for the response.
|
|
327
|
+
|
|
255
328
|
if request.accept_header?
|
|
256
|
-
|
|
257
|
-
|
|
329
|
+
content_type =
|
|
330
|
+
if config.formats.empty? || config.formats.accepted.include?(:all)
|
|
331
|
+
permissive_response_content_type(request, config)
|
|
332
|
+
else
|
|
333
|
+
restrictive_response_content_type(request, config)
|
|
334
|
+
end
|
|
258
335
|
|
|
259
336
|
return content_type if content_type
|
|
260
337
|
end
|
|
261
338
|
|
|
262
339
|
if config.formats.default
|
|
263
|
-
return
|
|
340
|
+
return format_to_media_type(config.formats.default, config)
|
|
264
341
|
end
|
|
265
342
|
|
|
266
343
|
Action::DEFAULT_CONTENT_TYPE
|
|
267
344
|
end
|
|
268
345
|
|
|
269
|
-
# @since 2.0.0
|
|
270
346
|
# @api private
|
|
271
|
-
def
|
|
272
|
-
|
|
273
|
-
|
|
347
|
+
def permissive_response_content_type(request, config)
|
|
348
|
+
# If no accepted formats are configured, or if the formats include :all, then we're
|
|
349
|
+
# working with a "permissive" action. In this case we simply want a response content type
|
|
350
|
+
# that corresponds to the request's accept header as closely as possible. This means we
|
|
351
|
+
# work from _all_ the media types we know of.
|
|
352
|
+
|
|
353
|
+
all_media_types =
|
|
354
|
+
(ACCEPT_TYPES_TO_FORMATS.keys | MEDIA_TYPES_TO_FORMATS.keys) +
|
|
355
|
+
config.formats.accept_types
|
|
356
|
+
|
|
357
|
+
best_q_match(request.accept, all_media_types)
|
|
274
358
|
end
|
|
275
359
|
|
|
276
|
-
# @since 2.0.0
|
|
277
360
|
# @api private
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
361
|
+
def restrictive_response_content_type(request, config)
|
|
362
|
+
# When specific formats are configured, this is a "resitrctive" action. Here we want to
|
|
363
|
+
# match against the configured accept types only, and work back from those to the
|
|
364
|
+
# configured format, so we can use its canonical media type for the content type.
|
|
365
|
+
|
|
366
|
+
accept_types_to_formats = config.formats.accepted_formats(FORMATS)
|
|
367
|
+
.each_with_object({}) { |(_, format), hsh|
|
|
368
|
+
format.accept_types.each { hsh[_1] = format }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
accept_type = best_q_match(request.accept, accept_types_to_formats.keys)
|
|
372
|
+
accept_types_to_formats[accept_type].media_type if accept_type
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# @api private
|
|
376
|
+
def format_to_media_type(format, config)
|
|
377
|
+
configured_type = config.formats.media_type_for(format)
|
|
378
|
+
return configured_type if configured_type
|
|
379
|
+
|
|
380
|
+
FORMATS
|
|
381
|
+
.fetch(format) { raise Hanami::Action::UnknownFormatError.new(format) }
|
|
382
|
+
.media_type
|
|
282
383
|
end
|
|
283
384
|
end
|
|
284
385
|
end
|
data/lib/hanami/action/params.rb
CHANGED
|
@@ -362,13 +362,7 @@ module Hanami
|
|
|
362
362
|
# @since 0.7.0
|
|
363
363
|
# @api private
|
|
364
364
|
def _router_params(fallback = {})
|
|
365
|
-
env.fetch(ROUTER_PARAMS)
|
|
366
|
-
if session = fallback.delete(Action::RACK_SESSION)
|
|
367
|
-
fallback[Action::RACK_SESSION] = Utils::Hash.deep_symbolize(session)
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
fallback
|
|
371
|
-
end
|
|
365
|
+
env.fetch(ROUTER_PARAMS, fallback)
|
|
372
366
|
end
|
|
373
367
|
end
|
|
374
368
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "rack/
|
|
3
|
+
require "rack/files"
|
|
4
4
|
|
|
5
5
|
module Hanami
|
|
6
6
|
class Action
|
|
@@ -21,7 +21,7 @@ module Hanami
|
|
|
21
21
|
# @since 0.4.3
|
|
22
22
|
# @api private
|
|
23
23
|
def initialize(path, root)
|
|
24
|
-
@file = ::Rack::
|
|
24
|
+
@file = ::Rack::Files.new(root.to_s)
|
|
25
25
|
@path = path.to_s
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
class Action
|
|
7
|
+
class Request < ::Rack::Request
|
|
8
|
+
# Wrapper for Rack-provided sessions, allowing access using symbol keys.
|
|
9
|
+
#
|
|
10
|
+
# @since 2.3.0
|
|
11
|
+
# @api public
|
|
12
|
+
class Session
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
15
|
+
def_delegators \
|
|
16
|
+
:@session,
|
|
17
|
+
:clear,
|
|
18
|
+
:delete,
|
|
19
|
+
:empty?,
|
|
20
|
+
:size,
|
|
21
|
+
:length,
|
|
22
|
+
:each,
|
|
23
|
+
:to_h,
|
|
24
|
+
:inspect,
|
|
25
|
+
:keys,
|
|
26
|
+
:values
|
|
27
|
+
|
|
28
|
+
def initialize(session)
|
|
29
|
+
@session = session
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def [](key)
|
|
33
|
+
@session[key.to_s]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def []=(key, value)
|
|
37
|
+
@session[key.to_s] = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def key?(key)
|
|
41
|
+
@session.key?(key.to_s)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias_method :has_key?, :key?
|
|
45
|
+
alias_method :include?, :key?
|
|
46
|
+
|
|
47
|
+
def ==(other)
|
|
48
|
+
Utils::Hash.deep_symbolize(@session) == Utils::Hash.deep_symbolize(other)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Provides a fallback for any methods not handled by the def_delegators.
|
|
54
|
+
def method_missing(method_name, *args, &block)
|
|
55
|
+
if @session.respond_to?(method_name)
|
|
56
|
+
@session.send(method_name, *args, &block)
|
|
57
|
+
else
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
63
|
+
@session.respond_to?(method_name, include_private) || super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -26,11 +26,12 @@ module Hanami
|
|
|
26
26
|
|
|
27
27
|
# @since 2.0.0
|
|
28
28
|
# @api private
|
|
29
|
-
def initialize(env:, params:, session_enabled: false)
|
|
29
|
+
def initialize(env:, params:, default_tld_length: 1, session_enabled: false)
|
|
30
30
|
super(env)
|
|
31
31
|
|
|
32
32
|
@params = params
|
|
33
33
|
@session_enabled = session_enabled
|
|
34
|
+
@default_tld_length = default_tld_length
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Returns the request's ID
|
|
@@ -56,7 +57,7 @@ module Hanami
|
|
|
56
57
|
|
|
57
58
|
# Returns the session for the request.
|
|
58
59
|
#
|
|
59
|
-
# @return [
|
|
60
|
+
# @return [Hanami::Request::Session] the session object
|
|
60
61
|
#
|
|
61
62
|
# @raise [MissingSessionError] if the session is not enabled
|
|
62
63
|
#
|
|
@@ -70,7 +71,7 @@ module Hanami
|
|
|
70
71
|
raise Hanami::Action::MissingSessionError.new("Hanami::Action::Request#session")
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
super
|
|
74
|
+
@session ||= Session.new(super)
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
# Returns the flash for the request.
|
|
@@ -91,6 +92,31 @@ module Hanami
|
|
|
91
92
|
@flash ||= Flash.new(session[Flash::KEY])
|
|
92
93
|
end
|
|
93
94
|
|
|
95
|
+
# Returns the subdomains for the current host.
|
|
96
|
+
#
|
|
97
|
+
# @return [Array<String>]
|
|
98
|
+
#
|
|
99
|
+
# @api public
|
|
100
|
+
# @since 2.3.0
|
|
101
|
+
def subdomains(tld_length = @default_tld_length)
|
|
102
|
+
return [] if IP_ADDRESS_HOST_REGEXP.match?(host)
|
|
103
|
+
|
|
104
|
+
host.split(".")[0..-(tld_length + 2)]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
IP_ADDRESS_HOST_REGEXP = /\A\d+\.\d+\.\d+\.\d+\z/
|
|
108
|
+
private_constant :IP_ADDRESS_HOST_REGEXP
|
|
109
|
+
|
|
110
|
+
# Returns the subdomain for the current host.
|
|
111
|
+
#
|
|
112
|
+
# @return [String]
|
|
113
|
+
#
|
|
114
|
+
# @api public
|
|
115
|
+
# @since 2.3.0
|
|
116
|
+
def subdomain(tld_length = @default_tld_length)
|
|
117
|
+
subdomains(tld_length).join(".")
|
|
118
|
+
end
|
|
119
|
+
|
|
94
120
|
# @since 2.0.0
|
|
95
121
|
# @api private
|
|
96
122
|
def accept?(mime_type)
|
|
@@ -42,7 +42,7 @@ module Hanami
|
|
|
42
42
|
new(config: Action.config.dup, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r|
|
|
43
43
|
r.status = status
|
|
44
44
|
r.body = Http::Status.message_for(status)
|
|
45
|
-
r.set_format(Mime.
|
|
45
|
+
r.set_format(Mime.format_from_media_type(r.content_type), config)
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -71,12 +71,15 @@ module Hanami
|
|
|
71
71
|
# @api public
|
|
72
72
|
def body=(str)
|
|
73
73
|
@length = 0
|
|
74
|
-
@body
|
|
74
|
+
@body = EMPTY_BODY.dup
|
|
75
|
+
|
|
76
|
+
return if str.nil? || str == EMPTY_BODY
|
|
75
77
|
|
|
76
78
|
if str.is_a?(::Rack::Files::BaseIterator)
|
|
77
79
|
@body = str
|
|
80
|
+
buffered_body! # Ensure appropriate content-length is set
|
|
78
81
|
else
|
|
79
|
-
write(str)
|
|
82
|
+
write(str)
|
|
80
83
|
end
|
|
81
84
|
end
|
|
82
85
|
|
|
@@ -131,7 +134,7 @@ module Hanami
|
|
|
131
134
|
# @since 2.0.0
|
|
132
135
|
# @api public
|
|
133
136
|
def format
|
|
134
|
-
@format ||= Mime.
|
|
137
|
+
@format ||= Mime.format_from_media_type(content_type, @config)
|
|
135
138
|
end
|
|
136
139
|
|
|
137
140
|
# Sets the format and associated content type for the response.
|
|
@@ -162,7 +165,7 @@ module Hanami
|
|
|
162
165
|
# @since 2.0.0
|
|
163
166
|
# @api public
|
|
164
167
|
def format=(value)
|
|
165
|
-
format, content_type = Mime.
|
|
168
|
+
format, content_type = Mime.format_and_media_type(value, @config)
|
|
166
169
|
|
|
167
170
|
self.content_type = Mime.content_type_with_charset(content_type, charset)
|
|
168
171
|
|