hanami-controller 2.2.0 → 2.3.0.beta1

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: d36e2731984ff92e89c99426d31d8e7e2fdb99712bae1b8aaaab7d2eaa46bb5d
4
- data.tar.gz: 130d03fa13e2c842c957b29122e8787b6d93d09fda984261b917ca8dc39cc25a
3
+ metadata.gz: 16424b402710c09cc6727cf6ab94c84469edaa5d8e93f12d18c1f058cb973f00
4
+ data.tar.gz: '0639c98ced9c6f1cca83891bd74650f8dc6856e64490aa3a3321069810cd86a7'
5
5
  SHA512:
6
- metadata.gz: 75223bbc1df85cd7bd7edd396a1962c0b2c87bc1c08d1afec36918b845b701899ee25ec504b72d601b793f96138da46a3a2725f67730288b8093b75890d2b1b9
7
- data.tar.gz: 4cb14463bb01959827b13e2975d82860dca97587f87e1d940956376bfe295279d591baed2179c34c943a3c0e44ed8761c10f0685421c89ae79bb2178c546fc9e
6
+ metadata.gz: e353e4df54518fcdc9fc2ae60262b839772fdcc180335d56c3f451a26766dcfb31567ca6a45b852ffabe92e40638cdb1f259d55bb1539ec315bc15c2bfe70de7
7
+ data.tar.gz: 0434befd1175d44411ad2aa5b315ba5c212c69d47dbb2c6c990733ca07f1c4c3d1954139f21c0cd6b35d236f5be9f4b9293e286f50f189a3eac24069435fca1a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  Complete, fast and testable actions for Rack
4
4
 
5
+ ## v2.3.0.beta1 - 2025-10-03
6
+
7
+ ### Fixed
8
+
9
+ - [wuarmin] Avoid false negatives in format/content type matches by checking against the request's media type, which excludes content type parameters (e.g. "test/plain" instead of "text/plain;charset=utf-8") (#471)
10
+
11
+ ### Added
12
+
13
+ - [Wout] Add `Request#subdomains`, returning an array of subdomains for the current host, and `Request#subdomain` returning a dot-delimited subdomain string for the current host. Add `config.default_tld_length` setting for configuring the TLD length for your app's expected domain (#481)
14
+
15
+ ### Changed
16
+
17
+ - [Kyle Plump, Tim Riley] Support Rack 3 in addition to Rack 2 (#460)
18
+ - [Tim Riley] `request.session` is now an instance of `Hanami::Action::Request::Session`, which wraps the session object and provides access to session values via symbol keys. This was previously handled via symbolizing and reassigning the entire session hash, which is not compatible with Rack 3 (#477)
19
+
5
20
  ## v2.2.0 - 2024-11-05
6
21
 
7
22
  ### Added
data/README.md CHANGED
@@ -54,8 +54,8 @@ They are the endpoints that respond to incoming HTTP requests.
54
54
 
55
55
  ```ruby
56
56
  class Show < Hanami::Action
57
- def handle(req, res)
58
- res[:article] = ArticleRepository.new.find(req.params[:id])
57
+ def handle(request, response)
58
+ response[:article] = ArticleRepository.new.find(request.params[:id])
59
59
  end
60
60
  end
61
61
  ```
@@ -63,7 +63,7 @@ end
63
63
  `Hanami::Action` follows the Hanami philosophy: a single purpose object with a minimal interface.
64
64
 
65
65
  In this case, `Hanami::Action` provides the key public interface of `#call(env)`, making your actions Rack-compatible.
66
- To provide custom behaviour when your actions are being called, you can implement `#handle(req, res)`
66
+ To provide custom behaviour when your actions are being called, you can implement `#handle(request, response)`
67
67
 
68
68
  **An action is an object** and **you have full control over it**.
69
69
  In other words, you have the freedom to instantiate, inject dependencies and test it, both at the unit and integration level.
@@ -79,8 +79,8 @@ class Show < Hanami::Action
79
79
  super(configuration: configuration)
80
80
  end
81
81
 
82
- def handle(req, res)
83
- res[:article] = repository.find(req.params[:id])
82
+ def handle(request, response)
83
+ response[:article] = repository.find(request.params[:id])
84
84
  end
85
85
 
86
86
  private
@@ -96,16 +96,16 @@ action.call(id: 23)
96
96
  ### Params
97
97
 
98
98
  The request params are part of the request passed as an argument to the `#handle` method.
99
- If routed with *Hanami::Router*, it extracts the relevant bits from the Rack `env` (eg the requested `:id`).
99
+ If routed with *Hanami::Router*, it extracts the relevant bits from the Rack `env` (e.g. the requested `:id`).
100
100
  Otherwise everything is passed as is: the full Rack `env` in production, and the given `Hash` for unit tests.
101
101
 
102
102
  With `Hanami::Router`:
103
103
 
104
104
  ```ruby
105
105
  class Show < Hanami::Action
106
- def handle(req, *)
106
+ def handle(request, *)
107
107
  # ...
108
- puts req.params # => { id: 23 } extracted from Rack env
108
+ puts request.params # => { id: 23 } extracted from Rack env
109
109
  end
110
110
  end
111
111
  ```
@@ -114,9 +114,9 @@ Standalone:
114
114
 
115
115
  ```ruby
116
116
  class Show < Hanami::Action
117
- def handle(req, *)
117
+ def handle(request, *)
118
118
  # ...
119
- puts req.params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
119
+ puts request.params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
120
120
  end
121
121
  end
122
122
  ```
@@ -125,9 +125,9 @@ Unit Testing:
125
125
 
126
126
  ```ruby
127
127
  class Show < Hanami::Action
128
- def handle(req, *)
128
+ def handle(request, *)
129
129
  # ...
130
- puts req.params # => { id: 23, key: "value" } passed as it is from testing
130
+ puts request.params # => { id: 23, key: "value" } passed as it is from testing
131
131
  end
132
132
  end
133
133
 
@@ -157,18 +157,18 @@ class Signup < Hanami::Action
157
157
  end
158
158
  end
159
159
 
160
- def handle(req, *)
160
+ def handle(request, *)
161
161
  # Describe inheritance hierarchy
162
- puts req.params.class # => Signup::Params
163
- puts req.params.class.superclass # => Hanami::Action::Params
162
+ puts request.params.class # => Signup::Params
163
+ puts request.params.class.superclass # => Hanami::Action::Params
164
164
 
165
165
  # Whitelist :first_name, but not :admin
166
- puts req.params[:first_name] # => "Luca"
167
- puts req.params[:admin] # => nil
166
+ puts request.params[:first_name] # => "Luca"
167
+ puts request.params[:admin] # => nil
168
168
 
169
169
  # Whitelist nested params [:address][:line_one], not [:address][:line_two]
170
- puts req.params[:address][:line_one] # => "69 Tender St"
171
- puts req.params[:address][:line_two] # => nil
170
+ puts request.params[:address][:line_one] # => "69 Tender St"
171
+ puts request.params[:address][:line_two] # => nil
172
172
  end
173
173
  end
174
174
  ```
@@ -198,8 +198,8 @@ class Signup < Hanami::Action
198
198
  optional(:avatar).filled(size?: 1..(MEGABYTE * 3))
199
199
  end
200
200
 
201
- def handle(req, *)
202
- halt 400 unless req.params.valid?
201
+ def handle(request, *)
202
+ halt 400 unless request.params.valid?
203
203
  # ...
204
204
  end
205
205
  end
@@ -217,14 +217,14 @@ action = Show.new(configuration: configuration)
217
217
  action.call({}) # => #<Hanami::Action::Response:0x00007fe8be968418 @status=200 ...>
218
218
  ```
219
219
 
220
- This is the same `res` response object passed to `#handle`, where you can use its accessors to explicitly set status, headers, and body:
220
+ This is the same `response` object passed to `#handle`, where you can use its accessors to explicitly set status, headers, and body:
221
221
 
222
222
  ```ruby
223
223
  class Show < Hanami::Action
224
- def handle(*, res)
225
- res.status = 201
226
- res.body = "Hi!"
227
- res.headers.merge!("X-Custom" => "OK")
224
+ def handle(*, response)
225
+ response.status = 201
226
+ response.body = "Hi!"
227
+ response.headers.merge!("X-Custom" => "OK")
228
228
  end
229
229
  end
230
230
 
@@ -239,8 +239,8 @@ By default, an action exposes the received params.
239
239
 
240
240
  ```ruby
241
241
  class Show < Hanami::Action
242
- def handle(req, res)
243
- res[:article] = ArticleRepository.new.find(req.params[:id])
242
+ def handle(request, response)
243
+ response[:article] = ArticleRepository.new.find(request.params[:id])
244
244
  end
245
245
  end
246
246
 
@@ -272,9 +272,9 @@ class Show < Hanami::Action
272
272
  # ...
273
273
  end
274
274
 
275
- # `req` and `res` in the method signature is optional
276
- def set_article(req, res)
277
- res[:article] = ArticleRepository.new.find(req.params[:id])
275
+ # `request` and `response` in the method signature is optional
276
+ def set_article(request, response)
277
+ response[:article] = ArticleRepository.new.find(request.params[:id])
278
278
  end
279
279
  end
280
280
  ```
@@ -284,7 +284,7 @@ Callbacks can also be expressed as anonymous lambdas:
284
284
  ```ruby
285
285
  class Show < Hanami::Action
286
286
  before { ... } # do some authentication stuff
287
- before { |req, res| res[:article] = ArticleRepository.new.find(req.params[:id]) }
287
+ before { |request, response| response[:article] = ArticleRepository.new.find(request.params[:id]) }
288
288
 
289
289
  def handle(*)
290
290
  end
@@ -332,15 +332,15 @@ You can also define custom handlers for exceptions.
332
332
  class Create < Hanami::Action
333
333
  handle_exception ArgumentError => :my_custom_handler
334
334
 
335
- gle(*)
335
+ def handle(*)
336
336
  raise ArgumentError.new("Invalid arguments")
337
337
  end
338
338
 
339
339
  private
340
340
 
341
- def my_custom_handler(req, res, exception)
342
- res.status = 400
343
- res.body = exception.message
341
+ def my_custom_handler(request, response, exception)
342
+ response.status = 400
343
+ response.body = exception.message
344
344
  end
345
345
  end
346
346
 
@@ -381,7 +381,7 @@ module Articles
381
381
 
382
382
  private
383
383
 
384
- def handle_my_exception(req, res, exception)
384
+ def handle_my_exception(request, response, exception)
385
385
  # ...
386
386
  end
387
387
  end
@@ -395,7 +395,7 @@ module Articles
395
395
 
396
396
  private
397
397
 
398
- def handle_standard_error(req, res, exception)
398
+ def handle_standard_error(request, response, exception)
399
399
  # ...
400
400
  end
401
401
  end
@@ -433,8 +433,8 @@ Alternatively, you can specify a custom message.
433
433
 
434
434
  ```ruby
435
435
  class Show < Hanami::Action
436
- def handle(req, res)
437
- res[:droid] = DroidRepository.new.find(req.params[:id]) or not_found
436
+ def handle(request, response)
437
+ response[:droid] = DroidRepository.new.find(request.params[:id]) or not_found
438
438
  end
439
439
 
440
440
  private
@@ -450,8 +450,8 @@ action.call({}) # => [404, {}, ["This is not the droid you're looking for"]]
450
450
 
451
451
  ### Cookies
452
452
 
453
- You can read the original cookies sent from the HTTP client via `req.cookies`.
454
- If you want to send cookies in the response, use `res.cookies`.
453
+ You can read the original cookies sent from the HTTP client via `request.cookies`.
454
+ If you want to send cookies in the response, use `response.cookies`.
455
455
 
456
456
  They are read as a Hash from Rack env:
457
457
 
@@ -462,9 +462,9 @@ require "hanami/action/cookies"
462
462
  class ReadCookiesFromRackEnv < Hanami::Action
463
463
  include Hanami::Action::Cookies
464
464
 
465
- def handle(req, *)
465
+ def handle(request, *)
466
466
  # ...
467
- req.cookies[:foo] # => "bar"
467
+ request.cookies[:foo] # => "bar"
468
468
  end
469
469
  end
470
470
 
@@ -481,9 +481,9 @@ require "hanami/action/cookies"
481
481
  class SetCookies < Hanami::Action
482
482
  include Hanami::Action::Cookies
483
483
 
484
- def handle(*, res)
484
+ def handle(*, response)
485
485
  # ...
486
- res.cookies[:foo] = "bar"
486
+ response.cookies[:foo] = "bar"
487
487
  end
488
488
  end
489
489
 
@@ -500,9 +500,9 @@ require "hanami/action/cookies"
500
500
  class RemoveCookies < Hanami::Action
501
501
  include Hanami::Action::Cookies
502
502
 
503
- def handle(*, res)
503
+ def handle(*, response)
504
504
  # ...
505
- res.cookies[:foo] = nil
505
+ response.cookies[:foo] = nil
506
506
  end
507
507
  end
508
508
 
@@ -523,9 +523,9 @@ end
523
523
  class SetCookies < Hanami::Action
524
524
  include Hanami::Action::Cookies
525
525
 
526
- def handle(*, res)
526
+ def handle(*, response)
527
527
  # ...
528
- res.cookies[:foo] = { value: "bar", max_age: 100 }
528
+ response.cookies[:foo] = { value: "bar", max_age: 100 }
529
529
  end
530
530
  end
531
531
 
@@ -537,7 +537,7 @@ action.call({}) # => [200, {"Set-Cookie" => "foo=bar; max-age=100;"}, "..."]
537
537
 
538
538
  Actions have builtin support for Rack sessions.
539
539
  Similarly to cookies, you can read the session sent by the HTTP client via
540
- `req.session`, and also manipulate it via `res.ression`.
540
+ `request.session`, and also manipulate it via `response.session`.
541
541
 
542
542
  ```ruby
543
543
  require "hanami/controller"
@@ -546,9 +546,9 @@ require "hanami/action/session"
546
546
  class ReadSessionFromRackEnv < Hanami::Action
547
547
  include Hanami::Action::Session
548
548
 
549
- def handle(req, *)
549
+ def handle(request, *)
550
550
  # ...
551
- req.session[:age] # => "35"
551
+ request.session[:age] # => "35"
552
552
  end
553
553
  end
554
554
 
@@ -565,9 +565,9 @@ require "hanami/action/session"
565
565
  class SetSession < Hanami::Action
566
566
  include Hanami::Action::Session
567
567
 
568
- def handle(*, res)
568
+ def handle(*, response)
569
569
  # ...
570
- res.session[:age] = 31
570
+ response.session[:age] = 31
571
571
  end
572
572
  end
573
573
 
@@ -584,9 +584,9 @@ require "hanami/action/session"
584
584
  class RemoveSession < Hanami::Action
585
585
  include Hanami::Action::Session
586
586
 
587
- def handle(*, res)
587
+ def handle(*, response)
588
588
  # ...
589
- res.session[:age] = nil
589
+ response.session[:age] = nil
590
590
  end
591
591
  end
592
592
 
@@ -663,7 +663,7 @@ end
663
663
 
664
664
  If `resource.cache_key` is equal to `IfNoneMatch` header, then hanami will `halt 304`.
665
665
 
666
- An alterative to hashing based check, is the time based check:
666
+ An alternative to hashing based check, is the time based check:
667
667
 
668
668
  ```ruby
669
669
  require "hanami/controller"
@@ -674,23 +674,23 @@ class ConditionalGetController < Hanami::Action
674
674
 
675
675
  def handle(*)
676
676
  # ...
677
- fresh last_modified: resource.update_at
678
- # => halt 304 with header IfModifiedSince = resource.update_at.httpdate
677
+ fresh last_modified: resource.updated_at
678
+ # => halt 304 with header IfModifiedSince = resource.updated_at.httpdate
679
679
  end
680
680
  end
681
681
  ```
682
682
 
683
- If `resource.update_at` is equal to `IfModifiedSince` header, then hanami will `halt 304`.
683
+ If `resource.updated_at` is equal to `IfModifiedSince` header, then hanami will `halt 304`.
684
684
 
685
685
  ### Redirect
686
686
 
687
- If you need to redirect the client to another resource, use `res.redirect_to`:
687
+ If you need to redirect the client to another resource, use `response.redirect_to`:
688
688
 
689
689
  ```ruby
690
690
  class Create < Hanami::Action
691
- def handle(*, res)
691
+ def handle(*, response)
692
692
  # ...
693
- res.redirect_to "http://example.com/articles/23"
693
+ response.redirect_to "http://example.com/articles/23"
694
694
  end
695
695
  end
696
696
 
@@ -702,9 +702,9 @@ You can also redirect with a custom status code:
702
702
 
703
703
  ```ruby
704
704
  class Create < Hanami::Action
705
- def handle(*, res)
705
+ def handle(*, response)
706
706
  # ...
707
- res.redirect_to "http://example.com/articles/23", status: 301
707
+ response.redirect_to "http://example.com/articles/23", status: 301
708
708
  end
709
709
  end
710
710
 
@@ -735,9 +735,9 @@ However, you can force this value:
735
735
 
736
736
  ```ruby
737
737
  class Show < Hanami::Action
738
- def handle(*, res)
738
+ def handle(*, response)
739
739
  # ...
740
- res.format = :json
740
+ response.format = :json
741
741
  end
742
742
  end
743
743
 
@@ -761,7 +761,7 @@ class Show < Hanami::Action
761
761
  end
762
762
  end
763
763
 
764
- # When called with "\*/\*" => 200
764
+ # When called with "*/*" => 200
765
765
  # When called with "text/html" => 200
766
766
  # When called with "application/json" => 200
767
767
  # When called with "application/xml" => 415
@@ -771,23 +771,22 @@ You can check if the requested MIME type is accepted by the client.
771
771
 
772
772
  ```ruby
773
773
  class Show < Hanami::Action
774
- def handle(req, res)
774
+ def handle(request, response)
775
775
  # ...
776
776
  # @_env["HTTP_ACCEPT"] # => "text/html,application/xhtml+xml,application/xml;q=0.9"
777
777
 
778
- req.accept?("text/html") # => true
779
- req.accept?("application/xml") # => true
780
- req.accept?("application/json") # => false
781
- res.format # :html
782
-
778
+ request.accept?("text/html") # => true
779
+ request.accept?("application/xml") # => true
780
+ request.accept?("application/json") # => false
781
+ response.format # :html
783
782
 
784
783
 
785
784
  # @_env["HTTP_ACCEPT"] # => "*/*"
786
785
 
787
- req.accept?("text/html") # => true
788
- req.accept?("application/xml") # => true
789
- req.accept?("application/json") # => true
790
- res.format # :html
786
+ request.accept?("text/html") # => true
787
+ request.accept?("application/xml") # => true
788
+ request.accept?("application/json") # => true
789
+ response.format # :html
791
790
  end
792
791
  end
793
792
  ```
@@ -811,9 +810,9 @@ response = action.call({ "HTTP_ACCEPT" => "application/custom" }) # => Content-T
811
810
  response.format # => :custom
812
811
 
813
812
  class Show < Hanami::Action
814
- def handle(*, res)
813
+ def handle(*, response)
815
814
  # ...
816
- res.format = :custom
815
+ response.format = :custom
817
816
  end
818
817
  end
819
818
 
@@ -833,9 +832,9 @@ configuration = Hanami::Controller::Configuration.new do |config|
833
832
  end
834
833
 
835
834
  class Csv < Hanami::Action
836
- def handle(*, res)
837
- res.format = :csv
838
- res.body = Enumerator.new do |yielder|
835
+ def handle(*, response)
836
+ response.format = :csv
837
+ response.body = Enumerator.new do |yielder|
839
838
  yielder << csv_header
840
839
 
841
840
  # Expensive operation is streamed as each line becomes available
@@ -20,8 +20,8 @@ Gem::Specification.new do |spec|
20
20
  spec.metadata["rubygems_mfa_required"] = "true"
21
21
  spec.required_ruby_version = ">= 3.1"
22
22
 
23
- spec.add_dependency "rack", "~> 2.0"
24
- spec.add_dependency "hanami-utils", "~> 2.2"
23
+ spec.add_dependency "rack", ">= 2.1"
24
+ spec.add_dependency "hanami-utils", "~> 2.3.0.beta1"
25
25
  spec.add_dependency "dry-configurable", "~> 1.0", "< 2"
26
26
  spec.add_dependency "dry-core", "~> 1.0"
27
27
  spec.add_dependency "zeitwerk", "~> 2.6"
@@ -13,6 +13,7 @@ module Hanami
13
13
  def self.included(base)
14
14
  base.class_eval do
15
15
  extend ClassMethods
16
+
16
17
  @cache_control_directives = nil
17
18
  end
18
19
  end
@@ -86,7 +86,7 @@ module Hanami
86
86
 
87
87
  # @since 0.3.0
88
88
  # @api private
89
- def fresh?
89
+ def fresh? # rubocop:disable Naming/PredicateMethod
90
90
  yield if @validations.any?(&:fresh?)
91
91
  end
92
92
 
@@ -13,6 +13,7 @@ module Hanami
13
13
  def self.included(base)
14
14
  base.class_eval do
15
15
  extend ClassMethods
16
+
16
17
  @expires_directives = nil
17
18
  end
18
19
  end
@@ -117,6 +117,24 @@ module Hanami
117
117
  #
118
118
  # @since 0.4.0
119
119
 
120
+ # @!attribute [rw] default_tld_length
121
+ #
122
+ # Sets the default TLD length for host names. It is used to extract the
123
+ # subdomain(s) in `Request#subdomains`.
124
+ #
125
+ # Defaults to 1.
126
+ #
127
+ # @example
128
+ # # For *.example.com
129
+ # config.default_tld_length = 1
130
+ #
131
+ # # Or for *.example.co.uk
132
+ # config.default_tld_length = 2
133
+ #
134
+ # @return [Integer] the number of subdomains
135
+ #
136
+ # @since 2.3.0
137
+
120
138
  # @!attribute [rw] cookies
121
139
  #
122
140
  # Sets default cookie options for all responses.
@@ -50,14 +50,14 @@ module Hanami
50
50
  # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5
51
51
  # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html
52
52
  ENTITY_HEADERS = {
53
- "Allow" => true,
54
- "Content-Encoding" => true,
55
- "Content-Language" => true,
56
- "Content-Location" => true,
57
- "Content-MD5" => true,
58
- "Content-Range" => true,
59
- "Expires" => true,
60
- "Last-Modified" => true,
53
+ "allow" => true,
54
+ "content-encoding" => true,
55
+ "content-language" => true,
56
+ "content-location" => true,
57
+ "content-md5" => true,
58
+ "content-range" => true,
59
+ "expires" => true,
60
+ "last-modified" => true,
61
61
  "extension-header" => true
62
62
  }.freeze
63
63
 
@@ -82,7 +82,7 @@ module Hanami
82
82
  module MyApp
83
83
  class App < Hanami::App
84
84
  # See Rack::Session::Cookie for options
85
- config.sessions = :cookie, {**cookie_session_options}
85
+ config.actions.sessions = :cookie, {**cookie_session_options}
86
86
  end
87
87
  end
88
88
 
@@ -222,11 +222,13 @@ module Hanami
222
222
  # @since 2.0.0
223
223
  # @api private
224
224
  def enforce_content_type(request, config)
225
- content_type = request.content_type
225
+ # Compare media type (without parameters) instead of full Content-Type header
226
+ # to avoid false negatives (e.g., multipart/form-data; boundary=...)
227
+ media_type = request.media_type
226
228
 
227
- return if content_type.nil?
229
+ return if media_type.nil?
228
230
 
229
- return if accepted_mime_type?(content_type, config)
231
+ return if accepted_mime_type?(media_type, config)
230
232
 
231
233
  yield
232
234
  end
@@ -362,13 +362,7 @@ module Hanami
362
362
  # @since 0.7.0
363
363
  # @api private
364
364
  def _router_params(fallback = {})
365
- env.fetch(ROUTER_PARAMS) do
366
- if session = fallback.delete(Action::RACK_SESSION)
367
- fallback[Action::RACK_SESSION] = Utils::Hash.deep_symbolize(session)
368
- end
369
-
370
- fallback
371
- end
365
+ env.fetch(ROUTER_PARAMS, fallback)
372
366
  end
373
367
  end
374
368
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack/file"
3
+ require "rack/files"
4
4
 
5
5
  module Hanami
6
6
  class Action
@@ -21,7 +21,7 @@ module Hanami
21
21
  # @since 0.4.3
22
22
  # @api private
23
23
  def initialize(path, root)
24
- @file = ::Rack::File.new(root.to_s)
24
+ @file = ::Rack::Files.new(root.to_s)
25
25
  @path = path.to_s
26
26
  end
27
27
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Action
5
+ # @since 2.2.0
6
+ # @api private
7
+ def self.rack_3?
8
+ defined?(::Rack::Headers)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Hanami
6
+ class Action
7
+ class Request < ::Rack::Request
8
+ # Wrapper for Rack-provided sessions, allowing access using symbol keys.
9
+ #
10
+ # @since 2.3.0
11
+ # @api public
12
+ class Session
13
+ extend Forwardable
14
+
15
+ def_delegators \
16
+ :@session,
17
+ :clear,
18
+ :delete,
19
+ :empty?,
20
+ :size,
21
+ :length,
22
+ :each,
23
+ :to_h,
24
+ :inspect,
25
+ :keys,
26
+ :values
27
+
28
+ def initialize(session)
29
+ @session = session
30
+ end
31
+
32
+ def [](key)
33
+ @session[key.to_s]
34
+ end
35
+
36
+ def []=(key, value)
37
+ @session[key.to_s] = value
38
+ end
39
+
40
+ def key?(key)
41
+ @session.key?(key.to_s)
42
+ end
43
+
44
+ alias_method :has_key?, :key?
45
+ alias_method :include?, :key?
46
+
47
+ def ==(other)
48
+ Utils::Hash.deep_symbolize(@session) == Utils::Hash.deep_symbolize(other)
49
+ end
50
+
51
+ private
52
+
53
+ # Provides a fallback for any methods not handled by the def_delegators.
54
+ def method_missing(method_name, *args, &block)
55
+ if @session.respond_to?(method_name)
56
+ @session.send(method_name, *args, &block)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def respond_to_missing?(method_name, include_private = false)
63
+ @session.respond_to?(method_name, include_private) || super
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -26,11 +26,12 @@ module Hanami
26
26
 
27
27
  # @since 2.0.0
28
28
  # @api private
29
- def initialize(env:, params:, session_enabled: false)
29
+ def initialize(env:, params:, default_tld_length: 1, session_enabled: false)
30
30
  super(env)
31
31
 
32
32
  @params = params
33
33
  @session_enabled = session_enabled
34
+ @default_tld_length = default_tld_length
34
35
  end
35
36
 
36
37
  # Returns the request's ID
@@ -56,7 +57,7 @@ module Hanami
56
57
 
57
58
  # Returns the session for the request.
58
59
  #
59
- # @return [Hash] the session object
60
+ # @return [Hanami::Request::Session] the session object
60
61
  #
61
62
  # @raise [MissingSessionError] if the session is not enabled
62
63
  #
@@ -70,7 +71,7 @@ module Hanami
70
71
  raise Hanami::Action::MissingSessionError.new("Hanami::Action::Request#session")
71
72
  end
72
73
 
73
- super
74
+ @session ||= Session.new(super)
74
75
  end
75
76
 
76
77
  # Returns the flash for the request.
@@ -91,6 +92,31 @@ module Hanami
91
92
  @flash ||= Flash.new(session[Flash::KEY])
92
93
  end
93
94
 
95
+ # Returns the subdomains for the current host.
96
+ #
97
+ # @return [Array<String>]
98
+ #
99
+ # @api public
100
+ # @since 2.3.0
101
+ def subdomains(tld_length = @default_tld_length)
102
+ return [] if IP_ADDRESS_HOST_REGEXP.match?(host)
103
+
104
+ host.split(".")[0..-(tld_length + 2)]
105
+ end
106
+
107
+ IP_ADDRESS_HOST_REGEXP = /\A\d+\.\d+\.\d+\.\d+\z/
108
+ private_constant :IP_ADDRESS_HOST_REGEXP
109
+
110
+ # Returns the subdomain for the current host.
111
+ #
112
+ # @return [String]
113
+ #
114
+ # @api public
115
+ # @since 2.3.0
116
+ def subdomain(tld_length = @default_tld_length)
117
+ subdomains(tld_length).join(".")
118
+ end
119
+
94
120
  # @since 2.0.0
95
121
  # @api private
96
122
  def accept?(mime_type)
@@ -71,12 +71,15 @@ module Hanami
71
71
  # @api public
72
72
  def body=(str)
73
73
  @length = 0
74
- @body = EMPTY_BODY.dup
74
+ @body = EMPTY_BODY.dup
75
+
76
+ return if str.nil? || str == EMPTY_BODY
75
77
 
76
78
  if str.is_a?(::Rack::Files::BaseIterator)
77
79
  @body = str
80
+ buffered_body! # Ensure appropriate content-length is set
78
81
  else
79
- write(str) unless str.nil? || str == EMPTY_BODY
82
+ write(str)
80
83
  end
81
84
  end
82
85
 
data/lib/hanami/action.rb CHANGED
@@ -7,6 +7,7 @@ require "hanami/utils/kernel"
7
7
  require "hanami/utils/string"
8
8
  require "rack"
9
9
  require "rack/utils"
10
+ require "hanami/action/rack_utils"
10
11
  require "zeitwerk"
11
12
 
12
13
  require_relative "action/constants"
@@ -61,6 +62,7 @@ module Hanami
61
62
  setting :formats, default: Config::Formats.new, mutable: true
62
63
  setting :default_charset
63
64
  setting :default_headers, default: {}, constructor: -> (headers) { headers.compact }
65
+ setting :default_tld_length, default: 1
64
66
  setting :cookies, default: {}, constructor: -> (cookie_options) {
65
67
  # Call `to_h` here to permit `ApplicationConfiguration::Cookies` object to be
66
68
  # provided when application actions are configured
@@ -312,7 +314,8 @@ module Hanami
312
314
  request = build_request(
313
315
  env: env,
314
316
  params: params,
315
- session_enabled: session_enabled?
317
+ session_enabled: session_enabled?,
318
+ default_tld_length: config.default_tld_length
316
319
  )
317
320
  response = build_response(
318
321
  request: request,
@@ -561,7 +564,7 @@ module Hanami
561
564
  #
562
565
  # # Both Content-Type and X-No-Pass are removed because they're not allowed
563
566
  def keep_response_header?(header)
564
- ENTITY_HEADERS.include?(header)
567
+ ENTITY_HEADERS.include?(header.downcase)
565
568
  end
566
569
 
567
570
  # @since 2.0.0
@@ -8,6 +8,6 @@ module Hanami
8
8
  #
9
9
  # @since 0.1.0
10
10
  # @api public
11
- VERSION = "2.2.0"
11
+ VERSION = "2.3.0.beta1"
12
12
  end
13
13
  end
metadata CHANGED
@@ -1,43 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-controller
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-11-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rack
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '2.0'
18
+ version: '2.1'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '2.0'
25
+ version: '2.1'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: hanami-utils
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: '2.2'
32
+ version: 2.3.0.beta1
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - "~>"
39
38
  - !ruby/object:Gem::Version
40
- version: '2.2'
39
+ version: 2.3.0.beta1
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: dry-configurable
43
42
  requirement: !ruby/object:Gem::Requirement
@@ -193,7 +192,9 @@ files:
193
192
  - lib/hanami/action/mime/request_mime_weight.rb
194
193
  - lib/hanami/action/params.rb
195
194
  - lib/hanami/action/rack/file.rb
195
+ - lib/hanami/action/rack_utils.rb
196
196
  - lib/hanami/action/request.rb
197
+ - lib/hanami/action/request/session.rb
197
198
  - lib/hanami/action/response.rb
198
199
  - lib/hanami/action/session.rb
199
200
  - lib/hanami/action/validatable.rb
@@ -206,7 +207,6 @@ licenses:
206
207
  - MIT
207
208
  metadata:
208
209
  rubygems_mfa_required: 'true'
209
- post_install_message:
210
210
  rdoc_options: []
211
211
  require_paths:
212
212
  - lib
@@ -221,8 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
223
  requirements: []
224
- rubygems_version: 3.5.22
225
- signing_key:
224
+ rubygems_version: 3.6.9
226
225
  specification_version: 4
227
226
  summary: Complete, fast and testable actions for Rack and Hanami
228
227
  test_files: []