Skip to content

Commit 2706f01

Browse files
dcr8898Kyle Plump
and
Kyle Plump
authored
Support paths with different variable names in same path location (#273)
--------- Co-authored-by: Kyle Plump <[email protected]>
1 parent f69c923 commit 2706f01

File tree

8 files changed

+307
-73
lines changed

8 files changed

+307
-73
lines changed

lib/hanami/router/leaf.rb

+42
Original file line numberDiff line numberDiff line change
@@ -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

lib/hanami/router/node.rb

+12-39
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
require "hanami/router/segment"
3+
require "hanami/router/leaf"
44

55
module Hanami
66
class Router
@@ -18,15 +18,14 @@ class Node
1818
def initialize
1919
@variable = nil
2020
@fixed = nil
21-
@to = nil
21+
@leaves = nil
2222
end
2323

2424
# @api private
2525
# @since 2.0.0
26-
def put(segment, constraints)
26+
def put(segment)
2727
if variable?(segment)
28-
@variable ||= {}
29-
@variable[segment_for(segment, constraints)] ||= self.class.new
28+
@variable ||= self.class.new
3029
else
3130
@fixed ||= {}
3231
@fixed[segment] ||= self.class.new
@@ -35,35 +34,21 @@ def put(segment, constraints)
3534

3635
# @api private
3736
# @since 2.0.0
38-
#
39-
def get(segment) # rubocop:disable Metrics/PerceivedComplexity
40-
return unless @variable || @fixed
41-
42-
captured = nil
43-
44-
found = @fixed&.fetch(segment, nil)
45-
return [found, nil] if found
46-
47-
@variable&.each do |matcher, node|
48-
break if found
49-
50-
captured = matcher.match(segment)
51-
found = node if captured
52-
end
53-
54-
[found, captured&.named_captures]
37+
def get(segment)
38+
@fixed&.fetch(segment, nil) || @variable
5539
end
5640

5741
# @api private
5842
# @since 2.0.0
59-
def leaf?
60-
@to
43+
def leaf!(route, to, constraints)
44+
@leaves ||= []
45+
@leaves << Leaf.new(route, to, constraints)
6146
end
6247

6348
# @api private
64-
# @since 2.0.0
65-
def leaf!(to)
66-
@to = to
49+
# @since 2.2.0
50+
def match(path)
51+
@leaves&.find { |leaf| leaf.match(path) }
6752
end
6853

6954
private
@@ -73,18 +58,6 @@ def leaf!(to)
7358
def variable?(segment)
7459
Router::ROUTE_VARIABLE_MATCHER.match?(segment)
7560
end
76-
77-
# @api private
78-
# @since 2.0.0
79-
def segment_for(segment, constraints)
80-
Segment.fabricate(segment, **constraints)
81-
end
82-
83-
# @api private
84-
# @since 2.0.0
85-
def fixed?(matcher)
86-
matcher.names.empty?
87-
end
8861
end
8962
end
9063
end

lib/hanami/router/trie.rb

+13-19
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,26 @@ def initialize
2121

2222
# @api private
2323
# @since 2.0.0
24-
def add(path, to, constraints)
24+
def add(route, to, constraints)
25+
segments = segments_from(route)
2526
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)
2830
end
2931

30-
node.leaf!(to)
32+
node.leaf!(route, to, constraints)
3133
end
3234

3335
# @api private
3436
# @since 2.0.0
3537
def find(path)
38+
segments = segments_from(path)
3639
node = @root
37-
params = {}
38-
39-
for_each_segment(path) do |segment|
40-
break unless node
4140

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) }
4742

48-
return [node.to, params] if node&.leaf?
49-
50-
nil
43+
node.match(path)&.then { |found| [found.to, found.params] }
5144
end
5245

5346
private
@@ -58,10 +51,11 @@ def find(path)
5851
private_constant :SEGMENT_SEPARATOR
5952

6053
# @api private
61-
# @since 2.0.0
62-
def for_each_segment(path, &blk)
54+
# @since 2.2.0
55+
def segments_from(path)
6356
_, *segments = path.split(SEGMENT_SEPARATOR)
64-
segments.each(&blk)
57+
58+
segments
6559
end
6660
end
6761
end

spec/integration/hanami/router/recognition_spec.rb

+32-14
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,38 @@
256256
end
257257
end
258258

259+
describe "variable followed by variable with fixed with different slug names" do
260+
let(:router) do
261+
described_class.new do
262+
get "/:foo", as: :variable_one, to: RecognitionTestCase.endpoint("variable_one")
263+
get "/:bar/baz", as: :variable_two, to: RecognitionTestCase.endpoint("variable_two")
264+
end
265+
end
266+
267+
it "recognizes route(s)" do
268+
runner.run!([
269+
[:variable_one, "/one", {foo: "one"}],
270+
[:variable_two, "/two/baz", {bar: "two"}]
271+
])
272+
end
273+
end
274+
275+
describe "variable with fixed followed by variable with different slug names" do
276+
let(:router) do
277+
described_class.new do
278+
get "/:bar/baz", as: :variable_two, to: RecognitionTestCase.endpoint("variable_two")
279+
get "/:foo", as: :variable_one, to: RecognitionTestCase.endpoint("variable_one")
280+
end
281+
end
282+
283+
it "recognizes route(s)" do
284+
runner.run!([
285+
[:variable_one, "/one", {foo: "one"}],
286+
[:variable_two, "/two/baz", {bar: "two"}]
287+
])
288+
end
289+
end
290+
259291
describe "relative variable with constraints" do
260292
let(:router) do
261293
described_class.new do
@@ -569,20 +601,6 @@
569601
end
570602
end
571603

572-
describe "relative variable with permissive constraint" do
573-
let(:router) do
574-
described_class.new do
575-
get ":test", as: :regex, test: /.*/, to: RecognitionTestCase.endpoint("regex")
576-
end
577-
end
578-
579-
it "recognizes route(s)" do
580-
runner.run!([
581-
[:regex, "/test/", {test: "test"}]
582-
])
583-
end
584-
end
585-
586604
describe "variable with permissive constraint" do
587605
let(:router) do
588606
described_class.new do

spec/integration/hanami/router/scope_spec.rb

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
app = Rack::MockRequest.new(router)
5757

5858
expect(app.request("GET", "/it", lint: true).body).to eq("Root (it)!")
59-
expect(app.request("GET", "/it/", lint: true).body).to eq("Root (it)!")
6059
expect(app.request("GET", "/it/trees", lint: true).body).to eq("Trees (it)!")
6160
end
6261

spec/unit/hanami/router/leaf_spec.rb

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
require "hanami/router/leaf"
4+
5+
RSpec.describe Hanami::Router::Leaf do
6+
let(:subject) { described_class.new(route, to, constraints) }
7+
let(:route) { "/test/route" }
8+
let(:to) { "test proc" }
9+
let(:constraints) { {} }
10+
11+
describe "#initialize" do
12+
it "returns a #{described_class} instance" do
13+
expect(subject).to be_kind_of(described_class)
14+
end
15+
end
16+
17+
describe "#to" do
18+
it "returns the endpoint passed as 'to' when initialized" do
19+
expect(subject.to).to eq(to)
20+
end
21+
end
22+
23+
describe "#match" do
24+
context "when path matches route" do
25+
let(:matching_path) { route }
26+
27+
it "returns true" do
28+
expect(subject.match(matching_path)).to be_truthy
29+
end
30+
end
31+
32+
context "when path doesn't match route" do
33+
let(:non_matching_path) { "/bad/path" }
34+
35+
it "returns true" do
36+
expect(subject.match(non_matching_path)).to be_falsey
37+
end
38+
end
39+
end
40+
41+
describe "#params" do
42+
context "without previously calling #match(path)" do
43+
it "returns nil" do
44+
params = subject.params
45+
46+
expect(params).to be_nil
47+
end
48+
end
49+
50+
context "with variable path" do
51+
let(:route) { "test/:route" }
52+
let(:matching_path) { "test/path" }
53+
let(:matching_params) { {"route" => "path"} }
54+
55+
it "returns captured params" do
56+
subject.match(matching_path)
57+
params = subject.params
58+
59+
expect(params).to eq(matching_params)
60+
end
61+
end
62+
end
63+
end

0 commit comments

Comments
 (0)