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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f14c59426ea7017dc546100e0443d17d5f63362cd4b6f1546adfb18ae6bd4e3
4
- data.tar.gz: 62fafda341ad801430ab2575df6a28b6a9848a71739c977e481fe8fa00dbadfc
3
+ metadata.gz: 9f2dcbf06a2ab59f4e97934102b22e61e6ae0d8998c1489b5e30a610357a5da3
4
+ data.tar.gz: 16c6d14e8ed4eb97ba2aca1c24263f46129d84738133a1973cebfd2d0a09a300
5
5
  SHA512:
6
- metadata.gz: a51c5a37c6961bff7d1c3c30906cc2e072029e6d0b4ec5782ef92d592ed4f8b247807b9aabd803004afb31fd2f23900bb8a34c9d29bc1515e30e0caba4b3318a
7
- data.tar.gz: b69d515d608fbaf545e7d2d4e6d19fbd6d305bc10bcc0a2208dc6e740389eb7188514112028d02dd3d8e6729192eed95305b7ec3a09cdd77039b55ac5d054083
6
+ metadata.gz: a13ccd638bda53b97a5ec4d12a1f3815305e674a4321ec239764ef0093ba580949be8d5efc2e1fdc7175b71b350c01f2d92defd2e460b18c4e2e2d36add171ea
7
+ data.tar.gz: 11487c281a8331e5d5447f68b120b0f5dc582a5a93e9fbf8d2436bb0cee2f55f5f7ff51c6576792cab79d1be1760585c9bbbf16c019323a6701e8c0d233266ed
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Hanami::Controller
2
2
  Complete, fast and testable actions for Rack
3
3
 
4
+ ## v2.0.0.alpha8 - 2022-02-19
5
+
6
+ ### Changed
7
+ - [Tim Riley] Removed automatic integration of `Hanami::Action` subclasses with their surrounding Hanami application. Action base classes within Hanami apps should inherit from `Hanami::Application::Action` instead.
8
+
4
9
  ## v2.0.0.alpha6 - 2022-02-10
5
10
  ### Added
6
11
  - [Luca Guidi] Official support for Ruby: MRI 3.1
data/README.md CHANGED
@@ -33,7 +33,7 @@ __Hanami::Controller__ supports Ruby (MRI) 3.0+
33
33
  Add this line to your application's Gemfile:
34
34
 
35
35
  ```ruby
36
- gem "hanami/controller"
36
+ gem "hanami-controller"
37
37
  ```
38
38
 
39
39
  And then execute:
data/lib/hanami/action.rb CHANGED
@@ -1,5 +1,23 @@
1
- require_relative 'action/application_action'
2
- require_relative 'action/standalone_action'
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 'action/base_params'
15
+ require_relative 'action/configuration'
16
+ require_relative 'action/halt'
17
+ require_relative 'action/mime'
18
+ require_relative 'action/rack/file'
19
+ require_relative 'action/request'
20
+ require_relative 'action/response'
3
21
 
4
22
  module Hanami
5
23
  # An HTTP endpoint
@@ -144,24 +162,545 @@ module Hanami
144
162
  # @api private
145
163
  LOCATION = 'Location'.freeze
146
164
 
147
- include StandaloneAction
148
-
165
+ # Override Ruby's hook for modules.
166
+ # It includes basic Hanami::Action modules to the given class.
167
+ #
168
+ # @param subclass [Class] the target action
169
+ #
170
+ # @since 0.1.0
171
+ # @api private
149
172
  def self.inherited(subclass)
150
- super
173
+ if subclass.superclass == Action
174
+ subclass.class_eval do
175
+ include Utils::ClassAttribute
176
+
177
+ class_attribute :before_callbacks
178
+ self.before_callbacks = Utils::Callbacks::Chain.new
179
+
180
+ class_attribute :after_callbacks
181
+ self.after_callbacks = Utils::Callbacks::Chain.new
182
+
183
+ include Validatable if defined?(Validatable)
184
+ end
185
+ end
186
+
187
+ subclass.instance_variable_set '@configuration', configuration.dup
188
+ end
189
+
190
+ def self.configuration
191
+ @configuration ||= Configuration.new
192
+ end
193
+
194
+ class << self
195
+ alias_method :config, :configuration
196
+ end
197
+
198
+ # Returns the class which defines the params
199
+ #
200
+ # Returns the class which has been provided to define the
201
+ # params. By default this will be Hanami::Action::Params.
202
+ #
203
+ # @return [Class] A params class (when whitelisted) or
204
+ # Hanami::Action::Params
205
+ #
206
+ # @api private
207
+ # @since 0.7.0
208
+ def self.params_class
209
+ @params_class ||= BaseParams
210
+ end
211
+
212
+ # FIXME: make this thread-safe
213
+ def self.accepted_formats
214
+ @accepted_formats ||= []
215
+ end
216
+
217
+ # Define a callback for an Action.
218
+ # The callback will be executed **before** the action is called, in the
219
+ # order they are added.
220
+ #
221
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
222
+ # each of them is representing a name of a method available in the
223
+ # context of the Action.
224
+ #
225
+ # @param blk [Proc] an anonymous function to be executed
226
+ #
227
+ # @return [void]
228
+ #
229
+ # @since 0.3.2
230
+ #
231
+ # @see Hanami::Action::Callbacks::ClassMethods#append_after
232
+ #
233
+ # @example Method names (symbols)
234
+ # require 'hanami/controller'
235
+ #
236
+ # class Show
237
+ # include Hanami::Action
238
+ #
239
+ # before :authenticate, :set_article
240
+ #
241
+ # def call(params)
242
+ # end
243
+ #
244
+ # private
245
+ # def authenticate
246
+ # # ...
247
+ # end
248
+ #
249
+ # # `params` in the method signature is optional
250
+ # def set_article(params)
251
+ # @article = Article.find params[:id]
252
+ # end
253
+ # end
254
+ #
255
+ # # The order of execution will be:
256
+ # #
257
+ # # 1. #authenticate
258
+ # # 2. #set_article
259
+ # # 3. #call
260
+ #
261
+ # @example Anonymous functions (Procs)
262
+ # require 'hanami/controller'
263
+ #
264
+ # class Show
265
+ # include Hanami::Action
266
+ #
267
+ # before { ... } # 1 do some authentication stuff
268
+ # before {|params| @article = Article.find params[:id] } # 2
269
+ #
270
+ # def call(params)
271
+ # end
272
+ # end
273
+ #
274
+ # # The order of execution will be:
275
+ # #
276
+ # # 1. authentication
277
+ # # 2. set the article
278
+ # # 3. #call
279
+ def self.append_before(*callbacks, &blk)
280
+ before_callbacks.append(*callbacks, &blk)
281
+ end
282
+
283
+ class << self
284
+ # @since 0.1.0
285
+ alias_method :before, :append_before
286
+ end
287
+
288
+ # Define a callback for an Action.
289
+ # The callback will be executed **after** the action is called, in the
290
+ # order they are added.
291
+ #
292
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
293
+ # each of them is representing a name of a method available in the
294
+ # context of the Action.
295
+ #
296
+ # @param blk [Proc] an anonymous function to be executed
297
+ #
298
+ # @return [void]
299
+ #
300
+ # @since 0.3.2
301
+ #
302
+ # @see Hanami::Action::Callbacks::ClassMethods#append_before
303
+ def self.append_after(*callbacks, &blk)
304
+ after_callbacks.append(*callbacks, &blk)
305
+ end
306
+
307
+ class << self
308
+ # @since 0.1.0
309
+ alias_method :after, :append_after
310
+ end
311
+
312
+ # Define a callback for an Action.
313
+ # The callback will be executed **before** the action is called.
314
+ # It will add the callback at the beginning of the callbacks' chain.
315
+ #
316
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
317
+ # each of them is representing a name of a method available in the
318
+ # context of the Action.
319
+ #
320
+ # @param blk [Proc] an anonymous function to be executed
321
+ #
322
+ # @return [void]
323
+ #
324
+ # @since 0.3.2
325
+ #
326
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_after
327
+ def self.prepend_before(*callbacks, &blk)
328
+ before_callbacks.prepend(*callbacks, &blk)
329
+ end
330
+
331
+ # Define a callback for an Action.
332
+ # The callback will be executed **after** the action is called.
333
+ # It will add the callback at the beginning of the callbacks' chain.
334
+ #
335
+ # @param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
336
+ # each of them is representing a name of a method available in the
337
+ # context of the Action.
338
+ #
339
+ # @param blk [Proc] an anonymous function to be executed
340
+ #
341
+ # @return [void]
342
+ #
343
+ # @since 0.3.2
344
+ #
345
+ # @see Hanami::Action::Callbacks::ClassMethods#prepend_before
346
+ def self.prepend_after(*callbacks, &blk)
347
+ after_callbacks.prepend(*callbacks, &blk)
348
+ end
349
+
350
+ # Restrict the access to the specified mime type symbols.
351
+ #
352
+ # @param formats[Array<Symbol>] one or more symbols representing mime type(s)
353
+ #
354
+ # @raise [Hanami::Controller::UnknownFormatError] if the symbol cannot
355
+ # be converted into a mime type
356
+ #
357
+ # @since 0.1.0
358
+ #
359
+ # @see Hanami::Controller::Configuration#format
360
+ #
361
+ # @example
362
+ # require 'hanami/controller'
363
+ #
364
+ # class Show
365
+ # include Hanami::Action
366
+ # accept :html, :json
367
+ #
368
+ # def call(params)
369
+ # # ...
370
+ # end
371
+ # end
372
+ #
373
+ # # When called with "*/*" => 200
374
+ # # When called with "text/html" => 200
375
+ # # When called with "application/json" => 200
376
+ # # When called with "application/xml" => 406
377
+ def self.accept(*formats)
378
+ @accepted_formats = *formats
379
+ before :enforce_accepted_mime_types
380
+ end
381
+
382
+ # Returns a new action
383
+ #
384
+ # @overload new(**deps, ...)
385
+ # @param deps [Hash] action dependencies
386
+ #
387
+ # @overload new(configuration:, **deps, ...)
388
+ # @param configuration [Hanami::Controller::Configuration] action configuration
389
+ # @param deps [Hash] action dependencies
390
+ #
391
+ # @return [Hanami::Action] Action object
392
+ #
393
+ # @since 2.0.0
394
+ def self.new(*args, configuration: self.configuration, **kwargs, &block)
395
+ allocate.tap do |obj|
396
+ obj.instance_variable_set(:@name, Name[name])
397
+ obj.instance_variable_set(:@configuration, configuration.dup.finalize!)
398
+ obj.instance_variable_set(:@accepted_mime_types, Mime.restrict_mime_types(configuration, accepted_formats))
399
+ obj.send(:initialize, *args, **kwargs, &block)
400
+ obj.freeze
401
+ end
402
+ end
403
+
404
+ module Name
405
+ MODULE_SEPARATOR_TRANSFORMER = [:gsub, "::", "."].freeze
406
+
407
+ def self.call(name)
408
+ Utils::String.transform(name, MODULE_SEPARATOR_TRANSFORMER, :underscore) unless name.nil?
409
+ end
151
410
 
152
- # When inheriting within an Hanami app, and the application provider has
153
- # changed from the superclass, (re-)configure the action for the provider,
154
- # i.e. for the slice and/or the application itself
155
- if (provider = application_provider(subclass)) && provider != application_provider(subclass.superclass)
156
- subclass.include ApplicationAction.new(provider)
411
+ class << self
412
+ alias_method :[], :call
157
413
  end
158
414
  end
159
415
 
160
- def self.application_provider(subclass)
161
- if Hanami.respond_to?(:application?) && Hanami.application?
162
- Hanami.application.component_provider(subclass)
416
+ attr_reader :name
417
+
418
+ # Implements the Rack/Hanami::Action protocol
419
+ #
420
+ # @since 0.1.0
421
+ # @api private
422
+ def call(env)
423
+ request = nil
424
+ response = nil
425
+
426
+ halted = catch :halt do
427
+ begin
428
+ params = self.class.params_class.new(env)
429
+ request = build_request(env, params)
430
+ response = build_response(
431
+ request: request,
432
+ action: name,
433
+ configuration: configuration,
434
+ content_type: Mime.calculate_content_type_with_charset(configuration, request, accepted_mime_types),
435
+ env: env,
436
+ headers: configuration.default_headers
437
+ )
438
+
439
+ _run_before_callbacks(request, response)
440
+ handle(request, response)
441
+ _run_after_callbacks(request, response)
442
+ rescue => exception
443
+ _handle_exception(request, response, exception)
444
+ end
163
445
  end
446
+
447
+ finish(request, response, halted)
448
+ end
449
+
450
+ def initialize(**deps)
451
+ @_deps = deps
452
+ end
453
+
454
+ protected
455
+
456
+ # Hook for subclasses to apply behavior as part of action invocation
457
+ #
458
+ # @param request [Hanami::Action::Request]
459
+ # @param response [Hanami::Action::Response]
460
+ #
461
+ # @since 2.0.0
462
+ def handle(request, response)
463
+ end
464
+
465
+ # Halt the action execution with the given HTTP status code and message.
466
+ #
467
+ # When used, the execution of a callback or of an action is interrupted
468
+ # and the control returns to the framework, that decides how to handle
469
+ # the event.
470
+ #
471
+ # If a message is provided, it sets the response body with the message.
472
+ # Otherwise, it sets the response body with the default message associated
473
+ # to the code (eg 404 will set `"Not Found"`).
474
+ #
475
+ # @param status [Fixnum] a valid HTTP status code
476
+ # @param body [String] the response body
477
+ #
478
+ # @raise [StandardError] if the code isn't valid
479
+ #
480
+ # @since 0.2.0
481
+ #
482
+ # @see Hanami::Action::Throwable#handle_exception
483
+ # @see Hanami::Http::Status:ALL
484
+ #
485
+ # @example Basic usage
486
+ # require 'hanami/controller'
487
+ #
488
+ # class Show
489
+ # def call(params)
490
+ # halt 404
491
+ # end
492
+ # end
493
+ #
494
+ # # => [404, {}, ["Not Found"]]
495
+ #
496
+ # @example Custom message
497
+ # require 'hanami/controller'
498
+ #
499
+ # class Show
500
+ # def call(params)
501
+ # halt 404, "This is not the droid you're looking for."
502
+ # end
503
+ # end
504
+ #
505
+ # # => [404, {}, ["This is not the droid you're looking for."]]
506
+ def halt(status, body = nil)
507
+ Halt.call(status, body)
508
+ end
509
+
510
+ # @since 0.3.2
511
+ # @api private
512
+ def _requires_no_body?(res)
513
+ HTTP_STATUSES_WITHOUT_BODY.include?(res.status)
514
+ end
515
+
516
+ # @since 2.0.0
517
+ # @api private
518
+ def _requires_empty_headers?(res)
519
+ _requires_no_body?(res) || res.head?
520
+ end
521
+
522
+ private
523
+
524
+ attr_reader :configuration
525
+
526
+ def accepted_mime_types
527
+ @accepted_mime_types || configuration.mime_types
528
+ end
529
+
530
+ def enforce_accepted_mime_types(req, *)
531
+ Mime.accepted_mime_type?(req, accepted_mime_types, configuration) or halt 406
532
+ end
533
+
534
+ def exception_handler(exception)
535
+ configuration.handled_exceptions.each do |exception_class, handler|
536
+ return handler if exception.kind_of?(exception_class)
537
+ end
538
+
539
+ nil
540
+ end
541
+
542
+ def build_request(env, params)
543
+ Request.new(env, params)
544
+ end
545
+
546
+ def build_response(**options)
547
+ Response.new(**options)
548
+ end
549
+
550
+ # @since 0.2.0
551
+ # @api private
552
+ def _reference_in_rack_errors(req, exception)
553
+ req.env[RACK_EXCEPTION] = exception
554
+
555
+ if errors = req.env[RACK_ERRORS]
556
+ errors.write(_dump_exception(exception))
557
+ errors.flush
558
+ end
559
+ end
560
+
561
+ # @since 0.2.0
562
+ # @api private
563
+ def _dump_exception(exception)
564
+ [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
565
+ end
566
+
567
+ # @since 0.1.0
568
+ # @api private
569
+ def _handle_exception(req, res, exception)
570
+ handler = exception_handler(exception)
571
+
572
+ if handler.nil?
573
+ _reference_in_rack_errors(req, exception)
574
+ raise exception
575
+ end
576
+
577
+ instance_exec(
578
+ req,
579
+ res,
580
+ exception,
581
+ &_exception_handler(handler)
582
+ )
583
+
584
+ nil
585
+ end
586
+
587
+ # @since 0.3.0
588
+ # @api private
589
+ def _exception_handler(handler)
590
+ if respond_to?(handler.to_s, true)
591
+ method(handler)
592
+ else
593
+ ->(*) { halt handler }
594
+ end
595
+ end
596
+
597
+ # @since 0.1.0
598
+ # @api private
599
+ def _run_before_callbacks(*args)
600
+ self.class.before_callbacks.run(self, *args)
601
+ nil
602
+ end
603
+
604
+ # @since 0.1.0
605
+ # @api private
606
+ def _run_after_callbacks(*args)
607
+ self.class.after_callbacks.run(self, *args)
608
+ nil
609
+ end
610
+
611
+ # According to RFC 2616, when a response MUST have an empty body, it only
612
+ # allows Entity Headers.
613
+ #
614
+ # For instance, a <tt>204</tt> doesn't allow <tt>Content-Type</tt> or any
615
+ # other custom header.
616
+ #
617
+ # This restriction is enforced by <tt>Hanami::Action#_requires_no_body?</tt>.
618
+ #
619
+ # However, there are cases that demand to bypass this rule to set meta
620
+ # informations via headers.
621
+ #
622
+ # An example is a <tt>DELETE</tt> request for a JSON API application.
623
+ # It returns a <tt>204</tt> but still wants to specify the rate limit
624
+ # quota via <tt>X-Rate-Limit</tt>.
625
+ #
626
+ # @since 0.5.0
627
+ #
628
+ # @see Hanami::Action#_requires_no_body?
629
+ #
630
+ # @example
631
+ # require 'hanami/controller'
632
+ #
633
+ # module Books
634
+ # class Destroy
635
+ # include Hanami::Action
636
+ #
637
+ # def call(params)
638
+ # # ...
639
+ # self.headers.merge!(
640
+ # 'Last-Modified' => 'Fri, 27 Nov 2015 13:32:36 GMT',
641
+ # 'X-Rate-Limit' => '4000',
642
+ # 'Content-Type' => 'application/json',
643
+ # 'X-No-Pass' => 'true'
644
+ # )
645
+ #
646
+ # self.status = 204
647
+ # end
648
+ #
649
+ # private
650
+ #
651
+ # def keep_response_header?(header)
652
+ # super || header == 'X-Rate-Limit'
653
+ # end
654
+ # end
655
+ # end
656
+ #
657
+ # # Only the following headers will be sent:
658
+ # # * Last-Modified - because we used `super' in the method that respects the HTTP RFC
659
+ # # * X-Rate-Limit - because we explicitely allow it
660
+ #
661
+ # # Both Content-Type and X-No-Pass are removed because they're not allowed
662
+ def keep_response_header?(header)
663
+ ENTITY_HEADERS.include?(header)
664
+ end
665
+
666
+ # @since 2.0.0
667
+ # @api private
668
+ def _empty_headers(res)
669
+ res.headers.select! { |header, _| keep_response_header?(header) }
670
+ end
671
+
672
+ def format(value)
673
+ case value
674
+ when Symbol
675
+ format = Utils::Kernel.Symbol(value)
676
+ [format, Action::Mime.format_to_mime_type(format, configuration)]
677
+ when String
678
+ [Action::Mime.detect_format(value, configuration), value]
679
+ else
680
+ raise Hanami::Controller::UnknownFormatError.new(value)
681
+ end
682
+ end
683
+
684
+ # Finalize the response
685
+ #
686
+ # Prepare the data before the response will be returned to the webserver
687
+ #
688
+ # @since 0.1.0
689
+ # @api private
690
+ # @abstract
691
+ #
692
+ # @see Hanami::Action::Session#finish
693
+ # @see Hanami::Action::Cookies#finish
694
+ # @see Hanami::Action::Cache#finish
695
+ def finish(req, res, halted)
696
+ res.status, res.body = *halted unless halted.nil?
697
+
698
+ _empty_headers(res) if _requires_empty_headers?(res)
699
+
700
+ res.set_format(Action::Mime.detect_format(res.content_type, configuration))
701
+ res[:params] = req.params
702
+ res[:format] = res.format
703
+ res
164
704
  end
165
- private_class_method :application_provider
166
705
  end
167
706
  end
@@ -3,6 +3,6 @@ module Hanami
3
3
  # Defines the version
4
4
  #
5
5
  # @since 0.1.0
6
- VERSION = '2.0.0.alpha6'.freeze
6
+ VERSION = '2.0.0.alpha8'.freeze
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-controller
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.alpha6
4
+ version: 2.0.0.alpha8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-10 00:00:00.000000000 Z
11
+ date: 2022-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -132,11 +132,6 @@ files:
132
132
  - README.md
133
133
  - hanami-controller.gemspec
134
134
  - lib/hanami/action.rb
135
- - lib/hanami/action/application_action.rb
136
- - lib/hanami/action/application_configuration.rb
137
- - lib/hanami/action/application_configuration/content_security_policy.rb
138
- - lib/hanami/action/application_configuration/cookies.rb
139
- - lib/hanami/action/application_configuration/sessions.rb
140
135
  - lib/hanami/action/base_params.rb
141
136
  - lib/hanami/action/cache.rb
142
137
  - lib/hanami/action/cache/cache_control.rb
@@ -156,7 +151,6 @@ files:
156
151
  - lib/hanami/action/request.rb
157
152
  - lib/hanami/action/response.rb
158
153
  - lib/hanami/action/session.rb
159
- - lib/hanami/action/standalone_action.rb
160
154
  - lib/hanami/action/validatable.rb
161
155
  - lib/hanami/action/view_name_inferrer.rb
162
156
  - lib/hanami/controller.rb
@@ -183,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
177
  - !ruby/object:Gem::Version
184
178
  version: 1.3.1
185
179
  requirements: []
186
- rubygems_version: 3.3.3
180
+ rubygems_version: 3.3.7
187
181
  signing_key:
188
182
  specification_version: 4
189
183
  summary: Complete, fast and testable actions for Rack and Hanami