hanami-router 2.2.0.rc1 → 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: 6dbd5cec52e1e7a0d2ac82066294bf3846b58b4c8bacd3555d6f0f1a3e6cb873
4
- data.tar.gz: e96f0ff6c694e0964325d012f18c055a892af912e5f6dd3ba35f0bec13173fa0
3
+ metadata.gz: dd6e74eeead3378e5b8697f4a3d6ff3883a71c140ace0699a9bb30aa2b3f303d
4
+ data.tar.gz: dd4a259ce71090ed517152b0203896274c25d893afaf68768a118c1c77300c53
5
5
  SHA512:
6
- metadata.gz: 8dc08ace41fd84808b9706c2a4d07de0c45f3c206eb858020e8fa7970005e6eeefb721a8654bd86379d8389c71d813b9406ffb4c47eecde4c97558baf6eff3e2
7
- data.tar.gz: e28b53acd76af8d279ca365cfa8c9901ce5fc5d789736eef5cd7925e9ecb89845e58caaa9548ebefe2050b792a7704880c84bb2b27adf27c0c6df288b5bd3969
6
+ metadata.gz: cd35252d98f215ec28a0676a3a36db818af56b78d3e46b84f2e02bff14a9e49422c6fd89b4a3f6313929f3cbaedd2f193b9f35dd15b7c03b5a1252405f424e8d
7
+ data.tar.gz: 23f275b9f2fbd71d68453e69c00983f9729e98fd3b16e3b51866bfb27b73dab0678d7c204e7ba63d6b9d24dc5a5f1e1f91b88567ee0ec315c5bc1fd2669ae9fb
data/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  Rack compatible HTTP router for Ruby
4
4
 
5
+ ## v2.3.0.beta1 - 2025-10-03
6
+
7
+ ### Changed
8
+
9
+ - [Kyle Plump, Tim Riley] Support Rack 3 in addition to Rack 2 (#277)
10
+ - [Sven Schwyn] Convert -, +, ~, and . to underscore in URL helper names (#280)
11
+ - [inouire] Allow redirects to absolute URLs: `redirect "/redirect", to: "https://hanamirb.org/"` (#282)
12
+
13
+ ## v2.2.0 - 2024-11-05
14
+
15
+ ### Changed
16
+
17
+ - [Damian C. Rossney, Kyle Plump] Scopes with a dynamic segment (e.g. `scope ":locale" do`) and containing a `root` route will no longer match the root route for requests with a trailing slash. This makes the behavior consistent with scopes using static strings. (#273)
18
+
19
+ ### Fixed
20
+
21
+ - [Damian C. Rossney, Kyle Plump] Support paths with different variable names in same path location (#273)
22
+
5
23
  ## v2.2.0.rc1 - 2024-10-29
6
24
 
7
25
  ## v2.2.0.beta2 - 2024-09-25
data/README.md CHANGED
@@ -157,6 +157,8 @@ end
157
157
  Hanami::Router.new do
158
158
  get "/redirect_destination", to: ->(env) { [200, {}, ["Redirect destination!"]] }
159
159
  redirect "/legacy", to: "/redirect_destination"
160
+ redirect "/learn-more", to: "https://hanamirb.org/"
161
+ redirect "/chat", to: URI("xmpp://myapp.net/")
160
162
  end
161
163
  ```
162
164
 
@@ -349,12 +351,17 @@ __Hanami::Router__ uses [Semantic Versioning 2.0.0](http://semver.org)
349
351
 
350
352
  ## Contributing
351
353
 
352
- 1. Fork it
354
+ 1. Fork this repo to your account and clone it locally (`git clone git@github.com:your-pseudo/your-cloned-repo.git`)
353
355
  2. Create your feature branch (`git checkout -b my-new-feature`)
354
- 3. Commit your changes (`git commit -am 'Add some feature'`)
355
- 4. Push to the branch (`git push origin my-new-feature`)
356
- 5. Create new Pull Request
356
+ 3. Install the dependencies (`bundle install`)
357
+ 4. Run tests, they should all pass (`./script/ci`)
358
+ 5. Make your changes & check that the tests still pass. Add some test cases if needed.
359
+ 6. Commit your changes (`git commit -am 'Add some feature'`)
360
+ 7. Push to the branch (`git push origin my-new-feature`)
361
+ 8. Create new Pull Request on Github with some context on what you're trying to fix or to improve with this contribution
362
+
363
+ Thank you for contributing!
357
364
 
358
365
  ## Copyright
359
366
 
360
- Copyright © 2014–2024 Hanami Team – Released under MIT License
367
+ Copyright © 2014–2025 Hanami Team – Released under MIT License
@@ -20,13 +20,14 @@ 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"
23
+ spec.add_dependency "rack", ">= 2.0"
24
24
  spec.add_dependency "mustermann", "~> 3.0"
25
25
  spec.add_dependency "mustermann-contrib", "~> 3.0"
26
+ spec.add_dependency "csv", "~> 3.3"
26
27
 
27
28
  spec.add_development_dependency "bundler", ">= 1.6", "< 3"
28
29
  spec.add_development_dependency "rake", "~> 13"
29
- spec.add_development_dependency "rack-test", "~> 1.0"
30
+ spec.add_development_dependency "rack-test", "~> 2.0"
30
31
  spec.add_development_dependency "rspec", "~> 3.8"
31
32
 
32
33
  spec.add_development_dependency "rubocop", "~> 1.0"
@@ -100,11 +100,13 @@ module Hanami
100
100
 
101
101
  begin
102
102
  require "hanami/middleware/body_parser/#{parser_name}_parser"
103
- rescue LoadError; end
103
+ rescue LoadError
104
+ end
104
105
 
105
106
  begin
106
107
  parser = load_parser!("#{classify(parser_name)}Parser")
107
- rescue NameError; end
108
+ rescue NameError
109
+ end
108
110
 
109
111
  parser
110
112
  ensure
@@ -30,6 +30,8 @@ module Hanami
30
30
  # @since 2.0.1
31
31
  # @api private
32
32
  def parse(*, env)
33
+ # Rewind to ensure successful parsing (required for Rack 3's `.parse_multipart`).
34
+ env[::Rack::RACK_INPUT].rewind if env[::Rack::RACK_INPUT].respond_to?(:rewind)
33
35
  ::Rack::Multipart.parse_multipart(env)
34
36
  rescue StandardError => exception
35
37
  raise BodyParsingError.new(exception.message)
@@ -39,10 +39,11 @@ 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.empty?
42
+ body = env[RACK_INPUT]&.read
43
+ return @app.call(env) if body.nil? || body.empty?
44
44
 
45
- env[RACK_INPUT].rewind # somebody might try to read this stream
45
+ # Somebody might try to read this stream
46
+ Rack::RewindableInput.new(env[RACK_INPUT]).rewind
46
47
 
47
48
  if (parser = @parsers[media_type(env)])
48
49
  env[Router::ROUTER_PARSED_BODY] = parser.parse(body, env)
@@ -66,6 +66,7 @@ module Hanami
66
66
  # @since 2.0.0
67
67
  def call
68
68
  body = instance_exec(&@blk)
69
+
69
70
  [status, headers, [body]]
70
71
  end
71
72
  end
@@ -37,7 +37,7 @@ module Hanami
37
37
  # @api private
38
38
  # @since 2.0.0
39
39
  def call(routes, **csv_opts)
40
- ::CSV.generate(**DEFAULT_OPTIONS.merge(csv_opts)) do |csv|
40
+ ::CSV.generate(**DEFAULT_OPTIONS, **csv_opts) do |csv|
41
41
  csv << HEADERS if csv.write_headers?
42
42
  routes.reduce(csv) do |acc, route|
43
43
  route.head? ? acc : acc << row(route)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mustermann/rails"
4
+
5
+ module Hanami
6
+ class Router
7
+ class Leaf
8
+ # Trie Leaf
9
+ #
10
+ # @api private
11
+ # @since 2.2.0
12
+ attr_reader :to, :params
13
+
14
+ # @api private
15
+ # @since 2.2.0
16
+ def initialize(route, to, constraints)
17
+ @route = route
18
+ @to = to
19
+ @constraints = constraints
20
+ @params = nil
21
+ end
22
+
23
+ # @api private
24
+ # @since 2.2.0
25
+ def match(path)
26
+ match = matcher.match(path)
27
+
28
+ @params = match.named_captures if match
29
+
30
+ match
31
+ end
32
+
33
+ private
34
+
35
+ # @api private
36
+ # @since 2.2.0
37
+ def matcher
38
+ @matcher ||= Mustermann.new(@route, type: :rails, version: "5.0", capture: @constraints)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hanami/router/segment"
3
+ require "hanami/router/leaf"
4
4
 
5
5
  module Hanami
6
6
  class Router
@@ -18,15 +18,14 @@ module Hanami
18
18
  def initialize
19
19
  @variable = nil
20
20
  @fixed = nil
21
- @to = nil
21
+ @leaves = nil
22
22
  end
23
23
 
24
24
  # @api private
25
25
  # @since 2.0.0
26
- def put(segment, constraints)
26
+ def put(segment)
27
27
  if variable?(segment)
28
- @variable ||= {}
29
- @variable[segment_for(segment, constraints)] ||= self.class.new
28
+ @variable ||= self.class.new
30
29
  else
31
30
  @fixed ||= {}
32
31
  @fixed[segment] ||= self.class.new
@@ -35,36 +34,21 @@ module Hanami
35
34
 
36
35
  # @api private
37
36
  # @since 2.0.0
38
- #
39
- def get(segment) # rubocop:disable Metrics/PerceivedComplexity
40
- return unless @variable || @fixed
41
-
42
- found = nil
43
- captured = nil
44
-
45
- found = @fixed&.fetch(segment, nil)
46
- return [found, nil] if found
47
-
48
- @variable&.each do |matcher, node|
49
- break if found
50
-
51
- captured = matcher.match(segment)
52
- found = node if captured
53
- end
54
-
55
- [found, captured&.named_captures]
37
+ def get(segment)
38
+ @fixed&.fetch(segment, nil) || @variable
56
39
  end
57
40
 
58
41
  # @api private
59
42
  # @since 2.0.0
60
- def leaf?
61
- @to
43
+ def leaf!(route, to, constraints)
44
+ @leaves ||= []
45
+ @leaves << Leaf.new(route, to, constraints)
62
46
  end
63
47
 
64
48
  # @api private
65
- # @since 2.0.0
66
- def leaf!(to)
67
- @to = to
49
+ # @since 2.2.0
50
+ def match(path)
51
+ @leaves&.find { |leaf| leaf.match(path) }
68
52
  end
69
53
 
70
54
  private
@@ -74,18 +58,6 @@ module Hanami
74
58
  def variable?(segment)
75
59
  Router::ROUTE_VARIABLE_MATCHER.match?(segment)
76
60
  end
77
-
78
- # @api private
79
- # @since 2.0.0
80
- def segment_for(segment, constraints)
81
- Segment.fabricate(segment, **constraints)
82
- end
83
-
84
- # @api private
85
- # @since 2.0.0
86
- def fixed?(matcher)
87
- matcher.names.empty?
88
- end
89
61
  end
90
62
  end
91
63
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Router
5
+ # @since 2.2.0
6
+ # @api private
7
+ def self.rack_3?
8
+ defined?(Rack::Headers)
9
+ end
10
+ end
11
+ end
@@ -21,33 +21,26 @@ module Hanami
21
21
 
22
22
  # @api private
23
23
  # @since 2.0.0
24
- def add(path, to, constraints)
24
+ def add(route, to, constraints)
25
+ segments = segments_from(route)
25
26
  node = @root
26
- for_each_segment(path) do |segment|
27
- node = node.put(segment, constraints)
27
+
28
+ segments.each do |segment|
29
+ node = node.put(segment)
28
30
  end
29
31
 
30
- node.leaf!(to)
32
+ node.leaf!(route, to, constraints)
31
33
  end
32
34
 
33
35
  # @api private
34
36
  # @since 2.0.0
35
37
  def find(path)
38
+ segments = segments_from(path)
36
39
  node = @root
37
- params = {}
38
-
39
- for_each_segment(path) do |segment|
40
- break unless node
41
40
 
42
- child, captures = node.get(segment)
43
- params.merge!(captures) if captures
44
-
45
- node = child
46
- end
41
+ return unless segments.all? { |segment| node = node.get(segment) }
47
42
 
48
- return [node.to, params] if node&.leaf?
49
-
50
- nil
43
+ node.match(path)&.then { |found| [found.to, found.params] }
51
44
  end
52
45
 
53
46
  private
@@ -58,10 +51,11 @@ module Hanami
58
51
  private_constant :SEGMENT_SEPARATOR
59
52
 
60
53
  # @api private
61
- # @since 2.0.0
62
- def for_each_segment(path, &blk)
54
+ # @since 2.2.0
55
+ def segments_from(path)
63
56
  _, *segments = path.split(SEGMENT_SEPARATOR)
64
- segments.each(&blk)
57
+
58
+ segments
65
59
  end
66
60
  end
67
61
  end
@@ -8,6 +8,6 @@ module Hanami
8
8
  #
9
9
  # @since 0.1.0
10
10
  # @api public
11
- VERSION = "2.2.0.rc1"
11
+ VERSION = "2.3.0.beta1"
12
12
  end
13
13
  end
data/lib/hanami/router.rb CHANGED
@@ -22,6 +22,7 @@ module Hanami
22
22
  require "hanami/router/url_helpers"
23
23
  require "hanami/router/globbed_path"
24
24
  require "hanami/router/mounted_path"
25
+ require "hanami/router/rack_utils"
25
26
 
26
27
  # URL helpers for other Hanami integrations
27
28
  #
@@ -106,6 +107,14 @@ module Hanami
106
107
  return not_allowed(env) || not_found(env)
107
108
  end
108
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])
116
+ end
117
+
109
118
  endpoint.call(
110
119
  _params(env, params)
111
120
  ).to_a
@@ -693,6 +702,10 @@ module Hanami
693
702
  # @api private
694
703
  PREFIXED_NAME_SEPARATOR = "_"
695
704
 
705
+ # @since x.x.x
706
+ # @api private
707
+ UNDERSCORED_NAME_REGEXP = /[-+~.]/
708
+
696
709
  # @since 2.0.0
697
710
  # @api private
698
711
  ROOT_PATH = "/"
@@ -739,7 +752,11 @@ module Hanami
739
752
 
740
753
  # @since 2.0.0
741
754
  # @api private
742
- HTTP_HEADER_LOCATION = "Location"
755
+ HTTP_HEADER_LOCATION = Hanami::Router.rack_3? ? "location" : "Location"
756
+
757
+ # @since 2.2.0
758
+ # @api private
759
+ HTTP_HEADER_ALLOW = Hanami::Router.rack_3? ? "allow" : "Allow"
743
760
 
744
761
  # @since 2.0.0
745
762
  # @api private
@@ -762,7 +779,7 @@ module Hanami
762
779
  HTTP_STATUS_NOT_ALLOWED,
763
780
  {
764
781
  ::Rack::CONTENT_LENGTH => HTTP_BODY_NOT_ALLOWED_LENGTH,
765
- "Allow" => allowed_http_methods.join(", ")
782
+ HTTP_HEADER_ALLOW => allowed_http_methods.join(", ")
766
783
  },
767
784
  [HTTP_BODY_NOT_ALLOWED]
768
785
  ]
@@ -774,12 +791,13 @@ module Hanami
774
791
  # @since 2.0.0
775
792
  NOT_FOUND = ->(*) {
776
793
  [HTTP_STATUS_NOT_FOUND, {::Rack::CONTENT_LENGTH => HTTP_BODY_NOT_FOUND_LENGTH}, [HTTP_BODY_NOT_FOUND]]
777
- }.freeze
794
+ }
778
795
 
779
796
  # @since 2.0.0
780
797
  # @api private
781
798
  def lookup(env)
782
799
  endpoint = fixed(env)
800
+
783
801
  return [endpoint, {}] if endpoint
784
802
 
785
803
  variable(env) || globbed_or_mounted(env)
@@ -800,7 +818,7 @@ module Hanami
800
818
  end
801
819
 
802
820
  if as
803
- as = prefixed_name(as)
821
+ as = prefixed_underscored_name(as)
804
822
  add_named_route(path, as, constraints)
805
823
  end
806
824
 
@@ -872,10 +890,13 @@ module Hanami
872
890
  @path_prefix.join(path).to_s
873
891
  end
874
892
 
875
- # @since 2.0.0
893
+ # @since x.x.x
876
894
  # @api private
877
- def prefixed_name(name)
878
- @name_prefix.relative_join(name, PREFIXED_NAME_SEPARATOR).to_sym
895
+ def prefixed_underscored_name(name)
896
+ @name_prefix
897
+ .relative_join(name, PREFIXED_NAME_SEPARATOR)
898
+ .gsub(UNDERSCORED_NAME_REGEXP, "_")
899
+ .to_sym
879
900
  end
880
901
 
881
902
  # Returns a new instance of Hanami::Router with the modified options.
@@ -907,7 +928,13 @@ module Hanami
907
928
  raise UnknownHTTPStatusCodeError.new(code)
908
929
  end
909
930
 
910
- destination = prefixed_path(to)
931
+ destination = if to.is_a?(URI)
932
+ to.to_s
933
+ elsif to.start_with?("http://") || to.start_with?("https://")
934
+ to
935
+ else
936
+ prefixed_path(to)
937
+ end
911
938
  Redirect.new(destination, code, ->(*) { [code, {HTTP_HEADER_LOCATION => destination}, [body]] })
912
939
  end
913
940
 
@@ -925,6 +952,7 @@ module Hanami
925
952
  env[PARAMS].merge!(::Rack::Utils.parse_nested_query(env[::Rack::QUERY_STRING]))
926
953
  env[PARAMS].merge!(params)
927
954
  env[PARAMS] = Params.deep_symbolize(env[PARAMS])
955
+
928
956
  env
929
957
  end
930
958
 
metadata CHANGED
@@ -1,27 +1,26 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-router
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0.rc1
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-10-29 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
18
  version: '2.0'
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
25
  version: '2.0'
27
26
  - !ruby/object:Gem::Dependency
@@ -52,6 +51,20 @@ dependencies:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: csv
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.3'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.3'
55
68
  - !ruby/object:Gem::Dependency
56
69
  name: bundler
57
70
  requirement: !ruby/object:Gem::Requirement
@@ -92,14 +105,14 @@ dependencies:
92
105
  requirements:
93
106
  - - "~>"
94
107
  - !ruby/object:Gem::Version
95
- version: '1.0'
108
+ version: '2.0'
96
109
  type: :development
97
110
  prerelease: false
98
111
  version_requirements: !ruby/object:Gem::Requirement
99
112
  requirements:
100
113
  - - "~>"
101
114
  - !ruby/object:Gem::Version
102
- version: '1.0'
115
+ version: '2.0'
103
116
  - !ruby/object:Gem::Dependency
104
117
  name: rspec
105
118
  requirement: !ruby/object:Gem::Requirement
@@ -171,10 +184,12 @@ files:
171
184
  - lib/hanami/router/formatter/human_friendly.rb
172
185
  - lib/hanami/router/globbed_path.rb
173
186
  - lib/hanami/router/inspector.rb
187
+ - lib/hanami/router/leaf.rb
174
188
  - lib/hanami/router/mounted_path.rb
175
189
  - lib/hanami/router/node.rb
176
190
  - lib/hanami/router/params.rb
177
191
  - lib/hanami/router/prefix.rb
192
+ - lib/hanami/router/rack_utils.rb
178
193
  - lib/hanami/router/recognized_route.rb
179
194
  - lib/hanami/router/redirect.rb
180
195
  - lib/hanami/router/route.rb
@@ -187,7 +202,6 @@ licenses:
187
202
  - MIT
188
203
  metadata:
189
204
  rubygems_mfa_required: 'true'
190
- post_install_message:
191
205
  rdoc_options: []
192
206
  require_paths:
193
207
  - lib
@@ -202,8 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
216
  - !ruby/object:Gem::Version
203
217
  version: '0'
204
218
  requirements: []
205
- rubygems_version: 3.5.22
206
- signing_key:
219
+ rubygems_version: 3.6.9
207
220
  specification_version: 4
208
221
  summary: Rack compatible HTTP router for Ruby and Hanami
209
222
  test_files: []