hanami-controller 2.0.0.alpha6 → 2.0.0.alpha8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,578 +0,0 @@
1
- begin
2
- require 'hanami/validations'
3
- require 'hanami/action/validatable'
4
- rescue LoadError
5
- end
6
-
7
- require 'hanami/utils/class_attribute'
8
- require 'hanami/utils/callbacks'
9
- require 'hanami/utils'
10
- require 'hanami/utils/string'
11
- require 'hanami/utils/kernel'
12
- require 'rack/utils'
13
-
14
- require_relative 'base_params'
15
- require_relative 'configuration'
16
- require_relative 'halt'
17
- require_relative 'mime'
18
- require_relative 'rack/file'
19
- require_relative 'request'
20
- require_relative 'response'
21
-
22
- module Hanami
23
- class Action
24
- module StandaloneAction
25
- def self.included(klass)
26
- klass.include InstanceMethods
27
- klass.extend ClassMethods
28
- end
29
-
30
- module ClassMethods
31
- def configuration
32
- @configuration ||= Configuration.new
33
- end
34
-
35
- alias_method :config, :configuration
36
-
37
- # Override Ruby's hook for modules.
38
- # It includes basic Hanami::Action modules to the given class.
39
- #
40
- # @param subclass [Class] the target action
41
- #
42
- # @since 0.1.0
43
- # @api private
44
- def inherited(subclass)
45
- if subclass.superclass == Action
46
- subclass.class_eval do
47
- include Utils::ClassAttribute
48
-
49
- class_attribute :before_callbacks
50
- self.before_callbacks = Utils::Callbacks::Chain.new
51
-
52
- class_attribute :after_callbacks
53
- self.after_callbacks = Utils::Callbacks::Chain.new
54
-
55
- include Validatable if defined?(Validatable)
56
- end
57
- end
58
-
59
- subclass.instance_variable_set '@configuration', configuration.dup
60
- end
61
-
62
- # Returns the class which defines the params
63
- #
64
- # Returns the class which has been provided to define the
65
- # params. By default this will be Hanami::Action::Params.
66
- #
67
- # @return [Class] A params class (when whitelisted) or
68
- # Hanami::Action::Params
69
- #
70
- # @api private
71
- # @since 0.7.0
72
- def params_class
73
- @params_class ||= BaseParams
74
- end
75
-
76
- # FIXME: make this thread-safe
77
- def accepted_formats
78
- @accepted_formats ||= []
79
- end
80
-
81
- # Define a callback for an Action.
82
- # The callback will be executed **before** the action is called, in the
83
- # order they are added.
84
- #
85
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
86
- # each of them is representing a name of a method available in the
87
- # context of the Action.
88
- #
89
- # @param blk [Proc] an anonymous function to be executed
90
- #
91
- # @return [void]
92
- #
93
- # @since 0.3.2
94
- #
95
- # @see Hanami::Action::Callbacks::ClassMethods#append_after
96
- #
97
- # @example Method names (symbols)
98
- # require 'hanami/controller'
99
- #
100
- # class Show
101
- # include Hanami::Action
102
- #
103
- # before :authenticate, :set_article
104
- #
105
- # def call(params)
106
- # end
107
- #
108
- # private
109
- # def authenticate
110
- # # ...
111
- # end
112
- #
113
- # # `params` in the method signature is optional
114
- # def set_article(params)
115
- # @article = Article.find params[:id]
116
- # end
117
- # end
118
- #
119
- # # The order of execution will be:
120
- # #
121
- # # 1. #authenticate
122
- # # 2. #set_article
123
- # # 3. #call
124
- #
125
- # @example Anonymous functions (Procs)
126
- # require 'hanami/controller'
127
- #
128
- # class Show
129
- # include Hanami::Action
130
- #
131
- # before { ... } # 1 do some authentication stuff
132
- # before {|params| @article = Article.find params[:id] } # 2
133
- #
134
- # def call(params)
135
- # end
136
- # end
137
- #
138
- # # The order of execution will be:
139
- # #
140
- # # 1. authentication
141
- # # 2. set the article
142
- # # 3. #call
143
- def append_before(*callbacks, &blk)
144
- before_callbacks.append(*callbacks, &blk)
145
- end
146
-
147
- alias_method :before, :append_before
148
-
149
- # class << self
150
- # # @since 0.1.0
151
- # alias before append_before
152
- # end
153
-
154
- # Define a callback for an Action.
155
- # The callback will be executed **after** the action is called, in the
156
- # order they are added.
157
- #
158
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
159
- # each of them is representing a name of a method available in the
160
- # context of the Action.
161
- #
162
- # @param blk [Proc] an anonymous function to be executed
163
- #
164
- # @return [void]
165
- #
166
- # @since 0.3.2
167
- #
168
- # @see Hanami::Action::Callbacks::ClassMethods#append_before
169
- def append_after(*callbacks, &blk)
170
- after_callbacks.append(*callbacks, &blk)
171
- end
172
-
173
- alias_method :after, :append_after
174
-
175
- # class << self
176
- # # @since 0.1.0
177
- # alias after append_after
178
- # end
179
-
180
- # Define a callback for an Action.
181
- # The callback will be executed **before** the action is called.
182
- # It will add the callback at the beginning of the callbacks' chain.
183
- #
184
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
185
- # each of them is representing a name of a method available in the
186
- # context of the Action.
187
- #
188
- # @param blk [Proc] an anonymous function to be executed
189
- #
190
- # @return [void]
191
- #
192
- # @since 0.3.2
193
- #
194
- # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
195
- def prepend_before(*callbacks, &blk)
196
- before_callbacks.prepend(*callbacks, &blk)
197
- end
198
-
199
- # Define a callback for an Action.
200
- # The callback will be executed **after** the action is called.
201
- # It will add the callback at the beginning of the callbacks' chain.
202
- #
203
- # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
204
- # each of them is representing a name of a method available in the
205
- # context of the Action.
206
- #
207
- # @param blk [Proc] an anonymous function to be executed
208
- #
209
- # @return [void]
210
- #
211
- # @since 0.3.2
212
- #
213
- # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
214
- def prepend_after(*callbacks, &blk)
215
- after_callbacks.prepend(*callbacks, &blk)
216
- end
217
-
218
- # Restrict the access to the specified mime type symbols.
219
- #
220
- # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
221
- #
222
- # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
223
- # be converted into a mime type
224
- #
225
- # @since 0.1.0
226
- #
227
- # @see Hanami::Controller::Configuration#format
228
- #
229
- # @example
230
- # require 'hanami/controller'
231
- #
232
- # class Show
233
- # include Hanami::Action
234
- # accept :html, :json
235
- #
236
- # def call(params)
237
- # # ...
238
- # end
239
- # end
240
- #
241
- # # When called with "*/*" => 200
242
- # # When called with "text/html" => 200
243
- # # When called with "application/json" => 200
244
- # # When called with "application/xml" => 406
245
- def accept(*formats)
246
- @accepted_formats = *formats
247
- before :enforce_accepted_mime_types
248
- end
249
-
250
- # Returns a new action
251
- #
252
- # @overload new(**deps, ...)
253
- # @param deps [Hash] action dependencies
254
- #
255
- # @overload new(configuration:, **deps, ...)
256
- # @param configuration [Hanami::Controller::Configuration] action configuration
257
- # @param deps [Hash] action dependencies
258
- #
259
- # @return [Hanami::Action] Action object
260
- #
261
- # @since 2.0.0
262
- def new(*args, configuration: self.configuration, **kwargs, &block)
263
- allocate.tap do |obj|
264
- obj.instance_variable_set(:@name, Name[name])
265
- obj.instance_variable_set(:@configuration, configuration.dup.finalize!)
266
- obj.instance_variable_set(:@accepted_mime_types, Mime.restrict_mime_types(configuration, accepted_formats))
267
- obj.send(:initialize, *args, **kwargs, &block)
268
- obj.freeze
269
- end
270
- end
271
-
272
- module Name
273
- MODULE_SEPARATOR_TRANSFORMER = [:gsub, "::", "."].freeze
274
-
275
- def self.call(name)
276
- Utils::String.transform(name, MODULE_SEPARATOR_TRANSFORMER, :underscore) unless name.nil?
277
- end
278
-
279
- class << self
280
- alias_method :[], :call
281
- end
282
- end
283
- end
284
-
285
- module InstanceMethods
286
- attr_reader :name
287
-
288
- # Implements the Rack/Hanami::Action protocol
289
- #
290
- # @since 0.1.0
291
- # @api private
292
- def call(env)
293
- request = nil
294
- response = nil
295
-
296
- halted = catch :halt do
297
- begin
298
- params = self.class.params_class.new(env)
299
- request = build_request(env, params)
300
- response = build_response(
301
- request: request,
302
- action: name,
303
- configuration: configuration,
304
- content_type: Mime.calculate_content_type_with_charset(configuration, request, accepted_mime_types),
305
- env: env,
306
- headers: configuration.default_headers
307
- )
308
-
309
- _run_before_callbacks(request, response)
310
- handle(request, response)
311
- _run_after_callbacks(request, response)
312
- rescue => exception
313
- _handle_exception(request, response, exception)
314
- end
315
- end
316
-
317
- finish(request, response, halted)
318
- end
319
-
320
- def initialize(**deps)
321
- @_deps = deps
322
- end
323
-
324
- protected
325
-
326
- # Hook for subclasses to apply behavior as part of action invocation
327
- #
328
- # @param request [Hanami::Action::Request]
329
- # @param response [Hanami::Action::Response]
330
- #
331
- # @since 2.0.0
332
- def handle(request, response)
333
- end
334
-
335
- # Halt the action execution with the given HTTP status code and message.
336
- #
337
- # When used, the execution of a callback or of an action is interrupted
338
- # and the control returns to the framework, that decides how to handle
339
- # the event.
340
- #
341
- # If a message is provided, it sets the response body with the message.
342
- # Otherwise, it sets the response body with the default message associated
343
- # to the code (eg 404 will set `"Not Found"`).
344
- #
345
- # @param status [Fixnum] a valid HTTP status code
346
- # @param body [String] the response body
347
- #
348
- # @raise [StandardError] if the code isn't valid
349
- #
350
- # @since 0.2.0
351
- #
352
- # @see Hanami::Action::Throwable#handle_exception
353
- # @see Hanami::Http::Status:ALL
354
- #
355
- # @example Basic usage
356
- # require 'hanami/controller'
357
- #
358
- # class Show
359
- # def call(params)
360
- # halt 404
361
- # end
362
- # end
363
- #
364
- # # => [404, {}, ["Not Found"]]
365
- #
366
- # @example Custom message
367
- # require 'hanami/controller'
368
- #
369
- # class Show
370
- # def call(params)
371
- # halt 404, "This is not the droid you're looking for."
372
- # end
373
- # end
374
- #
375
- # # => [404, {}, ["This is not the droid you're looking for."]]
376
- def halt(status, body = nil)
377
- Halt.call(status, body)
378
- end
379
-
380
- # @since 0.3.2
381
- # @api private
382
- def _requires_no_body?(res)
383
- HTTP_STATUSES_WITHOUT_BODY.include?(res.status)
384
- end
385
-
386
- # @since 2.0.0
387
- # @api private
388
- def _requires_empty_headers?(res)
389
- _requires_no_body?(res) || res.head?
390
- end
391
-
392
- private
393
-
394
- attr_reader :configuration
395
-
396
- def accepted_mime_types
397
- @accepted_mime_types || configuration.mime_types
398
- end
399
-
400
- def enforce_accepted_mime_types(req, *)
401
- Mime.accepted_mime_type?(req, accepted_mime_types, configuration) or halt 406
402
- end
403
-
404
- def exception_handler(exception)
405
- configuration.handled_exceptions.each do |exception_class, handler|
406
- return handler if exception.kind_of?(exception_class)
407
- end
408
-
409
- nil
410
- end
411
-
412
- def build_request(env, params)
413
- Request.new(env, params)
414
- end
415
-
416
- def build_response(**options)
417
- Response.new(**options)
418
- end
419
-
420
- # @since 0.2.0
421
- # @api private
422
- def _reference_in_rack_errors(req, exception)
423
- req.env[RACK_EXCEPTION] = exception
424
-
425
- if errors = req.env[RACK_ERRORS]
426
- errors.write(_dump_exception(exception))
427
- errors.flush
428
- end
429
- end
430
-
431
- # @since 0.2.0
432
- # @api private
433
- def _dump_exception(exception)
434
- [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
435
- end
436
-
437
- # @since 0.1.0
438
- # @api private
439
- def _handle_exception(req, res, exception)
440
- handler = exception_handler(exception)
441
-
442
- if handler.nil?
443
- _reference_in_rack_errors(req, exception)
444
- raise exception
445
- end
446
-
447
- instance_exec(
448
- req,
449
- res,
450
- exception,
451
- &_exception_handler(handler)
452
- )
453
-
454
- nil
455
- end
456
-
457
- # @since 0.3.0
458
- # @api private
459
- def _exception_handler(handler)
460
- if respond_to?(handler.to_s, true)
461
- method(handler)
462
- else
463
- ->(*) { halt handler }
464
- end
465
- end
466
-
467
- # @since 0.1.0
468
- # @api private
469
- def _run_before_callbacks(*args)
470
- self.class.before_callbacks.run(self, *args)
471
- nil
472
- end
473
-
474
- # @since 0.1.0
475
- # @api private
476
- def _run_after_callbacks(*args)
477
- self.class.after_callbacks.run(self, *args)
478
- nil
479
- end
480
-
481
- # According to RFC 2616, when a response MUST have an empty body, it only
482
- # allows Entity Headers.
483
- #
484
- # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
485
- # other custom header.
486
- #
487
- # This restriction is enforced by <tt>Hanami::Action#_requires_no_body?</tt>.
488
- #
489
- # However, there are cases that demand to bypass this rule to set meta
490
- # informations via headers.
491
- #
492
- # An example is a <tt>DELETE</tt> request for a JSON API application.
493
- # It returns a <tt>204</tt> but still wants to specify the rate limit
494
- # quota via <tt>X-Rate-Limit</tt>.
495
- #
496
- # @since 0.5.0
497
- #
498
- # @see Hanami::Action#_requires_no_body?
499
- #
500
- # @example
501
- # require 'hanami/controller'
502
- #
503
- # module Books
504
- # class Destroy
505
- # include Hanami::Action
506
- #
507
- # def call(params)
508
- # # ...
509
- # self.headers.merge!(
510
- # 'Last-Modified' => 'Fri, 27 Nov 2015 13:32:36 GMT',
511
- # 'X-Rate-Limit' => '4000',
512
- # 'Content-Type' => 'application/json',
513
- # 'X-No-Pass' => 'true'
514
- # )
515
- #
516
- # self.status = 204
517
- # end
518
- #
519
- # private
520
- #
521
- # def keep_response_header?(header)
522
- # super || header == 'X-Rate-Limit'
523
- # end
524
- # end
525
- # end
526
- #
527
- # # Only the following headers will be sent:
528
- # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
529
- # # * X-Rate-Limit - because we explicitely allow it
530
- #
531
- # # Both Content-Type and X-No-Pass are removed because they're not allowed
532
- def keep_response_header?(header)
533
- ENTITY_HEADERS.include?(header)
534
- end
535
-
536
- # @since 2.0.0
537
- # @api private
538
- def _empty_headers(res)
539
- res.headers.select! { |header, _| keep_response_header?(header) }
540
- end
541
-
542
- def format(value)
543
- case value
544
- when Symbol
545
- format = Utils::Kernel.Symbol(value)
546
- [format, Action::Mime.format_to_mime_type(format, configuration)]
547
- when String
548
- [Action::Mime.detect_format(value, configuration), value]
549
- else
550
- raise Hanami::Controller::UnknownFormatError.new(value)
551
- end
552
- end
553
-
554
- # Finalize the response
555
- #
556
- # Prepare the data before the response will be returned to the webserver
557
- #
558
- # @since 0.1.0
559
- # @api private
560
- # @abstract
561
- #
562
- # @see Hanami::Action::Session#finish
563
- # @see Hanami::Action::Cookies#finish
564
- # @see Hanami::Action::Cache#finish
565
- def finish(req, res, halted)
566
- res.status, res.body = *halted unless halted.nil?
567
-
568
- _empty_headers(res) if _requires_empty_headers?(res)
569
-
570
- res.set_format(Action::Mime.detect_format(res.content_type, configuration))
571
- res[:params] = req.params
572
- res[:format] = res.format
573
- res
574
- end
575
- end
576
- end
577
- end
578
- end