hanami-router 2.3.0.beta2 → 2.3.1

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: 5c9e51f6dddcf337224f949ca5f28c79b82702f6743a9aef5fbecdfebd5efcb9
4
- data.tar.gz: 3d02dfc438dac3cd1f07a62934c0c81059db209123a1a0f7a639c30885013a63
3
+ metadata.gz: c9a6b9498cdc6f09a3792b52accbe0fda997f5de907fb0e523408a0a8437465c
4
+ data.tar.gz: 40fc3193dcd6e471f1464852844456748f1ef8deb53ad630290e1343abdfbbbd
5
5
  SHA512:
6
- metadata.gz: 27585ee48b5a0070bf60044676b7c6473093b139c6d92a34bc204df51e87acb643933e9cc92a38ace8ad31da701f58db480dad0da288bf21270376613e192a1f
7
- data.tar.gz: 6e170d49fd689cad6fe447cf8bb1e93c76e50254751a108c85bf8b0c07c269673aa617f99a87197a1d7d89b169297ca6b752bea994c118663613b431535ba601
6
+ metadata.gz: b514e367ea3eb3b3be40f89d19b5a00388365b1a39fd0dc5bc98c1f971ff2831a4fcc80c3c463079d4a61773e0375504cfd9afa198aae7d1c42d762fb2e56def
7
+ data.tar.gz: ef993a463b78db8deba2a6b6304aeb408961ee6ae89e92e62f8b79e52f662a6c0d7155921aee78c1ddce67055fa218294b09afefb19674d27aa1edb32622c0e8
data/CHANGELOG.md CHANGED
@@ -1,6 +1,85 @@
1
1
  # Hanami::Router
2
2
 
3
- Rack compatible HTTP router for Ruby
3
+ Rack compatible HTTP router for Ruby.
4
+
5
+ ## [Unreleased]
6
+
7
+ ### Added
8
+
9
+ ### Changed
10
+
11
+ ### Deprecated
12
+
13
+ ### Removed
14
+
15
+ ### Fixed
16
+
17
+ ### Security
18
+
19
+ [Unreleased]: http://github.com/hanami/hanami-router/compare/v2.3.1...main
20
+
21
+ ## [2.3.1] - 2025-12-17
22
+
23
+ ### Changed
24
+
25
+ - Require Rack version 2.2.16 or higher, which is the earliest version of Rack 2.x that will run on Ruby 4.0 as well as our currently supported Ruby versions (3.2, 3.3, 3.4). (@cllns in 2b15582)
26
+
27
+ ### Fixed
28
+
29
+ - Set correct Rack `"SCRIPT_NAME"` value for mounts under dynamic prefixes. (@timriley in #294).
30
+
31
+ For example, given a mount under `scope '/stations/:station_id'`, the "SCRIPT_NAME" will be e.g. `"/stations/42"` rather than `"/stations/:station_id"`.
32
+ - For a mount with a prefix, allow the Rack `"PATH_INFO"` value to remain an empty string when a request is made for that exact prefix only. (@timriley in #295)
33
+
34
+ Given the following:
35
+
36
+ ```ruby
37
+ mount ->(env) { [200, {}, [env["PATH_INFO"]] }, at: "/settings"
38
+ ```
39
+
40
+ When a request is made to `"/settings"`, the `"SCRIPT_NAME"` will be `"/settings"` and `"PATH_INFO"` will be `""`. This ensures that the Rack environment can be used to reconstruct the same path as used for the original request.
41
+
42
+ To ensure a mounted router instance can still route to its root, treat `""` the same as `"/"` for the purposes of matching routes.
43
+
44
+ [2.3.1]: http://github.com/hanami/hanami-router/compare/v2.3.0...v2.3.1
45
+
46
+ ## v2.3.0 - 2025-11-12
47
+
48
+ ### Changed
49
+
50
+ - Allow scopes to be given a custom name prefix, with `as:`. This prefix is given to any named routes within the scope. (@timriley in #286)
51
+ - Allow route names given to `as:` to specify their own _prefix_, which goes before any name prefixes added by the scopes the route is nested within. To specify a prefix, provide an array to `as:`. (@timriley in #286)
52
+
53
+ ```ruby
54
+ scope "backend" do
55
+ scope "admin", as: :secret do
56
+ get "/cats/new", to: ->(*) { [200, {}, ["OK"]] }, as: [:new, :cat]
57
+ end
58
+ end
59
+
60
+ router.path(:new_backend_secret_cat)
61
+ # => "/backend/admin/cats/new
62
+ ```
63
+ - As part of Rack v2/v3 compatibility, improve coordination between `Hanami::Middleware::BodyParser` and `Hanami::Router` regarding access of the request body. (@timriley in #287)
64
+
65
+ `BodyParser` will now make `env["rack.input"]` rewindable, and rewind the body after accessing it, ensuring downstream users can still read `"rack.input"` if needed. `Router` will only attempt to make `"rack.input"` rewindable if it hasn’t already been made so.
66
+ - Change `Hanami::Middleware::BodyParser::Parser` to refer to `media_types` instead of `mime_types`. (@timriley in #289)
67
+
68
+ A body parser subclass should now look like this:
69
+
70
+ ```ruby
71
+ class CustomParser < Hanami::Middleware::BodyParser::Parser
72
+ def self.media_types = ["application/custom"]
73
+ def parse(body)
74
+ body # Your parsing logic here
75
+ end
76
+ end
77
+ ```
78
+ - Allow `Hanami::Middleware::BodyParser` to be initialized with a single body parser, instead of requiring an array. (@timriley in #288)
79
+
80
+ ```ruby
81
+ Hanami::Middleware::BodyParser.new(app, MyCustomParser)
82
+ ```
4
83
 
5
84
  ## v2.3.0.beta2 - 2025-10-17
6
85
 
data/README.md CHANGED
@@ -6,8 +6,6 @@ Rack compatible, lightweight, and fast HTTP Router for Ruby and [Hanami](http://
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/hanami-router.svg)](https://badge.fury.io/rb/hanami-router)
8
8
  [![CI](https://github.com/hanami/router/actions/workflows/ci.yml/badge.svg)](https://github.com/hanami/router/actions?query=workflow%3Aci+branch%3Amain)
9
- [![Test Coverage](https://codecov.io/gh/hanami/router/branch/main/graph/badge.svg)](https://codecov.io/gh/hanami/router)
10
- [![Depfu](https://badges.depfu.com/badges/5f6b8e8fa3b0d082539f0b0f84d55960/overview.svg)](https://depfu.com/github/hanami/router?project=Bundler)
11
9
 
12
10
  ## Contact
13
11
 
@@ -20,8 +18,6 @@ Rack compatible, lightweight, and fast HTTP Router for Ruby and [Hanami](http://
20
18
 
21
19
  ## Installation
22
20
 
23
- __Hanami::Router__ supports Ruby (MRI) 3.1.+
24
-
25
21
  Add this line to your application's Gemfile:
26
22
 
27
23
  ```ruby
@@ -7,8 +7,8 @@ require "hanami/router/version"
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = "hanami-router"
9
9
  spec.version = Hanami::Router::VERSION
10
- spec.authors = ["Luca Guidi"]
11
- spec.email = ["me@lucaguidi.com"]
10
+ spec.authors = ["Hanakai team"]
11
+ spec.email = ["info@hanakai.org"]
12
12
  spec.description = "Rack compatible HTTP router for Ruby"
13
13
  spec.summary = "Rack compatible HTTP router for Ruby and Hanami"
14
14
  spec.homepage = "http://hanamirb.org"
@@ -20,15 +20,15 @@ Gem::Specification.new do |spec|
20
20
  spec.metadata["rubygems_mfa_required"] = "true"
21
21
  spec.required_ruby_version = ">= 3.2"
22
22
 
23
- spec.add_dependency "rack", ">= 2.0"
23
+ spec.add_dependency "rack", ">= 2.2.16"
24
24
  spec.add_dependency "mustermann", "~> 3.0"
25
25
  spec.add_dependency "mustermann-contrib", "~> 3.0"
26
26
  spec.add_dependency "csv", "~> 3.3"
27
27
 
28
- spec.add_development_dependency "bundler", ">= 1.6", "< 3"
29
28
  spec.add_development_dependency "rake", "~> 13"
30
29
  spec.add_development_dependency "rack-test", "~> 2.0"
31
30
  spec.add_development_dependency "rspec", "~> 3.8"
31
+ spec.add_development_dependency "ostruct" # Remove once we drop support for Rack 2
32
32
 
33
33
  spec.add_development_dependency "rubocop", "~> 1.0"
34
34
  spec.add_development_dependency "rubocop-performance", "~> 1.0"
@@ -53,22 +53,22 @@ module Hanami
53
53
  # @api private
54
54
  # @since 2.0.0
55
55
  def build_parsers(parser_specs, registry = {})
56
- return DEFAULT_BODY_PARSERS if parser_specs.empty?
57
-
58
56
  parsers = Array(parser_specs).flatten(0)
59
57
 
58
+ return DEFAULT_BODY_PARSERS if parsers.empty?
59
+
60
60
  parsers.each_with_object(registry) do |spec, memo|
61
61
  if spec.is_a?(Hash) && spec.size > 1
62
62
  spec.each do |key, value|
63
63
  build_parsers([key => [value]], memo)
64
64
  end
65
65
  else
66
- name, *mime_types = Array(*spec).flatten(0)
66
+ name, *media_types = Array(*spec).flatten(0)
67
67
 
68
- parser = build(name, mime_types: mime_types.flatten)
68
+ parser = build(name, media_types: media_types.flatten)
69
69
 
70
- parser.mime_types.each do |mime|
71
- memo[mime] = parser
70
+ parser.media_types.each do |type|
71
+ memo[type] = parser
72
72
  end
73
73
  end
74
74
  end
@@ -78,7 +78,7 @@ module Hanami
78
78
 
79
79
  # @api private
80
80
  # @since 1.3.0
81
- PARSER_METHODS = %i[mime_types parse].freeze
81
+ PARSER_METHODS = %i[media_types parse].freeze
82
82
 
83
83
  # @api private
84
84
  # @since 2.0.0
@@ -9,17 +9,12 @@ module Hanami
9
9
  # @since 2.0.1
10
10
  # @api private
11
11
  class FormParser < Parser
12
- # @since 2.0.1
13
12
  # @api private
14
- MIME_TYPES = [
15
- "multipart/form-data"
16
- ].freeze
13
+ MEDIA_TYPES = ["multipart/form-data"].freeze
17
14
 
18
15
  # @since 2.0.1
19
16
  # @api private
20
- def self.mime_types
21
- MIME_TYPES
22
- end
17
+ def self.media_types = MEDIA_TYPES
23
18
 
24
19
  # Parse a multipart body payload (form file uploading)
25
20
  #
@@ -9,11 +9,11 @@ module Hanami
9
9
  # @since 1.3.0
10
10
  # @api private
11
11
  class JsonParser < Parser
12
- # @since 1.3.0
13
12
  # @api private
14
- def self.mime_types
15
- ["application/json", "application/vnd.api+json"]
16
- end
13
+ MEDIA_TYPES = ["application/json", "application/vnd.api+json"].freeze
14
+
15
+ # @api private
16
+ def self.media_types = MEDIA_TYPES
17
17
 
18
18
  # Parse a json string
19
19
  #
@@ -7,11 +7,14 @@ module Hanami
7
7
  #
8
8
  # @since 2.0.0
9
9
  class Parser
10
- DEFAULT_MIME_TYPES = [].freeze
10
+ class << self
11
+ def media_types = []
12
+ alias_method :mime_types, :media_types
13
+ end
11
14
 
12
- # Return supported mime types
15
+ # Return supported media types
13
16
  #
14
- # @return [Array<String>] supported MIME types
17
+ # @return [Array<String>] supported media types
15
18
  #
16
19
  # @abstract
17
20
  # @since 2.0.0
@@ -20,15 +23,16 @@ module Hanami
20
23
  # require "hanami/middleware/body_parser"
21
24
  #
22
25
  # class XMLParser < Hanami::Middleware::BodyParser::Parser
23
- # def self.mime_types
26
+ # def self.media_types
24
27
  # ["application/xml", "text/xml"]
25
28
  # end
26
29
  # end
27
- attr_reader :mime_types
30
+ attr_reader :media_types
31
+ alias_method :mime_types, :media_types
28
32
 
29
33
  # @api private
30
- def initialize(mime_types: DEFAULT_MIME_TYPES)
31
- @mime_types = self.class.mime_types + mime_types
34
+ def initialize(media_types: [])
35
+ @media_types = self.class.media_types + media_types
32
36
  end
33
37
 
34
38
  # Parse raw HTTP request body
@@ -39,23 +39,31 @@ module Hanami
39
39
  end
40
40
 
41
41
  def call(env)
42
- body = env[RACK_INPUT]&.read
43
- return @app.call(env) if body.nil? || body.empty?
42
+ return @app.call(env) if env.key?(Router::ROUTER_PARSED_BODY)
44
43
 
45
- # Somebody might try to read this stream
46
- Rack::RewindableInput.new(env[RACK_INPUT]).rewind
44
+ input = env[RACK_INPUT]
45
+ return @app.call(env) unless input
47
46
 
48
- if (parser = @parsers[media_type(env)])
49
- env[Router::ROUTER_PARSED_BODY] = parser.parse(body, env)
50
- env[ROUTER_PARAMS] = _symbolize(env[Router::ROUTER_PARSED_BODY])
51
- end
47
+ parser = @parsers[media_type(env)]
48
+ return @app.call(env) unless parser
49
+
50
+ # The input in Rack 3 is not rewindable. For compatbility with Rack 2, make the input
51
+ # rewindable, rewind it for reading, then rewind once more before returning to the app.
52
+ input = env[RACK_INPUT] = Rack::RewindableInput.new(input) unless input.respond_to?(:rewind)
53
+ input.rewind
54
+ body = input.read
55
+ input.rewind
56
+
57
+ return @app.call(env) if body.nil? || body.empty?
58
+
59
+ env[Router::ROUTER_PARSED_BODY] = parser.parse(body, env)
60
+ env[ROUTER_PARAMS] = _symbolize(env[Router::ROUTER_PARSED_BODY])
52
61
 
53
62
  @app.call(env)
54
63
  end
55
64
 
56
65
  private
57
66
 
58
- # @api private
59
67
  def _symbolize(body)
60
68
  if body.is_a?(::Hash)
61
69
  Router::Params.deep_symbolize(body)
@@ -64,14 +72,6 @@ module Hanami
64
72
  end
65
73
  end
66
74
 
67
- # @api private
68
- def _parse(env, body)
69
- @parsers[
70
- media_type(env)
71
- ].parse(body)
72
- end
73
-
74
- # @api private
75
75
  def media_type(env)
76
76
  ct = content_type(env)
77
77
  return unless ct
@@ -79,7 +79,6 @@ module Hanami
79
79
  ct.split(MEDIA_TYPE_MATCHER, 2).first.downcase
80
80
  end
81
81
 
82
- # @api private
83
82
  def content_type(env)
84
83
  content_type = env[CONTENT_TYPE]
85
84
  content_type.nil? || content_type.empty? ? nil : content_type
@@ -14,9 +14,12 @@ module Hanami
14
14
  if @prefix.to_s == "/"
15
15
  env[::Rack::SCRIPT_NAME] = EMPTY_STRING
16
16
  else
17
- env[::Rack::SCRIPT_NAME] = env[::Rack::SCRIPT_NAME].to_s + @prefix.to_s
18
- env[::Rack::PATH_INFO] = env[::Rack::PATH_INFO].sub(@prefix.to_s, EMPTY_STRING)
19
- env[::Rack::PATH_INFO] = DEFAULT_PREFIX if env[::Rack::PATH_INFO] == EMPTY_STRING
17
+ # To set SCRIPT_NAME, use the actual matched portion of the path, not the prefix string
18
+ # itself. This is important for prefixes with dynamic segments like "/stations/:id". In
19
+ # this case, we want e.g. "/stations/42" as SCRIPT_NAME, not "/stations/:id".
20
+ matched_path = match.to_s
21
+ env[::Rack::SCRIPT_NAME] = env[::Rack::SCRIPT_NAME].to_s + matched_path
22
+ env[::Rack::PATH_INFO] = env[::Rack::PATH_INFO].sub(matched_path, EMPTY_STRING)
20
23
  end
21
24
 
22
25
  [@app, match.named_captures]
@@ -8,6 +8,6 @@ module Hanami
8
8
  #
9
9
  # @since 0.1.0
10
10
  # @api public
11
- VERSION = "2.3.0.beta2"
11
+ VERSION = "2.3.1"
12
12
  end
13
13
  end
data/lib/hanami/router.rb CHANGED
@@ -107,12 +107,10 @@ module Hanami
107
107
  return not_allowed(env) || not_found(env)
108
108
  end
109
109
 
110
- # Rack 3 no longer requires "rack.input" to be rewindable. Force input to be
111
- # rewindable to maintain Rack 2 behavior.
112
- #
113
- # @since 2.2.0
114
- if Hanami::Router.rack_3? && env[::Rack::RACK_INPUT]
115
- env[::Rack::RACK_INPUT] = Rack::RewindableInput.new(env[::Rack::RACK_INPUT])
110
+ # Rack 3 no longer requires "rack.input" to be rewindable. Force input to be rewindable to
111
+ # maintain Rack 2 behavior while we're still supporting both.
112
+ if (input = env[::Rack::RACK_INPUT]) && !input.respond_to?(:rewind)
113
+ env[::Rack::RACK_INPUT] = Rack::RewindableInput.new(input)
116
114
  end
117
115
 
118
116
  endpoint.call(
@@ -167,7 +165,8 @@ module Hanami
167
165
  #
168
166
  # @param path [String] the relative URL to be matched
169
167
  # @param to [#call] the Rack endpoint
170
- # @param as [Symbol] a unique name for the route
168
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
169
+ # to add a prefix to the name when nested within scopes.
171
170
  # @param constraints [Hash] a set of constraints for path variables
172
171
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
173
172
  #
@@ -203,6 +202,19 @@ module Hanami
203
202
  # router.path(:welcome) # => "/"
204
203
  # router.url(:welcome) # => #<URI::HTTP http://localhost/>
205
204
  #
205
+ # @example Named route with prefix inside scope
206
+ # require "hanami/router"
207
+ #
208
+ # router = Hanami::Router.new do
209
+ # scope "backend" do
210
+ # scope "admin", as: :secret do
211
+ # get "/cats/new", to: ->(*) { [200, {}, ["OK"]] }, as: [:new, :cat]
212
+ # end
213
+ # end
214
+ # end
215
+ #
216
+ # router.path(:new_backend_secret_cat) # => "/backend/admin/cats/new
217
+ #
206
218
  # @example Constraints
207
219
  # require "hanami/router"
208
220
  #
@@ -218,7 +230,8 @@ module Hanami
218
230
  #
219
231
  # @param path [String] the relative URL to be matched
220
232
  # @param to [#call] the Rack endpoint
221
- # @param as [Symbol] a unique name for the route
233
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
234
+ # to add a prefix to the name when nested within scopes.
222
235
  # @param constraints [Hash] a set of constraints for path variables
223
236
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
224
237
  #
@@ -236,7 +249,8 @@ module Hanami
236
249
  #
237
250
  # @param path [String] the relative URL to be matched
238
251
  # @param to [#call] the Rack endpoint
239
- # @param as [Symbol] a unique name for the route
252
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
253
+ # to add a prefix to the name when nested within scopes.
240
254
  # @param constraints [Hash] a set of constraints for path variables
241
255
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
242
256
  #
@@ -254,7 +268,8 @@ module Hanami
254
268
  #
255
269
  # @param path [String] the relative URL to be matched
256
270
  # @param to [#call] the Rack endpoint
257
- # @param as [Symbol] a unique name for the route
271
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
272
+ # to add a prefix to the name when nested within scopes.
258
273
  # @param constraints [Hash] a set of constraints for path variables
259
274
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
260
275
  #
@@ -272,7 +287,8 @@ module Hanami
272
287
  #
273
288
  # @param path [String] the relative URL to be matched
274
289
  # @param to [#call] the Rack endpoint
275
- # @param as [Symbol] a unique name for the route
290
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
291
+ # to add a prefix to the name when nested within scopes.
276
292
  # @param constraints [Hash] a set of constraints for path variables
277
293
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
278
294
  #
@@ -290,7 +306,8 @@ module Hanami
290
306
  #
291
307
  # @param path [String] the relative URL to be matched
292
308
  # @param to [#call] the Rack endpoint
293
- # @param as [Symbol] a unique name for the route
309
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
310
+ # to add a prefix to the name when nested within scopes.
294
311
  # @param constraints [Hash] a set of constraints for path variables
295
312
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
296
313
  #
@@ -308,7 +325,8 @@ module Hanami
308
325
  #
309
326
  # @param path [String] the relative URL to be matched
310
327
  # @param to [#call] the Rack endpoint
311
- # @param as [Symbol] a unique name for the route
328
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
329
+ # to add a prefix to the name when nested within scopes.
312
330
  # @param constraints [Hash] a set of constraints for path variables
313
331
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
314
332
  #
@@ -326,7 +344,8 @@ module Hanami
326
344
  #
327
345
  # @param path [String] the relative URL to be matched
328
346
  # @param to [#call] the Rack endpoint
329
- # @param as [Symbol] a unique name for the route
347
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
348
+ # to add a prefix to the name when nested within scopes.
330
349
  # @param constraints [Hash] a set of constraints for path variables
331
350
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
332
351
  #
@@ -344,7 +363,8 @@ module Hanami
344
363
  #
345
364
  # @param path [String] the relative URL to be matched
346
365
  # @param to [#call] the Rack endpoint
347
- # @param as [Symbol] a unique name for the route
366
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
367
+ # to add a prefix to the name when nested within scopes.
348
368
  # @param constraints [Hash] a set of constraints for path variables
349
369
  # @param blk [Proc] the anonymous proc to be used as endpoint for the route
350
370
  #
@@ -362,7 +382,8 @@ module Hanami
362
382
  #
363
383
  # @param path [String] the relative URL to be matched
364
384
  # @param to [#call] the Rack endpoint
365
- # @param as [Symbol] a unique name for the route
385
+ # @param as [Symbol, Array<Symbol>] a unique name for the route, or a ["prefix", "name"] array,
386
+ # to add a prefix to the name when nested within scopes.
366
387
  # @param code [Integer] a HTTP status code to use for the redirect
367
388
  #
368
389
  # @raise [Hanami::Router::UnknownHTTPStatusCodeError] when an unknown redirect code is given
@@ -379,6 +400,7 @@ module Hanami
379
400
  # inherit the given path as path prefix and as a named routes prefix.
380
401
  #
381
402
  # @param path [String] the scope path to be used as a path prefix
403
+ # @param as: [String, Symbol] the name prefix to use for nested routes
382
404
  # @param blk [Proc] the routes definitions withing the scope
383
405
  #
384
406
  # @since 2.0.0
@@ -395,13 +417,13 @@ module Hanami
395
417
  # end
396
418
  #
397
419
  # router.path(:v1_users) # => "/v1/users"
398
- def scope(path, &blk)
420
+ def scope(path, as: nil, &blk)
399
421
  path_prefix = @path_prefix
400
422
  name_prefix = @name_prefix
401
423
 
402
424
  begin
403
425
  @path_prefix = @path_prefix.join(path.to_s)
404
- @name_prefix = @name_prefix.join(path.to_s)
426
+ @name_prefix = @name_prefix.join((as || path).to_s)
405
427
  instance_eval(&blk)
406
428
  ensure
407
429
  @path_prefix = path_prefix
@@ -620,7 +642,12 @@ module Hanami
620
642
  # @since 2.0.0
621
643
  # @api private
622
644
  def fixed(env)
623
- @fixed.dig(env[::Rack::REQUEST_METHOD], env[::Rack::PATH_INFO])
645
+ path_info = env[::Rack::PATH_INFO]
646
+ # Treat empty PATH_INFO as "/" for route matching. This allows root routes (defined as "/") to
647
+ # match the empty PATH_INFO that is set for requests to a mount without a trailing slash.
648
+ path_info = DEFAULT_PREFIX if path_info == EMPTY_STRING
649
+
650
+ @fixed.dig(env[::Rack::REQUEST_METHOD], path_info)
624
651
  end
625
652
 
626
653
  # @since 2.0.0
@@ -818,15 +845,13 @@ module Hanami
818
845
  end
819
846
 
820
847
  if as
821
- as = prefixed_underscored_name(as)
848
+ as = prefixed_name(as)
822
849
  add_named_route(path, as, constraints)
823
850
  end
824
851
 
825
852
  if inspect?
826
853
  @inspector.add_route(
827
- Route.new(
828
- http_method: http_method, path: path, to: to || endpoint, as: as, constraints: constraints, blk: blk
829
- )
854
+ Route.new(http_method:, path:, to: to || endpoint, as:, constraints:, blk:)
830
855
  )
831
856
  end
832
857
  end
@@ -862,8 +887,8 @@ module Hanami
862
887
 
863
888
  # @since 2.0.0
864
889
  # @api private
865
- def add_named_route(path, as, constraints)
866
- @url_helpers.add(as, Segment.fabricate(path, **constraints))
890
+ def add_named_route(path, name, constraints)
891
+ @url_helpers.add(name, Segment.fabricate(path, **constraints))
867
892
  end
868
893
 
869
894
  # @since 2.0.0
@@ -890,13 +915,32 @@ module Hanami
890
915
  @path_prefix.join(path).to_s
891
916
  end
892
917
 
893
- # @since x.x.x
894
918
  # @api private
895
- def prefixed_underscored_name(name)
896
- @name_prefix
897
- .relative_join(name, PREFIXED_NAME_SEPARATOR)
898
- .gsub(UNDERSCORED_NAME_REGEXP, "_")
899
- .to_sym
919
+ def prefixed_name(name)
920
+ prefix, suffix = name_parts(name)
921
+
922
+ name = @name_prefix.relative_join(suffix, PREFIXED_NAME_SEPARATOR).to_s
923
+ name = prefix + PREFIXED_NAME_SEPARATOR + name if prefix
924
+
925
+ normalized_name(name)
926
+ end
927
+
928
+ # Returns a [prefix, suffix] array for a given route name.
929
+ #
930
+ # @api private
931
+ def name_parts(name)
932
+ name = Array(name)
933
+
934
+ if name.size < 2
935
+ [nil, name.first&.to_s]
936
+ else
937
+ [name.first&.to_s, name[1..].join("_")]
938
+ end
939
+ end
940
+
941
+ # @api private
942
+ def normalized_name(name)
943
+ name.gsub(UNDERSCORED_NAME_REGEXP, "_").to_sym
900
944
  end
901
945
 
902
946
  # Returns a new instance of Hanami::Router with the modified options.
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-router
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0.beta2
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
- - Luca Guidi
7
+ - Hanakai team
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '2.0'
18
+ version: 2.2.16
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '2.0'
25
+ version: 2.2.16
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: mustermann
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -65,26 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '3.3'
68
- - !ruby/object:Gem::Dependency
69
- name: bundler
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '1.6'
75
- - - "<"
76
- - !ruby/object:Gem::Version
77
- version: '3'
78
- type: :development
79
- prerelease: false
80
- version_requirements: !ruby/object:Gem::Requirement
81
- requirements:
82
- - - ">="
83
- - !ruby/object:Gem::Version
84
- version: '1.6'
85
- - - "<"
86
- - !ruby/object:Gem::Version
87
- version: '3'
88
68
  - !ruby/object:Gem::Dependency
89
69
  name: rake
90
70
  requirement: !ruby/object:Gem::Requirement
@@ -127,6 +107,20 @@ dependencies:
127
107
  - - "~>"
128
108
  - !ruby/object:Gem::Version
129
109
  version: '3.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: ostruct
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
130
124
  - !ruby/object:Gem::Dependency
131
125
  name: rubocop
132
126
  requirement: !ruby/object:Gem::Requirement
@@ -157,7 +151,7 @@ dependencies:
157
151
  version: '1.0'
158
152
  description: Rack compatible HTTP router for Ruby
159
153
  email:
160
- - me@lucaguidi.com
154
+ - info@hanakai.org
161
155
  executables: []
162
156
  extensions: []
163
157
  extra_rdoc_files: []