hanami-action 3.0.0.rc1
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 +7 -0
- data/CHANGELOG.md +985 -0
- data/LICENSE +20 -0
- data/README.md +873 -0
- data/hanami-action.gemspec +39 -0
- data/lib/hanami/action/body_parser/json.rb +20 -0
- data/lib/hanami/action/body_parser/multipart_form.rb +22 -0
- data/lib/hanami/action/body_parser.rb +109 -0
- data/lib/hanami/action/cache/cache_control.rb +84 -0
- data/lib/hanami/action/cache/conditional_get.rb +101 -0
- data/lib/hanami/action/cache/directives.rb +126 -0
- data/lib/hanami/action/cache/expires.rb +84 -0
- data/lib/hanami/action/cache.rb +29 -0
- data/lib/hanami/action/config/formats.rb +256 -0
- data/lib/hanami/action/config.rb +172 -0
- data/lib/hanami/action/constants.rb +283 -0
- data/lib/hanami/action/cookie_jar.rb +214 -0
- data/lib/hanami/action/cookies.rb +27 -0
- data/lib/hanami/action/csrf_protection.rb +217 -0
- data/lib/hanami/action/errors.rb +109 -0
- data/lib/hanami/action/flash.rb +176 -0
- data/lib/hanami/action/halt.rb +18 -0
- data/lib/hanami/action/mime/request_mime_weight.rb +66 -0
- data/lib/hanami/action/mime.rb +438 -0
- data/lib/hanami/action/params.rb +342 -0
- data/lib/hanami/action/rack/file.rb +41 -0
- data/lib/hanami/action/rack_utils.rb +11 -0
- data/lib/hanami/action/request/session.rb +68 -0
- data/lib/hanami/action/request.rb +141 -0
- data/lib/hanami/action/response.rb +481 -0
- data/lib/hanami/action/session.rb +47 -0
- data/lib/hanami/action/validatable.rb +166 -0
- data/lib/hanami/action/version.rb +13 -0
- data/lib/hanami/action/view_name_inferrer.rb +56 -0
- data/lib/hanami/action.rb +672 -0
- data/lib/hanami/http/status.rb +149 -0
- data/lib/hanami-action.rb +3 -0
- metadata +153 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "rack/response"
|
|
5
|
+
require "hanami/utils/kernel"
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
|
|
8
|
+
module Hanami
|
|
9
|
+
class Action
|
|
10
|
+
# The HTTP response for an action, given to {Action#handle}.
|
|
11
|
+
#
|
|
12
|
+
# Inherits from `Rack::Response`, providing compatibility with Rack functionality.
|
|
13
|
+
#
|
|
14
|
+
# @see http://www.rubydoc.info/gems/rack/Rack/Response
|
|
15
|
+
#
|
|
16
|
+
# @since 2.0.0
|
|
17
|
+
# @api private
|
|
18
|
+
class Response < ::Rack::Response
|
|
19
|
+
# @since 2.0.0
|
|
20
|
+
# @api private
|
|
21
|
+
DEFAULT_VIEW_OPTIONS = -> (*) { {} }.freeze
|
|
22
|
+
|
|
23
|
+
# @since 2.0.0
|
|
24
|
+
# @api private
|
|
25
|
+
EMPTY_BODY = [].freeze
|
|
26
|
+
|
|
27
|
+
# @since 2.0.0
|
|
28
|
+
# @api private
|
|
29
|
+
FILE_SYSTEM_ROOT = Pathname.new("/").freeze
|
|
30
|
+
|
|
31
|
+
# @since 2.0.0
|
|
32
|
+
# @api private
|
|
33
|
+
attr_reader :request, :exposures, :env, :view_options
|
|
34
|
+
|
|
35
|
+
# @since 2.0.0
|
|
36
|
+
# @api private
|
|
37
|
+
attr_accessor :charset
|
|
38
|
+
|
|
39
|
+
# @since 2.0.0
|
|
40
|
+
# @api private
|
|
41
|
+
def self.build(status, env)
|
|
42
|
+
new(config: Action.config.dup, content_type: Mime.best_q_match(env[Action::HTTP_ACCEPT]), env: env).tap do |r|
|
|
43
|
+
r.status = status
|
|
44
|
+
r.body = Http::Status.message_for(status)
|
|
45
|
+
r.set_format(Mime.format_from_media_type(r.content_type), config)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @since 2.0.0
|
|
50
|
+
# @api private
|
|
51
|
+
def initialize(request:, config:, content_type: nil, charset: nil, env: {}, headers: {}, view_options: nil, session_enabled: false) # rubocop:disable Layout/LineLength
|
|
52
|
+
# `Rack::Response#initialize` copies the headers into its own internal store on both Rack
|
|
53
|
+
# 2.2+ (`Utils::HeaderHash[headers]`) and Rack 3.x (`Headers.new` + per-entry copy), so it
|
|
54
|
+
# never aliases the hash passed in. That means we can skip a defensive `headers.dup` here
|
|
55
|
+
# and avoid an allocation per request without risk of pollution. See the regression spec
|
|
56
|
+
# in `spec/unit/hanami/action/response_spec.rb` if you're tempted to add one back.
|
|
57
|
+
super([], 200, headers)
|
|
58
|
+
self.content_type = content_type if content_type
|
|
59
|
+
|
|
60
|
+
@request = request
|
|
61
|
+
@config = config
|
|
62
|
+
@charset = charset
|
|
63
|
+
@exposures = {}
|
|
64
|
+
@env = env
|
|
65
|
+
@view_options = view_options || DEFAULT_VIEW_OPTIONS
|
|
66
|
+
|
|
67
|
+
@session_enabled = session_enabled
|
|
68
|
+
@sending_file = false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Sets the response body.
|
|
72
|
+
#
|
|
73
|
+
# @param str [String] the body string
|
|
74
|
+
#
|
|
75
|
+
# @since 2.0.0
|
|
76
|
+
# @api public
|
|
77
|
+
def body=(str)
|
|
78
|
+
@length = 0
|
|
79
|
+
|
|
80
|
+
if str.nil? || str == EMPTY_BODY
|
|
81
|
+
@body = EMPTY_BODY
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@body = []
|
|
86
|
+
|
|
87
|
+
if str.is_a?(::Rack::Files::BaseIterator)
|
|
88
|
+
@body = str
|
|
89
|
+
buffered_body! # Ensure appropriate content-length is set
|
|
90
|
+
else
|
|
91
|
+
write(str)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Sets the response status.
|
|
96
|
+
#
|
|
97
|
+
# @param code [Integer, Symbol] the status code
|
|
98
|
+
#
|
|
99
|
+
# @since 2.0.2
|
|
100
|
+
# @api public
|
|
101
|
+
#
|
|
102
|
+
# @raise [Hanami::Action::UnknownHttpStatusError] if the given code
|
|
103
|
+
# cannot be associated to a known HTTP status
|
|
104
|
+
#
|
|
105
|
+
# @example
|
|
106
|
+
# response.status = :unprocessable_entity
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# response.status = 422
|
|
110
|
+
#
|
|
111
|
+
# @see https://guides.hanamirb.org/v2.0/actions/status-codes/
|
|
112
|
+
def status=(code)
|
|
113
|
+
super(Http::Status.lookup(code))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Sets the response body from the rendered view.
|
|
117
|
+
#
|
|
118
|
+
# @param view [Hanami::View] the view to render
|
|
119
|
+
# @param input [Hash] keyword arguments to pass to the view's `#call` method
|
|
120
|
+
#
|
|
121
|
+
# @api public
|
|
122
|
+
# @since 2.1.0
|
|
123
|
+
def render(view, **input)
|
|
124
|
+
view_input = {
|
|
125
|
+
**view_options.call(request, self),
|
|
126
|
+
**exposures,
|
|
127
|
+
**input
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
self.body = view.call(**view_input).to_str
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns the format for the response.
|
|
134
|
+
#
|
|
135
|
+
# Returns nil if a format has not been assigned and also cannot be determined from the
|
|
136
|
+
# response's `#content_type`.
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# response.format # => :json
|
|
140
|
+
#
|
|
141
|
+
# @return [Symbol, nil]
|
|
142
|
+
#
|
|
143
|
+
# @since 2.0.0
|
|
144
|
+
# @api public
|
|
145
|
+
def format
|
|
146
|
+
@format ||= Mime.format_from_media_type(content_type, @config)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Sets the format and associated content type for the response.
|
|
150
|
+
#
|
|
151
|
+
# Either a format name (`:json`) or a MIME type (`"application/json"`) may be given. In either
|
|
152
|
+
# case, the format or content type will be derived from the given value, and both will be set.
|
|
153
|
+
#
|
|
154
|
+
# Providing an unknown format name will raise an {Hanami::Action::UnknownFormatError}.
|
|
155
|
+
#
|
|
156
|
+
# Providing an unknown MIME type will set the content type and set the format as nil.
|
|
157
|
+
#
|
|
158
|
+
# @example Assigning via a format name symbol
|
|
159
|
+
# response.format = :json
|
|
160
|
+
# response.content_type # => "application/json"
|
|
161
|
+
# response.headers["Content-Type"] # => "application/json"
|
|
162
|
+
#
|
|
163
|
+
# @example Assigning via a content type string
|
|
164
|
+
# response.format = "application/json"
|
|
165
|
+
# response.format # => :json
|
|
166
|
+
# response.content_type # => "application/json"
|
|
167
|
+
#
|
|
168
|
+
# @param value [Symbol, String] the format name or content type
|
|
169
|
+
#
|
|
170
|
+
# @raise [Hanami::Action::UnknownFormatError] if an unknown format name is given
|
|
171
|
+
#
|
|
172
|
+
# @see Config#formats
|
|
173
|
+
#
|
|
174
|
+
# @since 2.0.0
|
|
175
|
+
# @api public
|
|
176
|
+
def format=(value)
|
|
177
|
+
format, content_type = Mime.format_and_media_type(value, @config)
|
|
178
|
+
|
|
179
|
+
self.content_type = Mime.content_type_with_charset(content_type, charset)
|
|
180
|
+
|
|
181
|
+
@format = format
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns the exposure value for the given key.
|
|
185
|
+
#
|
|
186
|
+
# @param key [Object]
|
|
187
|
+
#
|
|
188
|
+
# @return [Object] the exposure value, if found
|
|
189
|
+
#
|
|
190
|
+
# @raise [KeyError] if the exposure was not found
|
|
191
|
+
#
|
|
192
|
+
# @since 2.0.0
|
|
193
|
+
# @api public
|
|
194
|
+
def [](key)
|
|
195
|
+
@exposures.fetch(key)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Sets an exposure value for the given key.
|
|
199
|
+
#
|
|
200
|
+
# @param key [Object]
|
|
201
|
+
# @param value [Object]
|
|
202
|
+
#
|
|
203
|
+
# @return [Object] the value
|
|
204
|
+
#
|
|
205
|
+
# @since 2.0.0
|
|
206
|
+
# @api public
|
|
207
|
+
def []=(key, value)
|
|
208
|
+
@exposures[key] = value
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Returns true if the session is enabled for the request.
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean]
|
|
214
|
+
#
|
|
215
|
+
# @api public
|
|
216
|
+
# @since 2.1.0
|
|
217
|
+
def session_enabled?
|
|
218
|
+
@session_enabled
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Returns the session for the response.
|
|
222
|
+
#
|
|
223
|
+
# This is the same session object as the {Request}.
|
|
224
|
+
#
|
|
225
|
+
# @return [Hash] the session object
|
|
226
|
+
#
|
|
227
|
+
# @raise [MissingSessionError] if sessions are not enabled
|
|
228
|
+
#
|
|
229
|
+
# @see Request#session
|
|
230
|
+
#
|
|
231
|
+
# @since 2.0.0
|
|
232
|
+
# @api public
|
|
233
|
+
def session
|
|
234
|
+
unless session_enabled?
|
|
235
|
+
raise Hanami::Action::MissingSessionError.new("Hanami::Action::Response#session")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
request.session
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Returns the flash for the request.
|
|
242
|
+
#
|
|
243
|
+
# This is the same flash object as the {Request}.
|
|
244
|
+
#
|
|
245
|
+
# @return [Flash]
|
|
246
|
+
#
|
|
247
|
+
# @raise [MissingSessionError] if sessions are not enabled
|
|
248
|
+
#
|
|
249
|
+
# @see Request#flash
|
|
250
|
+
#
|
|
251
|
+
# @since 2.0.0
|
|
252
|
+
# @api public
|
|
253
|
+
def flash
|
|
254
|
+
unless session_enabled?
|
|
255
|
+
raise Hanami::Action::MissingSessionError.new("Hanami::Action::Response#flash")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
request.flash
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Returns the set of cookies to be included in the response.
|
|
262
|
+
#
|
|
263
|
+
# @return [CookieJar]
|
|
264
|
+
#
|
|
265
|
+
# @since 2.0.0
|
|
266
|
+
# @api public
|
|
267
|
+
def cookies
|
|
268
|
+
@cookies ||= CookieJar.new(env.dup, headers, @config.cookies)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Sets the response to redirect to the given URL and halts further handling.
|
|
272
|
+
#
|
|
273
|
+
# @param url [String]
|
|
274
|
+
# @param status [Integer] the HTTP status to use for the redirect
|
|
275
|
+
#
|
|
276
|
+
# @since 2.0.0
|
|
277
|
+
# @api public
|
|
278
|
+
def redirect_to(url, status: 302)
|
|
279
|
+
return unless allow_redirect?
|
|
280
|
+
|
|
281
|
+
redirect(::String.new(url), status)
|
|
282
|
+
Halt.call(status)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Sends the file at the given path as the response, for any file within the configured
|
|
286
|
+
# `public_directory`.
|
|
287
|
+
#
|
|
288
|
+
# Handles the following aspects for file responses:
|
|
289
|
+
#
|
|
290
|
+
# - Setting `Content-Type` and `Content-Length` headers
|
|
291
|
+
# - File Not Found responses (returns a 404)
|
|
292
|
+
# - Conditional GET (via `If-Modified-Since` header)
|
|
293
|
+
# - Range requests (via `Range` header)
|
|
294
|
+
#
|
|
295
|
+
# @param path [String] the file path
|
|
296
|
+
#
|
|
297
|
+
# @return [void]
|
|
298
|
+
#
|
|
299
|
+
# @see Hanami::Action::Config#public_directory
|
|
300
|
+
# @see Hanami::Action::Rack::File
|
|
301
|
+
#
|
|
302
|
+
# @since 2.0.0
|
|
303
|
+
# @api public
|
|
304
|
+
def send_file(path)
|
|
305
|
+
_send_file(
|
|
306
|
+
Action::Rack::File.new(path, @config.public_directory).call(env)
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Send the file at the given path as the response, for a file anywhere in the file system.
|
|
311
|
+
#
|
|
312
|
+
# @param path [String, Pathname] path to the file to be sent
|
|
313
|
+
#
|
|
314
|
+
# @return [void]
|
|
315
|
+
#
|
|
316
|
+
# @see #send_file
|
|
317
|
+
# @see Hanami::Action::Rack::File
|
|
318
|
+
#
|
|
319
|
+
# @since 2.0.0
|
|
320
|
+
# @api public
|
|
321
|
+
def unsafe_send_file(path)
|
|
322
|
+
directory = if Pathname.new(path).relative?
|
|
323
|
+
@config.root_directory
|
|
324
|
+
else
|
|
325
|
+
FILE_SYSTEM_ROOT
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
_send_file(
|
|
329
|
+
Action::Rack::File.new(path, directory).call(env)
|
|
330
|
+
)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Specifies the response freshness policy for HTTP caches using the `Cache-Control` header.
|
|
334
|
+
#
|
|
335
|
+
# Any number of non-value directives (`:public`, `:private`, `:no_cache`, `:no_store`,
|
|
336
|
+
# `:must_revalidate`, `:proxy_revalidate`) may be passed along with a Hash of value directives
|
|
337
|
+
# (`:max_age`, `:min_stale`, `:s_max_age`).
|
|
338
|
+
#
|
|
339
|
+
# See [RFC 2616 / 14.9](http://tools.ietf.org/html/rfc2616#section-14.9.1) for more on
|
|
340
|
+
# standard cache control directives.
|
|
341
|
+
#
|
|
342
|
+
# @example
|
|
343
|
+
# # Set Cache-Control directives
|
|
344
|
+
# response.cache_control :public, max_age: 900, s_maxage: 86400
|
|
345
|
+
#
|
|
346
|
+
# # Overwrite previous Cache-Control directives
|
|
347
|
+
# response.cache_control :private, :no_cache, :no_store
|
|
348
|
+
#
|
|
349
|
+
# response.get_header("Cache-Control") # => "private, no-store, max-age=900"
|
|
350
|
+
#
|
|
351
|
+
# @param values [Array<Symbol, Hash>] values to map to `Cache-Control` directives
|
|
352
|
+
# @option values [Symbol] :public
|
|
353
|
+
# @option values [Symbol] :private
|
|
354
|
+
# @option values [Symbol] :no_cache
|
|
355
|
+
# @option values [Symbol] :no_store
|
|
356
|
+
# @option values [Symbol] :must_validate
|
|
357
|
+
# @option values [Symbol] :proxy_revalidate
|
|
358
|
+
# @option values [Hash] :max_age
|
|
359
|
+
# @option values [Hash] :min_stale
|
|
360
|
+
# @option values [Hash] :s_max_age
|
|
361
|
+
#
|
|
362
|
+
# @return void
|
|
363
|
+
#
|
|
364
|
+
# @since 2.0.0
|
|
365
|
+
# @api public
|
|
366
|
+
def cache_control(*values)
|
|
367
|
+
directives = Cache::CacheControl::Directives.new(*values)
|
|
368
|
+
headers.merge!(directives.headers)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Sets the `Expires` header and `Cache-Control`/`max-age` directive for the response.
|
|
372
|
+
#
|
|
373
|
+
# You can provide an integer number of seconds in the future, or a Time object indicating when
|
|
374
|
+
# the response should be considered "stale". The remaining arguments are passed to
|
|
375
|
+
# {#cache_control}.
|
|
376
|
+
#
|
|
377
|
+
# @example
|
|
378
|
+
# # Set Cache-Control directives and Expires
|
|
379
|
+
# response.expires 900, :public
|
|
380
|
+
#
|
|
381
|
+
# # Overwrite Cache-Control directives and Expires
|
|
382
|
+
# response.expires 300, :private, :no_cache, :no_store
|
|
383
|
+
#
|
|
384
|
+
# response.get_header("Expires") # => "Thu, 26 Jun 2014 12:00:00 GMT"
|
|
385
|
+
# response.get_header("Cache-Control") # => "private, no-cache, no-store max-age=300"
|
|
386
|
+
#
|
|
387
|
+
# @param amount [Integer, Time] number of seconds or point in time
|
|
388
|
+
# @param values [Array<Symbols>] values to map to `Cache-Control` directives via
|
|
389
|
+
# {#cache_control}
|
|
390
|
+
#
|
|
391
|
+
# @return void
|
|
392
|
+
#
|
|
393
|
+
# @since 2.0.0
|
|
394
|
+
# @api public
|
|
395
|
+
def expires(amount, *values)
|
|
396
|
+
directives = Cache::Expires::Directives.new(amount, *values)
|
|
397
|
+
headers.merge!(directives.headers)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Sets the `etag` and/or `last_modified` headers on the response and halts with a `304 Not
|
|
401
|
+
# Modified` response if the request is still fresh according to the `IfNoneMatch` and
|
|
402
|
+
# `IfModifiedSince` request headers.
|
|
403
|
+
#
|
|
404
|
+
# @example
|
|
405
|
+
# # Set etag header and halt 304 if request matches IF_NONE_MATCH header
|
|
406
|
+
# response.fresh etag: some_resource.updated_at.to_i
|
|
407
|
+
#
|
|
408
|
+
# # Set last_modified header and halt 304 if request matches IF_MODIFIED_SINCE
|
|
409
|
+
# response.fresh last_modified: some_resource.updated_at
|
|
410
|
+
#
|
|
411
|
+
# # Set etag and last_modified header and halt 304 if request matches IF_MODIFIED_SINCE and IF_NONE_MATCH
|
|
412
|
+
# response.fresh last_modified: some_resource.updated_at
|
|
413
|
+
#
|
|
414
|
+
# @param options [Hash]
|
|
415
|
+
# @option options [Integer] :etag for testing IfNoneMatch conditions
|
|
416
|
+
# @option options [Date] :last_modified for testing IfModifiedSince conditions
|
|
417
|
+
#
|
|
418
|
+
# @return void
|
|
419
|
+
#
|
|
420
|
+
# @since 2.0.0
|
|
421
|
+
# @api public
|
|
422
|
+
def fresh(options)
|
|
423
|
+
conditional_get = Cache::ConditionalGet.new(env, options)
|
|
424
|
+
|
|
425
|
+
headers.merge!(conditional_get.headers)
|
|
426
|
+
|
|
427
|
+
conditional_get.fresh? do
|
|
428
|
+
Halt.call(304)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# @since 2.0.0
|
|
433
|
+
# @api private
|
|
434
|
+
def set_format(value) # rubocop:disable Naming/AccessorMethodName
|
|
435
|
+
@format = value
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# @since 2.0.0
|
|
439
|
+
# @api private
|
|
440
|
+
def renderable?
|
|
441
|
+
return !head? && body.empty? if body.respond_to?(:empty?)
|
|
442
|
+
|
|
443
|
+
!@sending_file && !head?
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# @since 2.0.0
|
|
447
|
+
# @api private
|
|
448
|
+
def allow_redirect?
|
|
449
|
+
return body.empty? if body.respond_to?(:empty?)
|
|
450
|
+
|
|
451
|
+
!@sending_file
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# @since 2.0.0
|
|
455
|
+
# @api private
|
|
456
|
+
alias_method :to_ary, :to_a
|
|
457
|
+
|
|
458
|
+
# @since 2.0.0
|
|
459
|
+
# @api private
|
|
460
|
+
def head?
|
|
461
|
+
env[Action::REQUEST_METHOD] == Action::HEAD
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# @since 2.0.0
|
|
465
|
+
# @api private
|
|
466
|
+
def _send_file(send_file_response)
|
|
467
|
+
headers.merge!(send_file_response[Action::RESPONSE_HEADERS])
|
|
468
|
+
|
|
469
|
+
if send_file_response[Action::RESPONSE_CODE] == Action::NOT_FOUND
|
|
470
|
+
headers.delete(Action::X_CASCADE)
|
|
471
|
+
headers.delete(Action::CONTENT_LENGTH)
|
|
472
|
+
Halt.call(Action::NOT_FOUND)
|
|
473
|
+
else
|
|
474
|
+
self.status = send_file_response[Action::RESPONSE_CODE]
|
|
475
|
+
self.body = send_file_response[Action::RESPONSE_BODY]
|
|
476
|
+
@sending_file = true
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
class Action
|
|
5
|
+
# Session support for actions.
|
|
6
|
+
#
|
|
7
|
+
# Not included by default; you should include this module manually to enable session support.
|
|
8
|
+
# For actions within an Hanami app, this module will be included automatically if sessions are
|
|
9
|
+
# configured in the app config.
|
|
10
|
+
#
|
|
11
|
+
# @api public
|
|
12
|
+
# @since 0.1.0
|
|
13
|
+
module Session
|
|
14
|
+
# @api private
|
|
15
|
+
# @since 0.1.0
|
|
16
|
+
def self.included(base)
|
|
17
|
+
base.class_eval do
|
|
18
|
+
before { |req, _| req.id }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def session_enabled?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Finalize the response
|
|
29
|
+
#
|
|
30
|
+
# @return [void]
|
|
31
|
+
#
|
|
32
|
+
# @since 0.3.0
|
|
33
|
+
# @api private
|
|
34
|
+
#
|
|
35
|
+
# @see Hanami::Action#finish
|
|
36
|
+
def finish(req, res, *)
|
|
37
|
+
if (next_flash = res.flash.next).any?
|
|
38
|
+
res.session[Flash::KEY] = next_flash
|
|
39
|
+
else
|
|
40
|
+
res.session.delete(Flash::KEY)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hanami
|
|
4
|
+
class Action
|
|
5
|
+
# Support for validating params when calling actions.
|
|
6
|
+
#
|
|
7
|
+
# Included only when dry-validation is bundled.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
# @since 0.1.0
|
|
11
|
+
module Validatable
|
|
12
|
+
# @api private
|
|
13
|
+
# @since 0.1.0
|
|
14
|
+
def self.included(base)
|
|
15
|
+
base.extend ClassMethods
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Validatable API class methods
|
|
19
|
+
#
|
|
20
|
+
# @since 0.1.0
|
|
21
|
+
# @api private
|
|
22
|
+
module ClassMethods
|
|
23
|
+
# Defines a validation schema for the params passed to {Hanami::Action#call}.
|
|
24
|
+
#
|
|
25
|
+
# This feature isn't mandatory, but is highly recommended for secure handling of params:
|
|
26
|
+
# because params come from an untrusted source, it's good practice to filter these to only
|
|
27
|
+
# the keys and types required for your action's use case.
|
|
28
|
+
#
|
|
29
|
+
# The given block is evaluated inside a `params` schema of a `Dry::Validation::Contract`
|
|
30
|
+
# class. This constrains the validation to simple structure and type rules only. If you want
|
|
31
|
+
# to use all the features of dry-validation contracts, use {#contract} instead.
|
|
32
|
+
#
|
|
33
|
+
# Instead of defining the params validation schema inline, you can alternatively provide a
|
|
34
|
+
# concrete `Dry::Validation::Contract` subclass.
|
|
35
|
+
#
|
|
36
|
+
# @param klass [Class,nil] a Dry::Validation::Contract subclass
|
|
37
|
+
# @param block [Proc] the params schema definition
|
|
38
|
+
#
|
|
39
|
+
# @return void
|
|
40
|
+
#
|
|
41
|
+
# @see #contract
|
|
42
|
+
# @see Hanami::Action::Params
|
|
43
|
+
# @see https://dry-rb.org/gems/dry-validation/
|
|
44
|
+
#
|
|
45
|
+
# @example Inline definition
|
|
46
|
+
# require "hanami/action"
|
|
47
|
+
#
|
|
48
|
+
# class Signup < Hanami::Action
|
|
49
|
+
# params do
|
|
50
|
+
# required(:first_name)
|
|
51
|
+
# required(:last_name)
|
|
52
|
+
# required(:email)
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# def handle(req, *)
|
|
56
|
+
# puts req.params[:first_name] # => "Luca"
|
|
57
|
+
# puts req.params[:admin] # => nil
|
|
58
|
+
# end
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# @example Concrete class
|
|
62
|
+
# require "hanami/action"
|
|
63
|
+
#
|
|
64
|
+
# class SignupContract < Dry::Validation::Contract
|
|
65
|
+
# params do
|
|
66
|
+
# required(:first_name)
|
|
67
|
+
# required(:last_name)
|
|
68
|
+
# required(:email)
|
|
69
|
+
# end
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
# class Signup < Hanami::Action
|
|
73
|
+
# params SignupContract
|
|
74
|
+
#
|
|
75
|
+
# def handle(req, *)
|
|
76
|
+
# req.params[:first_name] # => "Luca"
|
|
77
|
+
# req.params[:admin] # => nil
|
|
78
|
+
# end
|
|
79
|
+
# end
|
|
80
|
+
#
|
|
81
|
+
# @api public
|
|
82
|
+
# @since 0.3.0
|
|
83
|
+
def params(klass = nil, &block)
|
|
84
|
+
contract_class = klass || Class.new(Dry::Validation::Contract) { params(&block) }
|
|
85
|
+
|
|
86
|
+
config.contract_class = contract_class
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Defines a validation contract for the params passed to {Hanami::Action#call}.
|
|
90
|
+
#
|
|
91
|
+
# This feature isn't mandatory, but is highly recommended for secure handling of params:
|
|
92
|
+
# because params come from an untrusted source, it's good practice to filter these to only
|
|
93
|
+
# the keys and types required for your action's use case.
|
|
94
|
+
#
|
|
95
|
+
# The given block is evaluated inside a `Dry::Validation::Contract` class. This allows you
|
|
96
|
+
# to use all features of dry-validation contracts
|
|
97
|
+
#
|
|
98
|
+
# Instead of defining the contract inline, you can alternatively provide a concrete
|
|
99
|
+
# `Dry::Validation::Contract` subclass.
|
|
100
|
+
#
|
|
101
|
+
# @param klass [Class,nil] a Dry::Validation::Contract subclass
|
|
102
|
+
# @param block [Proc] the params schema definition
|
|
103
|
+
#
|
|
104
|
+
# @return void
|
|
105
|
+
#
|
|
106
|
+
# @see #params
|
|
107
|
+
# @see Hanami::Action::Params
|
|
108
|
+
# @see https://dry-rb.org/gems/dry-validation/
|
|
109
|
+
#
|
|
110
|
+
# @example Inline definition
|
|
111
|
+
# require "hanami/action"
|
|
112
|
+
#
|
|
113
|
+
# class Signup < Hanami::Action
|
|
114
|
+
# contract do
|
|
115
|
+
# params do
|
|
116
|
+
# required(:first_name)
|
|
117
|
+
# required(:last_name)
|
|
118
|
+
# required(:email)
|
|
119
|
+
# end
|
|
120
|
+
#
|
|
121
|
+
# rule(:email) do
|
|
122
|
+
# # custom rule logic here
|
|
123
|
+
# end
|
|
124
|
+
# end
|
|
125
|
+
#
|
|
126
|
+
# def handle(req, *)
|
|
127
|
+
# puts req.params[:first_name] # => "Luca"
|
|
128
|
+
# puts req.params[:admin] # => nil
|
|
129
|
+
# end
|
|
130
|
+
# end
|
|
131
|
+
#
|
|
132
|
+
# @example Concrete class
|
|
133
|
+
# require "hanami/action"
|
|
134
|
+
#
|
|
135
|
+
# class SignupContract < Dry::Validation::Contract
|
|
136
|
+
# params do
|
|
137
|
+
# required(:first_name)
|
|
138
|
+
# required(:last_name)
|
|
139
|
+
# required(:email)
|
|
140
|
+
# end
|
|
141
|
+
#
|
|
142
|
+
# rule(:email) do
|
|
143
|
+
# # custom rule logic here
|
|
144
|
+
# end
|
|
145
|
+
# end
|
|
146
|
+
#
|
|
147
|
+
# class Signup < Hanami::Action
|
|
148
|
+
# contract SignupContract
|
|
149
|
+
#
|
|
150
|
+
# def handle(req, *)
|
|
151
|
+
# req.params[:first_name] # => "Luca"
|
|
152
|
+
# req.params[:admin] # => nil
|
|
153
|
+
# end
|
|
154
|
+
# end
|
|
155
|
+
#
|
|
156
|
+
# @api public
|
|
157
|
+
# @since 2.2.0
|
|
158
|
+
def contract(klass = nil, &block)
|
|
159
|
+
contract_class = klass || Class.new(Dry::Validation::Contract, &block)
|
|
160
|
+
|
|
161
|
+
config.contract_class = contract_class
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|