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,672 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/configurable"
|
|
4
|
+
require "hanami/utils"
|
|
5
|
+
require "hanami/utils/callbacks"
|
|
6
|
+
require "hanami/utils/kernel"
|
|
7
|
+
require "hanami/utils/string"
|
|
8
|
+
require "rack"
|
|
9
|
+
require "rack/utils"
|
|
10
|
+
require "zeitwerk"
|
|
11
|
+
|
|
12
|
+
require_relative "action/constants"
|
|
13
|
+
require_relative "action/errors"
|
|
14
|
+
require_relative "action/rack_utils"
|
|
15
|
+
require_relative "action/version"
|
|
16
|
+
|
|
17
|
+
module Hanami
|
|
18
|
+
# An HTTP endpoint
|
|
19
|
+
#
|
|
20
|
+
# @since 0.1.0
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# require "hanami/action"
|
|
24
|
+
#
|
|
25
|
+
# class Show < Hanami::Action
|
|
26
|
+
# def handle(request, response)
|
|
27
|
+
# # ...
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# @api public
|
|
32
|
+
class Action
|
|
33
|
+
# @since 2.0.0
|
|
34
|
+
# @api private
|
|
35
|
+
def self.gem_loader
|
|
36
|
+
@gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
|
|
37
|
+
root = File.expand_path("..", __dir__)
|
|
38
|
+
loader.tag = "hanami-action"
|
|
39
|
+
loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-action.rb")
|
|
40
|
+
loader.push_dir(root)
|
|
41
|
+
loader.ignore(
|
|
42
|
+
"#{root}/hanami-action.rb",
|
|
43
|
+
"#{root}/hanami-controller.rb",
|
|
44
|
+
"#{root}/hanami/action/version.rb",
|
|
45
|
+
"#{root}/hanami/action/{constants,errors,rack_utils,validatable}.rb"
|
|
46
|
+
)
|
|
47
|
+
loader.inflector.inflect(
|
|
48
|
+
"csrf_protection" => "CSRFProtection",
|
|
49
|
+
"json" => "JSON"
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
gem_loader.setup
|
|
55
|
+
|
|
56
|
+
# Make conditional requires after Zeitwerk setup so any internal autoloading works as expected
|
|
57
|
+
begin
|
|
58
|
+
require "dry/validation"
|
|
59
|
+
require_relative "action/validatable"
|
|
60
|
+
rescue LoadError # rubocop:disable Lint/SuppressedException
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
extend Dry::Configurable(config_class: Config)
|
|
64
|
+
|
|
65
|
+
# See {Config} for individual setting accessor API docs
|
|
66
|
+
setting :handled_exceptions, default: {}
|
|
67
|
+
setting :formats, default: Config::Formats.new, mutable: true
|
|
68
|
+
setting :default_charset
|
|
69
|
+
setting :default_headers, default: {}, constructor: -> (headers) { headers.compact }
|
|
70
|
+
setting :default_tld_length, default: 1
|
|
71
|
+
setting :cookies, default: {}, constructor: -> (cookie_options) {
|
|
72
|
+
# Call `to_h` here to permit `ApplicationConfiguration::Cookies` object to be
|
|
73
|
+
# provided when application actions are configured
|
|
74
|
+
cookie_options.to_h.compact
|
|
75
|
+
}
|
|
76
|
+
setting :root_directory, constructor: -> (dir) {
|
|
77
|
+
Pathname(File.expand_path(dir || Dir.pwd))
|
|
78
|
+
}
|
|
79
|
+
setting :public_directory, default: Config::DEFAULT_PUBLIC_DIRECTORY
|
|
80
|
+
setting :before_callbacks, default: Utils::Callbacks::Chain.new, mutable: true
|
|
81
|
+
setting :after_callbacks, default: Utils::Callbacks::Chain.new, mutable: true
|
|
82
|
+
setting :contract_class
|
|
83
|
+
|
|
84
|
+
# @!scope class
|
|
85
|
+
|
|
86
|
+
# @!method config
|
|
87
|
+
# Returns the action's config. Use this to configure your action.
|
|
88
|
+
#
|
|
89
|
+
# @example Access inside class body
|
|
90
|
+
# class Show < Hanami::Action
|
|
91
|
+
# config.format :json
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# @return [Config]
|
|
95
|
+
#
|
|
96
|
+
# @api public
|
|
97
|
+
# @since 2.0.0
|
|
98
|
+
|
|
99
|
+
# @!scope instance
|
|
100
|
+
|
|
101
|
+
# Override Ruby's hook for modules.
|
|
102
|
+
# It includes basic Hanami::Action modules to the given class.
|
|
103
|
+
#
|
|
104
|
+
# @param subclass [Class] the target action
|
|
105
|
+
#
|
|
106
|
+
# @since 0.1.0
|
|
107
|
+
# @api private
|
|
108
|
+
def self.inherited(subclass)
|
|
109
|
+
super
|
|
110
|
+
|
|
111
|
+
if subclass.superclass == Action
|
|
112
|
+
subclass.class_eval do
|
|
113
|
+
include Validatable if defined?(Validatable)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Placeholder for the `.params` method. Raises an error when the dry-validation gem is not
|
|
119
|
+
# installed.
|
|
120
|
+
#
|
|
121
|
+
# @raise [NoMethodError]
|
|
122
|
+
#
|
|
123
|
+
# @api public
|
|
124
|
+
# @since 2.0.0
|
|
125
|
+
def self.params(_klass = nil)
|
|
126
|
+
message = %(To use `.params`, please add the "dry-validation" gem to your Gemfile)
|
|
127
|
+
raise NoMethodError, message
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Placeholder for the `.contract` method. Raises an error when the dry-validation gem is not
|
|
131
|
+
# installed.
|
|
132
|
+
#
|
|
133
|
+
# @raise [NoMethodError]
|
|
134
|
+
#
|
|
135
|
+
# @api public
|
|
136
|
+
# @since 2.2.0
|
|
137
|
+
def self.contract
|
|
138
|
+
message = %(To use `.contract`, please add the "dry-validation" gem to your Gemfile)
|
|
139
|
+
raise NoMethodError, message
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @overload self.append_before(*callbacks, &block)
|
|
143
|
+
# Define a callback for an Action.
|
|
144
|
+
# The callback will be executed **before** the action is called, in the
|
|
145
|
+
# order they are added.
|
|
146
|
+
#
|
|
147
|
+
# @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
|
|
148
|
+
# each of them is representing a name of a method available in the
|
|
149
|
+
# context of the Action.
|
|
150
|
+
#
|
|
151
|
+
# @param blk [Proc] an anonymous function to be executed
|
|
152
|
+
#
|
|
153
|
+
# @return [void]
|
|
154
|
+
#
|
|
155
|
+
# @since 0.3.2
|
|
156
|
+
#
|
|
157
|
+
# @see Hanami::Action::Callbacks::ClassMethods#append_after
|
|
158
|
+
#
|
|
159
|
+
# @example Method names (symbols)
|
|
160
|
+
# require "hanami/action"
|
|
161
|
+
#
|
|
162
|
+
# class Show < Hanami::Action
|
|
163
|
+
# before :authenticate, :set_article
|
|
164
|
+
#
|
|
165
|
+
# def handle(request, response)
|
|
166
|
+
# end
|
|
167
|
+
#
|
|
168
|
+
# private
|
|
169
|
+
# def authenticate
|
|
170
|
+
# # ...
|
|
171
|
+
# end
|
|
172
|
+
#
|
|
173
|
+
# # `params` in the method signature is optional
|
|
174
|
+
# def set_article(params)
|
|
175
|
+
# @article = Article.find params[:id]
|
|
176
|
+
# end
|
|
177
|
+
# end
|
|
178
|
+
#
|
|
179
|
+
# # The order of execution will be:
|
|
180
|
+
# #
|
|
181
|
+
# # 1. #authenticate
|
|
182
|
+
# # 2. #set_article
|
|
183
|
+
# # 3. #call
|
|
184
|
+
#
|
|
185
|
+
# @example Anonymous functions (Procs)
|
|
186
|
+
# require "hanami/action"
|
|
187
|
+
#
|
|
188
|
+
# class Show < Hanami::Action
|
|
189
|
+
# before { ... } # 1 do some authentication stuff
|
|
190
|
+
# before {|request, response| @article = Article.find params[:id] } # 2
|
|
191
|
+
#
|
|
192
|
+
# def handle(request, response)
|
|
193
|
+
# end
|
|
194
|
+
# end
|
|
195
|
+
#
|
|
196
|
+
# # The order of execution will be:
|
|
197
|
+
# #
|
|
198
|
+
# # 1. authentication
|
|
199
|
+
# # 2. set the article
|
|
200
|
+
# # 3. `#handle`
|
|
201
|
+
def self.append_before(...)
|
|
202
|
+
config.before_callbacks.append(...)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
class << self
|
|
206
|
+
# @since 0.1.0
|
|
207
|
+
alias_method :before, :append_before
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @overload self.append_after(*callbacks, &block)
|
|
211
|
+
# Define a callback for an Action.
|
|
212
|
+
# The callback will be executed **after** the action is called, in the
|
|
213
|
+
# order they are added.
|
|
214
|
+
#
|
|
215
|
+
# @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
|
|
216
|
+
# each of them is representing a name of a method available in the
|
|
217
|
+
# context of the Action.
|
|
218
|
+
#
|
|
219
|
+
# @param blk [Proc] an anonymous function to be executed
|
|
220
|
+
#
|
|
221
|
+
# @return [void]
|
|
222
|
+
#
|
|
223
|
+
# @since 0.3.2
|
|
224
|
+
#
|
|
225
|
+
# @see Hanami::Action::Callbacks::ClassMethods#append_before
|
|
226
|
+
def self.append_after(...)
|
|
227
|
+
config.after_callbacks.append(...)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
class << self
|
|
231
|
+
# @since 0.1.0
|
|
232
|
+
alias_method :after, :append_after
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# @overload self.prepend_before(*callbacks, &block)
|
|
236
|
+
# Define a callback for an Action.
|
|
237
|
+
# The callback will be executed **before** the action is called.
|
|
238
|
+
# It will add the callback at the beginning of the callbacks' chain.
|
|
239
|
+
#
|
|
240
|
+
# @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
|
|
241
|
+
# each of them is representing a name of a method available in the
|
|
242
|
+
# context of the Action.
|
|
243
|
+
#
|
|
244
|
+
# @param blk [Proc] an anonymous function to be executed
|
|
245
|
+
#
|
|
246
|
+
# @return [void]
|
|
247
|
+
#
|
|
248
|
+
# @since 0.3.2
|
|
249
|
+
#
|
|
250
|
+
# @see Hanami::Action::Callbacks::ClassMethods#prepend_after
|
|
251
|
+
def self.prepend_before(...)
|
|
252
|
+
config.before_callbacks.prepend(...)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# @overload self.prepend_after(*callbacks, &block)
|
|
256
|
+
# Define a callback for an Action.
|
|
257
|
+
# The callback will be executed **after** the action is called.
|
|
258
|
+
# It will add the callback at the beginning of the callbacks' chain.
|
|
259
|
+
#
|
|
260
|
+
# @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
|
|
261
|
+
# each of them is representing a name of a method available in the
|
|
262
|
+
# context of the Action.
|
|
263
|
+
#
|
|
264
|
+
# @param blk [Proc] an anonymous function to be executed
|
|
265
|
+
#
|
|
266
|
+
# @return [void]
|
|
267
|
+
#
|
|
268
|
+
# @since 0.3.2
|
|
269
|
+
#
|
|
270
|
+
# @see Hanami::Action::Callbacks::ClassMethods#prepend_before
|
|
271
|
+
def self.prepend_after(...)
|
|
272
|
+
config.after_callbacks.prepend(...)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# @see Config#handle_exception
|
|
276
|
+
#
|
|
277
|
+
# @since 2.0.0
|
|
278
|
+
# @api public
|
|
279
|
+
def self.handle_exception(...)
|
|
280
|
+
config.handle_exception(...)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Returns the action's frozen `Data` snapshot of its resolved configuration.
|
|
284
|
+
#
|
|
285
|
+
# Instances expose `config` as a read-only `Data` value object (built via
|
|
286
|
+
# dry-configurable's `#to_data`), not the live `Hanami::Action::Config` used at the class
|
|
287
|
+
# level. All setting readers (`formats`, `default_headers`, `default_charset`, etc.) work
|
|
288
|
+
# exactly the same; Config-specific methods like `#handle_exception` are not available on
|
|
289
|
+
# instances and must be called on the class config instead.
|
|
290
|
+
#
|
|
291
|
+
# @since x.x.x
|
|
292
|
+
# @api private
|
|
293
|
+
private attr_reader :config
|
|
294
|
+
|
|
295
|
+
# @since 2.2.0
|
|
296
|
+
# @api private
|
|
297
|
+
private attr_reader :contract
|
|
298
|
+
|
|
299
|
+
# Resolved response charset, computed once from config. Passed to each {Response} so it
|
|
300
|
+
# doesn't have to re-parse the content type's charset on every request.
|
|
301
|
+
#
|
|
302
|
+
# @api private
|
|
303
|
+
private attr_reader :default_charset
|
|
304
|
+
|
|
305
|
+
# Response content type (with charset) and format used when a request has no usable `Accept`
|
|
306
|
+
# header. These depend only on config, so they're computed once and reused per request.
|
|
307
|
+
#
|
|
308
|
+
# @api private
|
|
309
|
+
private attr_reader :default_response_content_type, :default_response_format
|
|
310
|
+
|
|
311
|
+
# Returns a new action
|
|
312
|
+
#
|
|
313
|
+
# @since 2.0.0
|
|
314
|
+
# @api public
|
|
315
|
+
def initialize(config: self.class.config, contract: nil)
|
|
316
|
+
@config = config.finalize!.to_data
|
|
317
|
+
@contract = contract || config.contract_class&.new # TODO: tests showing this overridden by a dep
|
|
318
|
+
@default_charset = @config.default_charset || DEFAULT_CHARSET
|
|
319
|
+
@default_response_content_type = Mime.default_response_content_type_with_charset(@config)
|
|
320
|
+
@default_response_format = Mime.format_from_media_type(@default_response_content_type, @config)
|
|
321
|
+
freeze
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# rubocop:disable Metrics/AbcSize
|
|
325
|
+
|
|
326
|
+
# Implements the Rack/Hanami::Action protocol
|
|
327
|
+
#
|
|
328
|
+
# @since 0.1.0
|
|
329
|
+
# @api private
|
|
330
|
+
def call(env)
|
|
331
|
+
request = nil
|
|
332
|
+
response = nil
|
|
333
|
+
|
|
334
|
+
halted = catch :halt do
|
|
335
|
+
# Catch body parsing errors early, and wait to raise them until _after_ we've built our
|
|
336
|
+
# request and response, to give exception handlers real objects to work with.
|
|
337
|
+
body_parse_error = nil
|
|
338
|
+
begin
|
|
339
|
+
BodyParser.parse env, config
|
|
340
|
+
rescue BodyParsingError => exception
|
|
341
|
+
body_parse_error = exception
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
params = Params.new(
|
|
345
|
+
# Create empty params if body parsing failed, to avoid validating corrupted input.
|
|
346
|
+
env: body_parse_error ? {} : env,
|
|
347
|
+
contract: contract
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Ensure env has REQUEST_METHOD for downstream code (e.g. Response) when actions are called
|
|
351
|
+
# with a direct params hash as a testing convenience.
|
|
352
|
+
env[REQUEST_METHOD] ||= DEFAULT_REQUEST_METHOD
|
|
353
|
+
|
|
354
|
+
request = build_request(
|
|
355
|
+
env: env,
|
|
356
|
+
params: params,
|
|
357
|
+
session_enabled: session_enabled?,
|
|
358
|
+
default_tld_length: config.default_tld_length
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
content_type =
|
|
362
|
+
if request.accept_header?
|
|
363
|
+
Mime.response_content_type_with_charset(request, config)
|
|
364
|
+
else
|
|
365
|
+
default_response_content_type
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
response = build_response(
|
|
369
|
+
request: request,
|
|
370
|
+
config: config,
|
|
371
|
+
content_type: content_type,
|
|
372
|
+
charset: default_charset,
|
|
373
|
+
env: env,
|
|
374
|
+
headers: config.default_headers,
|
|
375
|
+
session_enabled: session_enabled?
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
raise body_parse_error if body_parse_error
|
|
379
|
+
|
|
380
|
+
enforce_accepted_media_types(request)
|
|
381
|
+
|
|
382
|
+
_run_before_callbacks(request, response)
|
|
383
|
+
handle(request, response)
|
|
384
|
+
_run_after_callbacks(request, response)
|
|
385
|
+
rescue StandardError => exception
|
|
386
|
+
_handle_exception(request, response, exception)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Before finishing, put ourself into the Rack env for third-party instrumentation tools to
|
|
390
|
+
# integrate with actions
|
|
391
|
+
env[ACTION_INSTANCE] = self
|
|
392
|
+
|
|
393
|
+
finish(request, response, halted)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# rubocop:enable Metrics/AbcSize
|
|
397
|
+
|
|
398
|
+
protected
|
|
399
|
+
|
|
400
|
+
# Hook for subclasses to apply behavior as part of action invocation
|
|
401
|
+
#
|
|
402
|
+
# @param request [Hanami::Action::Request]
|
|
403
|
+
# @param response [Hanami::Action::Response]
|
|
404
|
+
#
|
|
405
|
+
# @since 2.0.0
|
|
406
|
+
# @api public
|
|
407
|
+
def handle(request, response)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Halt the action execution with the given HTTP status code and message.
|
|
411
|
+
#
|
|
412
|
+
# When used, the execution of a callback or of an action is interrupted
|
|
413
|
+
# and the control returns to the framework, that decides how to handle
|
|
414
|
+
# the event.
|
|
415
|
+
#
|
|
416
|
+
# If a message is provided, it sets the response body with the message.
|
|
417
|
+
# Otherwise, it sets the response body with the default message associated
|
|
418
|
+
# to the code (eg 404 will set `"Not Found"`).
|
|
419
|
+
#
|
|
420
|
+
# @param status [Integer, Symbol] A valid HTTP status code or a symbol representing the HTTP status code
|
|
421
|
+
# @param body [String] the response body
|
|
422
|
+
#
|
|
423
|
+
# @raise [StandardError] if the code isn't valid
|
|
424
|
+
#
|
|
425
|
+
# @since 0.2.0
|
|
426
|
+
#
|
|
427
|
+
# @see https://hanakai.org/learn/hanami/actions/status-codes List of status codes and symbols
|
|
428
|
+
#
|
|
429
|
+
# @see Hanami::Action::Throwable#handle_exception
|
|
430
|
+
# @see Hanami::Http::Status:ALL
|
|
431
|
+
#
|
|
432
|
+
# @example Basic usage
|
|
433
|
+
# require "hanami/action"
|
|
434
|
+
#
|
|
435
|
+
# class Show < Hanami::Action
|
|
436
|
+
# def handle(*)
|
|
437
|
+
# halt 404
|
|
438
|
+
# end
|
|
439
|
+
# end
|
|
440
|
+
#
|
|
441
|
+
# # => [404, {}, ["Not Found"]]
|
|
442
|
+
#
|
|
443
|
+
# @example Custom message
|
|
444
|
+
# require "hanami/action"
|
|
445
|
+
#
|
|
446
|
+
# class Show < Hanami::Action
|
|
447
|
+
# def handle(*)
|
|
448
|
+
# halt 404, "This is not the droid you're looking for."
|
|
449
|
+
# end
|
|
450
|
+
# end
|
|
451
|
+
#
|
|
452
|
+
# # => [404, {}, ["This is not the droid you're looking for."]]
|
|
453
|
+
def halt(status, body = nil)
|
|
454
|
+
Halt.call(status, body)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# @since 0.3.2
|
|
458
|
+
# @api private
|
|
459
|
+
def _requires_no_body?(response)
|
|
460
|
+
HTTP_STATUSES_WITHOUT_BODY.include?(response.status)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# @since 2.0.0
|
|
464
|
+
# @api private
|
|
465
|
+
alias_method :_requires_empty_headers?, :_requires_no_body?
|
|
466
|
+
|
|
467
|
+
private
|
|
468
|
+
|
|
469
|
+
# @since 2.0.0
|
|
470
|
+
# @api private
|
|
471
|
+
def enforce_accepted_media_types(request)
|
|
472
|
+
return if config.formats.empty?
|
|
473
|
+
|
|
474
|
+
Mime.enforce_accept(request, config) { return halt 406 }
|
|
475
|
+
Mime.enforce_content_type(request, config) { return halt 415 }
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# @since 2.0.0
|
|
479
|
+
# @api private
|
|
480
|
+
def exception_handler(exception)
|
|
481
|
+
config.handled_exceptions.each do |exception_class, handler|
|
|
482
|
+
case exception_class
|
|
483
|
+
when String
|
|
484
|
+
return handler if exception.class.name == exception_class # rubocop:disable Style/ClassEqualityComparison
|
|
485
|
+
else
|
|
486
|
+
return handler if exception.is_a?(exception_class)
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
nil
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# @see Session#session_enabled?
|
|
494
|
+
# @since 2.0.0
|
|
495
|
+
# @api private
|
|
496
|
+
def session_enabled?
|
|
497
|
+
false
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Hook to be overridden by `Hanami::Extensions::Action` for integrated actions
|
|
501
|
+
#
|
|
502
|
+
# @since 2.0.0
|
|
503
|
+
# @api private
|
|
504
|
+
def build_request(env:, params:, session_enabled:, default_tld_length:)
|
|
505
|
+
Request.new(env:, params:, session_enabled:, default_tld_length:)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Hook to be overridden by `Hanami::Extensions::Action` for integrated actions
|
|
509
|
+
#
|
|
510
|
+
# @since 2.0.0
|
|
511
|
+
# @api private
|
|
512
|
+
def build_response(request:, config:, content_type:, env:, headers:, session_enabled:, charset: nil,
|
|
513
|
+
view_options: nil)
|
|
514
|
+
Response.new(request:, config:, content_type:, charset:, env:, headers:, session_enabled:, view_options:)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# @since 0.2.0
|
|
518
|
+
# @api private
|
|
519
|
+
def _reference_in_rack_errors(request, exception)
|
|
520
|
+
request.env[RACK_EXCEPTION] = exception
|
|
521
|
+
|
|
522
|
+
if errors = request.env[RACK_ERRORS]
|
|
523
|
+
errors.write(_dump_exception(exception))
|
|
524
|
+
errors.flush
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# @since 0.2.0
|
|
529
|
+
# @api private
|
|
530
|
+
def _dump_exception(exception)
|
|
531
|
+
[[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# @since 0.1.0
|
|
535
|
+
# @api private
|
|
536
|
+
def _handle_exception(request, response, exception)
|
|
537
|
+
handler = exception_handler(exception)
|
|
538
|
+
|
|
539
|
+
if handler.nil?
|
|
540
|
+
_reference_in_rack_errors(request, exception)
|
|
541
|
+
raise exception
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
instance_exec(
|
|
545
|
+
request,
|
|
546
|
+
response,
|
|
547
|
+
exception,
|
|
548
|
+
&_exception_handler(handler)
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
nil
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# @since 0.3.0
|
|
555
|
+
# @api private
|
|
556
|
+
def _exception_handler(handler)
|
|
557
|
+
if respond_to?(handler.to_s, true)
|
|
558
|
+
method(handler)
|
|
559
|
+
else
|
|
560
|
+
->(*) { halt handler }
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# @since 0.1.0
|
|
565
|
+
# @api private
|
|
566
|
+
def _run_before_callbacks(request, response)
|
|
567
|
+
config.before_callbacks.run(self, request, response)
|
|
568
|
+
nil
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# @since 0.1.0
|
|
572
|
+
# @api private
|
|
573
|
+
def _run_after_callbacks(request, response)
|
|
574
|
+
config.after_callbacks.run(self, request, response)
|
|
575
|
+
nil
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# According to RFC 2616, when a response MUST have an empty body, it only
|
|
579
|
+
# allows Entity Headers.
|
|
580
|
+
#
|
|
581
|
+
# For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
|
|
582
|
+
# other custom header.
|
|
583
|
+
#
|
|
584
|
+
# This restriction is enforced by <tt>Hanami::Action#_requires_no_body?</tt>.
|
|
585
|
+
#
|
|
586
|
+
# However, there are cases that demand to bypass this rule to set meta
|
|
587
|
+
# informations via headers.
|
|
588
|
+
#
|
|
589
|
+
# An example is a <tt>DELETE</tt> request for a JSON API application.
|
|
590
|
+
# It returns a <tt>204</tt> but still wants to specify the rate limit
|
|
591
|
+
# quota via <tt>X-Rate-Limit</tt>.
|
|
592
|
+
#
|
|
593
|
+
# @since 0.5.0
|
|
594
|
+
#
|
|
595
|
+
# @see Hanami::Action#_requires_no_body?
|
|
596
|
+
#
|
|
597
|
+
# @example
|
|
598
|
+
# require "hanami/action"
|
|
599
|
+
#
|
|
600
|
+
# module Books
|
|
601
|
+
# class Destroy < Hanami::Action
|
|
602
|
+
# def handle(*, response)
|
|
603
|
+
# # ...
|
|
604
|
+
# response.headers.merge!(
|
|
605
|
+
# "Last-Modified" => "Fri, 27 Nov 2015 13:32:36 GMT",
|
|
606
|
+
# "X-Rate-Limit" => "4000",
|
|
607
|
+
# "Content-Type" => "application/json",
|
|
608
|
+
# "X-No-Pass" => "true"
|
|
609
|
+
# )
|
|
610
|
+
#
|
|
611
|
+
# response.status = 204
|
|
612
|
+
# end
|
|
613
|
+
#
|
|
614
|
+
# private
|
|
615
|
+
#
|
|
616
|
+
# def keep_response_header?(header)
|
|
617
|
+
# super || header == "X-Rate-Limit"
|
|
618
|
+
# end
|
|
619
|
+
# end
|
|
620
|
+
# end
|
|
621
|
+
#
|
|
622
|
+
# # Only the following headers will be sent:
|
|
623
|
+
# # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
|
|
624
|
+
# # * X-Rate-Limit - because we explicitely allow it
|
|
625
|
+
#
|
|
626
|
+
# # Both Content-Type and X-No-Pass are removed because they're not allowed
|
|
627
|
+
def keep_response_header?(header)
|
|
628
|
+
ENTITY_HEADERS.include?(header.downcase)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# @since 2.0.0
|
|
632
|
+
# @api private
|
|
633
|
+
def _empty_headers(response)
|
|
634
|
+
response.headers.select! { |header, _| keep_response_header?(header) }
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# @since 2.0.0
|
|
638
|
+
# @api private
|
|
639
|
+
def _empty_body(response)
|
|
640
|
+
response.body = Response::EMPTY_BODY
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Finalize the response
|
|
644
|
+
#
|
|
645
|
+
# Prepare the data before the response will be returned to the webserver
|
|
646
|
+
#
|
|
647
|
+
# @since 0.1.0
|
|
648
|
+
# @api private
|
|
649
|
+
# @abstract
|
|
650
|
+
#
|
|
651
|
+
# @see Hanami::Action::Session#finish
|
|
652
|
+
# @see Hanami::Action::Cookies#finish
|
|
653
|
+
# @see Hanami::Action::Cache#finish
|
|
654
|
+
def finish(request, response, halted)
|
|
655
|
+
response.status, response.body = *halted unless halted.nil?
|
|
656
|
+
|
|
657
|
+
_empty_headers(response) if _requires_empty_headers?(response)
|
|
658
|
+
_empty_body(response) if response.head?
|
|
659
|
+
|
|
660
|
+
format =
|
|
661
|
+
if response.content_type == default_response_content_type
|
|
662
|
+
default_response_format
|
|
663
|
+
else
|
|
664
|
+
Mime.format_from_media_type(response.content_type, config)
|
|
665
|
+
end
|
|
666
|
+
response.set_format(format)
|
|
667
|
+
response[:params] = request.params
|
|
668
|
+
response[:format] = response.format
|
|
669
|
+
response
|
|
670
|
+
end
|
|
671
|
+
end
|
|
672
|
+
end
|