hanami-router 2.3.0.beta2 → 2.3.0

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: 5bc038728052f6719d7a3c7ecfbfbb02a5b5d730ab95bafdb1a8415602faa861
4
+ data.tar.gz: 961ad5a29c273a42526261c5305d41dc21530c16baf20ed01c106ab7c8b635ff
5
5
  SHA512:
6
- metadata.gz: 27585ee48b5a0070bf60044676b7c6473093b139c6d92a34bc204df51e87acb643933e9cc92a38ace8ad31da701f58db480dad0da288bf21270376613e192a1f
7
- data.tar.gz: 6e170d49fd689cad6fe447cf8bb1e93c76e50254751a108c85bf8b0c07c269673aa617f99a87197a1d7d89b169297ca6b752bea994c118663613b431535ba601
6
+ metadata.gz: 723502c7e0e4dd4beba5de8a1c3e77dd8e912103f833dc709f7c8f001702a54b93128222768721b4391ce4ee3e1a7d951046bdcb67132b406d6582159394bdd6
7
+ data.tar.gz: 646f78f7b642bffc4ff8be39e6c7fff011ade92ee8154651d66aafe1b803de0e4901151a01a962af6832c3ace23d4f27167186cdac26de1a29cc3453f02b2ca2
data/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  Rack compatible HTTP router for Ruby
4
4
 
5
+ ## v2.3.0 - 2025-11-12
6
+
7
+ ### Changed
8
+
9
+ - 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)
10
+ - 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)
11
+
12
+ ```ruby
13
+ scope "backend" do
14
+ scope "admin", as: :secret do
15
+ get "/cats/new", to: ->(*) { [200, {}, ["OK"]] }, as: [:new, :cat]
16
+ end
17
+ end
18
+
19
+ router.path(:new_backend_secret_cat)
20
+ # => "/backend/admin/cats/new
21
+ ```
22
+ - 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)
23
+
24
+ `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.
25
+ - Change `Hanami::Middleware::BodyParser::Parser` to refer to `media_types` instead of `mime_types`. (@timriley in #289)
26
+
27
+ A body parser subclass should now look like this:
28
+
29
+ ```ruby
30
+ class CustomParser < Hanami::Middleware::BodyParser::Parser
31
+ def self.media_types = ["application/custom"]
32
+ def parse(body)
33
+ body # Your parsing logic here
34
+ end
35
+ end
36
+ ```
37
+ - Allow `Hanami::Middleware::BodyParser` to be initialized with a single body parser, instead of requiring an array. (@timriley in #288)
38
+
39
+ ```ruby
40
+ Hanami::Middleware::BodyParser.new(app, MyCustomParser)
41
+ ```
42
+
5
43
  ## v2.3.0.beta2 - 2025-10-17
6
44
 
7
45
  ### Changed
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"
@@ -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
@@ -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.0"
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
@@ -818,15 +840,13 @@ module Hanami
818
840
  end
819
841
 
820
842
  if as
821
- as = prefixed_underscored_name(as)
843
+ as = prefixed_name(as)
822
844
  add_named_route(path, as, constraints)
823
845
  end
824
846
 
825
847
  if inspect?
826
848
  @inspector.add_route(
827
- Route.new(
828
- http_method: http_method, path: path, to: to || endpoint, as: as, constraints: constraints, blk: blk
829
- )
849
+ Route.new(http_method:, path:, to: to || endpoint, as:, constraints:, blk:)
830
850
  )
831
851
  end
832
852
  end
@@ -862,8 +882,8 @@ module Hanami
862
882
 
863
883
  # @since 2.0.0
864
884
  # @api private
865
- def add_named_route(path, as, constraints)
866
- @url_helpers.add(as, Segment.fabricate(path, **constraints))
885
+ def add_named_route(path, name, constraints)
886
+ @url_helpers.add(name, Segment.fabricate(path, **constraints))
867
887
  end
868
888
 
869
889
  # @since 2.0.0
@@ -890,13 +910,32 @@ module Hanami
890
910
  @path_prefix.join(path).to_s
891
911
  end
892
912
 
893
- # @since x.x.x
894
913
  # @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
914
+ def prefixed_name(name)
915
+ prefix, suffix = name_parts(name)
916
+
917
+ name = @name_prefix.relative_join(suffix, PREFIXED_NAME_SEPARATOR).to_s
918
+ name = prefix + PREFIXED_NAME_SEPARATOR + name if prefix
919
+
920
+ normalized_name(name)
921
+ end
922
+
923
+ # Returns a [prefix, suffix] array for a given route name.
924
+ #
925
+ # @api private
926
+ def name_parts(name)
927
+ name = Array(name)
928
+
929
+ if name.size < 2
930
+ [nil, name.first&.to_s]
931
+ else
932
+ [name.first&.to_s, name[1..].join("_")]
933
+ end
934
+ end
935
+
936
+ # @api private
937
+ def normalized_name(name)
938
+ name.gsub(UNDERSCORED_NAME_REGEXP, "_").to_sym
900
939
  end
901
940
 
902
941
  # 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.0
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
@@ -157,7 +157,7 @@ dependencies:
157
157
  version: '1.0'
158
158
  description: Rack compatible HTTP router for Ruby
159
159
  email:
160
- - me@lucaguidi.com
160
+ - info@hanakai.org
161
161
  executables: []
162
162
  extensions: []
163
163
  extra_rdoc_files: []