hanami-controller 2.0.0.alpha6 → 2.0.0.alpha8

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 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