From b5b4c9331ed8bf9870f8ae2bb2ffd3539e29ec31 Mon Sep 17 00:00:00 2001 From: "Damian C. Rossney" Date: Tue, 27 Aug 2024 00:08:43 -0400 Subject: [PATCH 1/7] add failing test case to document Issue #272 --- .../hanami/router/recognition_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/integration/hanami/router/recognition_spec.rb b/spec/integration/hanami/router/recognition_spec.rb index bd56f4b..7dea806 100644 --- a/spec/integration/hanami/router/recognition_spec.rb +++ b/spec/integration/hanami/router/recognition_spec.rb @@ -256,6 +256,22 @@ end end + describe "variable and variable with fixed" do + let(:router) do + described_class.new do + get "/:foo", as: :variable_one, to: RecognitionTestCase.endpoint("variable_one") + get "/:bar/baz", as: :variable_two, to: RecognitionTestCase.endpoint("variable_two") + end + end + + it "recognizes route(s)" do + runner.run!([ + [:variable_one, "/one", {foo: "one"}], + [:variable_two, "/two/baz", {bar: "two"}], + ]) + end + end + describe "relative variable with constraints" do let(:router) do described_class.new do From 4b82b830e49d156e38d3c52deb2d05178ad3e461 Mon Sep 17 00:00:00 2001 From: "Damian C. Rossney" Date: Thu, 29 Aug 2024 00:06:49 -0400 Subject: [PATCH 2/7] implement Leaf class with tests --- lib/hanami/router/leaf.rb | 42 +++++++++++++++++++ spec/unit/hanami/router/leaf_spec.rb | 63 ++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 lib/hanami/router/leaf.rb create mode 100644 spec/unit/hanami/router/leaf_spec.rb diff --git a/lib/hanami/router/leaf.rb b/lib/hanami/router/leaf.rb new file mode 100644 index 0000000..e99a1ff --- /dev/null +++ b/lib/hanami/router/leaf.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "mustermann/rails" + +module Hanami + class Router + class Leaf + # Trie Leaf + # + # @api private + # @since 2.1.1 + attr_reader :to, :params + + # @api private + # @since 2.1.1 + def initialize(route, to, constraints) + @route = route + @to = to + @constraints = constraints + @params = nil + end + + # @api private + # @since 2.1.1 + def match(path) + match = matcher.match(path) + + @params = match.named_captures if match + + match + end + + private + + # @api private + # @since 2.1.1 + def matcher + @matcher ||= Mustermann.new(@route, type: :rails, version: "5.0", capture: @constraints) + end + end + end +end diff --git a/spec/unit/hanami/router/leaf_spec.rb b/spec/unit/hanami/router/leaf_spec.rb new file mode 100644 index 0000000..4b4e0f6 --- /dev/null +++ b/spec/unit/hanami/router/leaf_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "hanami/router/leaf" + +RSpec.describe Hanami::Router::Leaf do + let(:subject) { described_class.new(route, to, constraints) } + let(:route) { "/test/route" } + let(:to) { "test proc" } + let(:constraints) { {} } + + describe "#initialize" do + it "returns a #{described_class} instance" do + expect(subject).to be_kind_of(described_class) + end + end + + describe "#to" do + it "returns the endpoint passed as 'to' when initialized" do + expect(subject.to).to eq(to) + end + end + + describe "#match" do + context "when path matches route" do + let(:matching_path) { route } + + it "returns true" do + expect(subject.match(matching_path)).to be_truthy + end + end + + context "when path doesn't match route" do + let(:non_matching_path) { "/bad/path" } + + it "returns true" do + expect(subject.match(non_matching_path)).to be_falsey + end + end + end + + describe "#params" do + context "without previously calling #match(path)" do + it "returns nil" do + params = subject.params + + expect(params).to be_nil + end + end + + context "with variable path" do + let(:route) { "test/:route" } + let(:matching_path) { "test/path" } + let(:matching_params) { {"route" => "path"} } + + it "returns captured params" do + subject.match(matching_path) + params = subject.params + + expect(params).to eq(matching_params) + end + end + end +end From 3eced52483f1caf06df8fa0517949a1eb2c2fb4b Mon Sep 17 00:00:00 2001 From: Kyle Plump Date: Thu, 29 Aug 2024 16:24:09 -0400 Subject: [PATCH 3/7] new integration test case, created router/trie_spec --- .../hanami/router/recognition_spec.rb | 18 +++- spec/unit/hanami/router/trie_spec.rb | 93 +++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 spec/unit/hanami/router/trie_spec.rb diff --git a/spec/integration/hanami/router/recognition_spec.rb b/spec/integration/hanami/router/recognition_spec.rb index 7dea806..8025c45 100644 --- a/spec/integration/hanami/router/recognition_spec.rb +++ b/spec/integration/hanami/router/recognition_spec.rb @@ -256,7 +256,7 @@ end end - describe "variable and variable with fixed" do + describe "variable followed by variable with fixed with different slug names" do let(:router) do described_class.new do get "/:foo", as: :variable_one, to: RecognitionTestCase.endpoint("variable_one") @@ -272,6 +272,22 @@ end end + describe "variable with fixed followed by variable with different slug names" do + let(:router) do + described_class.new do + get "/:bar/baz", as: :variable_two, to: RecognitionTestCase.endpoint("variable_two") + get "/:foo", as: :variable_one, to: RecognitionTestCase.endpoint("variable_one") + end + end + + it "recognizes route(s)" do + runner.run!([ + [:variable_one, "/one", {foo: "one"}], + [:variable_two, "/two/baz", {bar: "two"}], + ]) + end + end + describe "relative variable with constraints" do let(:router) do described_class.new do diff --git a/spec/unit/hanami/router/trie_spec.rb b/spec/unit/hanami/router/trie_spec.rb new file mode 100644 index 0000000..9906def --- /dev/null +++ b/spec/unit/hanami/router/trie_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require "hanami/middleware/trie" + +RSpec.describe Hanami::Middleware::Trie do + subject { described_class.new(app) } + let(:app) { -> (*) { [200, {"content-length" => "2"}, ["OK"]] } } + + describe "#initialize" do + it "returns an instance of #{described_class}" do + expect(subject).to be_kind_of(described_class) + end + end + + describe "#add" do + it "adds node" do + subject.add("/foo", double("foo")) + end + it "adds multiple fixed segments" do + subject.add("/foo/bar/baz", double("foo")) + end + it "adds a fixed segment followed by a variable segment" do + subject.add("/foo/:bar", double("foo")) + end + it "adds a variable segment followed by a fixed segment" do + subject.add("/:foo/bar", double("foo")) + end + it "adds a variable segment, and then a variable segment followed by a fixed segment with different variable slugs" do + subject.add("/:foo", double("foo")) + subject.add("/:bar/foo", double("bar")) + end + it "adds a variable segment followed by a fixed segment, and then a variable segment with different variable slugs" do + subject.add("/:bar/foo", double("bar")) + subject.add("/:foo", double("foo")) + end + end + + describe "#find" do + before do + subject.add("/admin", admin) + subject.add("/api", api) + subject.add("/api/v1", api_v1) + subject.add("/var/:foo", foo) + subject.add("/var/:bar/foo", bar) + end + let(:admin) { double("admin") } + let(:api) { double("api") } + let(:api_v1) { double("api_v1") } + let(:foo) { double("foo") } + let(:bar) { double("bar") } + + it "finds nodes by given path" do + expect(subject.find("/")).to eq(app) + expect(subject.find("/admin")).to eq(admin) + expect(subject.find("/admin/")).to eq(admin) # trailing slash + expect(subject.find("/api")).to eq(api) + expect(subject.find("/api/v1")).to eq(api_v1) + end + + it "matches path prefix" do + expect(subject.find("/admin/users")).to eq(admin) + expect(subject.find("/api/v1/songs")).to eq(api_v1) + expect(subject.find("/api/v1/songs/")).to eq(api_v1) # trailing slash + end + + it "matches path with a variable segment" do + expect(subject.find("/var/foo")).to eq(foo) + end + + it "matches path with a variable segment followed by a fixed segment" do + expect(subject.find("/var/bar/foo")).to eq(bar) + end + + it "falls back to app, if no node is associated with given path" do + expect(subject.find("/foo")).to eq(app) + end + end + + describe "#empty?" do + context "without nodes" do + it "returns true" do + expect(subject).to be_empty + end + end + + context "with nodes" do + it "returns false" do + subject.add("/bar", double("bar")) + expect(subject).to_not be_empty + end + end + end +end From 36185688789292cd35807c37d2370cd8e769a2cf Mon Sep 17 00:00:00 2001 From: "Damian C. Rossney" Date: Sat, 31 Aug 2024 19:59:29 -0400 Subject: [PATCH 4/7] refactor Node class with new tests --- lib/hanami/router/node.rb | 52 +++--------- spec/unit/hanami/router/node_spec.rb | 116 +++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 40 deletions(-) create mode 100644 spec/unit/hanami/router/node_spec.rb diff --git a/lib/hanami/router/node.rb b/lib/hanami/router/node.rb index b169ce1..bf81a15 100644 --- a/lib/hanami/router/node.rb +++ b/lib/hanami/router/node.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "hanami/router/segment" +require "hanami/router/leaf" module Hanami class Router @@ -18,15 +18,14 @@ class Node def initialize @variable = nil @fixed = nil - @to = nil + @leaves = nil end # @api private # @since 2.0.0 - def put(segment, constraints) + def put(segment) if variable?(segment) - @variable ||= {} - @variable[segment_for(segment, constraints)] ||= self.class.new + @variable ||= self.class.new else @fixed ||= {} @fixed[segment] ||= self.class.new @@ -35,36 +34,21 @@ def put(segment, constraints) # @api private # @since 2.0.0 - # - def get(segment) # rubocop:disable Metrics/PerceivedComplexity - return unless @variable || @fixed - - found = nil - captured = nil - - found = @fixed&.fetch(segment, nil) - return [found, nil] if found - - @variable&.each do |matcher, node| - break if found - - captured = matcher.match(segment) - found = node if captured - end - - [found, captured&.named_captures] + def get(segment) + @fixed&.fetch(segment, nil) || @variable end # @api private # @since 2.0.0 - def leaf? - @to + def leaf!(route, to, constraints) + @leaves ||= [] + @leaves << Leaf.new(route, to, constraints) end # @api private - # @since 2.0.0 - def leaf!(to) - @to = to + # @since 2.1.1 + def match(path) + @leaves&.find { |leaf| leaf.match(path) } end private @@ -74,18 +58,6 @@ def leaf!(to) def variable?(segment) Router::ROUTE_VARIABLE_MATCHER.match?(segment) end - - # @api private - # @since 2.0.0 - def segment_for(segment, constraints) - Segment.fabricate(segment, **constraints) - end - - # @api private - # @since 2.0.0 - def fixed?(matcher) - matcher.names.empty? - end end end end diff --git a/spec/unit/hanami/router/node_spec.rb b/spec/unit/hanami/router/node_spec.rb new file mode 100644 index 0000000..4a0db18 --- /dev/null +++ b/spec/unit/hanami/router/node_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require "hanami/router/node" +require "hanami/router/leaf" + +RSpec.describe Hanami::Router::Node do + describe "#initialize" do + it "returns a #{described_class} instance" do + expect(subject).to be_kind_of(described_class) + end + end + + describe "#put" do + it "adds a node matching segment" do + segment = "foo" + subject.put(segment) + end + end + + describe "#get" do + context "when segment is found" do + context "and segment is fixed" do + it "returns a node" do + segment = "foo" + subject.put(segment) + + expect(subject.get(segment)).to be_kind_of(described_class) + end + end + + context "and segment is variable" do + it "returns a node" do + dynamic_segment = ":foo" + subject.put(dynamic_segment) + + expect(subject.get("bar")).to be_kind_of(described_class) + end + end + end + + context "when segment is not found" do + it "returns nil" do + segment = "foo" + subject.put(segment) + + expect(subject.get("bar")).to be_nil + end + end + end + + describe "#leaf!" do + it "sets leaf data" do + route = "/route" + to = double("to") + constraints = {} + subject.leaf!(route, to, constraints) + end + end + + describe "#match" do + context "when segment is fixed" do + context "and match not found" do + it "returns nil" do + segment = "foo" + route = "/foo" + to = double("to") + constraints = {} + path = "/bar" + subject.put(segment).leaf!(route, to, constraints) + + expect(subject.get(segment).match(path)).to be_nil + end + end + + context "and match is found" do + it "returns a Leaf object" do + segment = "foo" + route = "/foo" + to = double("to") + constraints = {} + subject.put(segment).leaf!(route, to, constraints) + + expect(subject.get(segment).match(route)).to be_kind_of(Hanami::Router::Leaf) + end + end + end + + context "when segment is variable" do + context "and match not found" do + it "returns nil" do + segment = ":foo" + route = "/:foo" + to = double("to") + constraints = { foo: :digit } + path = "/bar" + subject.put(segment).leaf!(route, to, constraints) + + expect(subject.get(segment).match(path)).to be_nil + end + end + + context "and match found" do + it "returns Leaf object" do + segment = ":foo" + route = "/:foo" + to = double("to") + constraints = { foo: :digit } + path = "/123" + subject.put(segment).leaf!(route, to, constraints) + + expect(subject.get(segment).match(path)).to be_kind_of(Hanami::Router::Leaf) + end + end + end + end +end From e5a4c0eeea8b82e9879ddecbe438ee4a5fb044f1 Mon Sep 17 00:00:00 2001 From: "Damian C. Rossney" Date: Sun, 1 Sep 2024 21:14:34 -0400 Subject: [PATCH 5/7] refactor Trie class with new tests --- lib/hanami/router/trie.rb | 32 ++++----- spec/unit/hanami/router/trie_spec.rb | 102 ++++++++++++--------------- 2 files changed, 60 insertions(+), 74 deletions(-) diff --git a/lib/hanami/router/trie.rb b/lib/hanami/router/trie.rb index 67e3db1..b2fb7c1 100644 --- a/lib/hanami/router/trie.rb +++ b/lib/hanami/router/trie.rb @@ -21,33 +21,26 @@ def initialize # @api private # @since 2.0.0 - def add(path, to, constraints) + def add(route, to, constraints) + segments = split(route) node = @root - for_each_segment(path) do |segment| - node = node.put(segment, constraints) + + segments.each do |segment| + node = node.put(segment) end - node.leaf!(to) + node.leaf!(route, to, constraints) end # @api private # @since 2.0.0 def find(path) + segments = split(path) node = @root - params = {} - - for_each_segment(path) do |segment| - break unless node - child, captures = node.get(segment) - params.merge!(captures) if captures - - node = child - end + return unless segments.all? { |segment| node = node.get(segment) } - return [node.to, params] if node&.leaf? - - nil + node.match(path)&.then { |found| [found.to, found.params] } end private @@ -58,10 +51,11 @@ def find(path) private_constant :SEGMENT_SEPARATOR # @api private - # @since 2.0.0 - def for_each_segment(path, &blk) + # @since 2.1.1 + def split(path) _, *segments = path.split(SEGMENT_SEPARATOR) - segments.each(&blk) + + segments end end end diff --git a/spec/unit/hanami/router/trie_spec.rb b/spec/unit/hanami/router/trie_spec.rb index 9906def..4c3ca0c 100644 --- a/spec/unit/hanami/router/trie_spec.rb +++ b/spec/unit/hanami/router/trie_spec.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true -require "hanami/middleware/trie" - -RSpec.describe Hanami::Middleware::Trie do - subject { described_class.new(app) } - let(:app) { -> (*) { [200, {"content-length" => "2"}, ["OK"]] } } +require "hanami/router/trie" +RSpec.describe Hanami::Router::Trie do describe "#initialize" do it "returns an instance of #{described_class}" do expect(subject).to be_kind_of(described_class) @@ -13,81 +10,76 @@ end describe "#add" do + let(:constraints) { {foo: :digit} } + let(:empty_constraints) { {} } + it "adds node" do - subject.add("/foo", double("foo")) + subject.add("/foo", double("foo"), empty_constraints) end - it "adds multiple fixed segments" do - subject.add("/foo/bar/baz", double("foo")) + + it "adds fixed segment" do + subject.add("/foo", double("foo"), empty_constraints) end - it "adds a fixed segment followed by a variable segment" do - subject.add("/foo/:bar", double("foo")) + + it "adds variable segment" do + subject.add("/:foo", double("foo"), empty_constraints) end + it "adds a variable segment followed by a fixed segment" do - subject.add("/:foo/bar", double("foo")) + subject.add("/:foo/bar", double("foo"), empty_constraints) end + + it "adds a variable segment with constraints" do + subject.add("/:foo", double("foo"), constraints) + end + it "adds a variable segment, and then a variable segment followed by a fixed segment with different variable slugs" do - subject.add("/:foo", double("foo")) - subject.add("/:bar/foo", double("bar")) + subject.add("/:foo", double("foo"), empty_constraints) + subject.add("/:bar/foo", double("bar"), empty_constraints) end + it "adds a variable segment followed by a fixed segment, and then a variable segment with different variable slugs" do - subject.add("/:bar/foo", double("bar")) - subject.add("/:foo", double("foo")) + subject.add("/:bar/foo", double("bar"), empty_constraints) + subject.add("/:foo", double("foo"), empty_constraints) + end + + it "adds a variable segment with constriants followed by a variable segment with different variable slugs" do + subject.add("/:foo", double("foo"), constraints) + subject.add("/:bar", double("bar"), empty_constraints) end end describe "#find" do before do - subject.add("/admin", admin) - subject.add("/api", api) - subject.add("/api/v1", api_v1) - subject.add("/var/:foo", foo) - subject.add("/var/:bar/foo", bar) + subject.add("/:foo", foo, foo_constraints) + subject.add("/:foo/bar", bar, foo_constraints) + subject.add("/:baz", baz, empty_constraints) end - let(:admin) { double("admin") } - let(:api) { double("api") } - let(:api_v1) { double("api_v1") } let(:foo) { double("foo") } let(:bar) { double("bar") } + let(:baz) { double("baz") } + let(:foo_constraints) { {foo: :digit} } + let(:empty_constraints) { {} } - it "finds nodes by given path" do - expect(subject.find("/")).to eq(app) - expect(subject.find("/admin")).to eq(admin) - expect(subject.find("/admin/")).to eq(admin) # trailing slash - expect(subject.find("/api")).to eq(api) - expect(subject.find("/api/v1")).to eq(api_v1) - end - - it "matches path prefix" do - expect(subject.find("/admin/users")).to eq(admin) - expect(subject.find("/api/v1/songs")).to eq(api_v1) - expect(subject.find("/api/v1/songs/")).to eq(api_v1) # trailing slash - end + it "matches path with variable segment and matching constraints" do + to, params = subject.find("/123") - it "matches path with a variable segment" do - expect(subject.find("/var/foo")).to eq(foo) + expect(to).to eq(foo) + expect(params).to eq({"foo" => "123"}) end - it "matches path with a variable segment followed by a fixed segment" do - expect(subject.find("/var/bar/foo")).to eq(bar) - end + it "matches path with variable segment followed by fixed segment" do + to, params = subject.find("/123/bar") - it "falls back to app, if no node is associated with given path" do - expect(subject.find("/foo")).to eq(app) + expect(to).to eq(bar) + expect(params).to eq({"foo" => "123"}) end - end - describe "#empty?" do - context "without nodes" do - it "returns true" do - expect(subject).to be_empty - end - end + it "matches correct path with variable segment based on constraints" do + to, params = subject.find("/qux") - context "with nodes" do - it "returns false" do - subject.add("/bar", double("bar")) - expect(subject).to_not be_empty - end + expect(to).to eq(baz) + expect(params).to eq({"baz" => "qux"}) end end end From 66668b3090343fdcca2a28245fc46aab5ba67a51 Mon Sep 17 00:00:00 2001 From: "Damian C. Rossney" Date: Wed, 4 Sep 2024 22:18:58 -0400 Subject: [PATCH 6/7] appease Rubocop --- spec/integration/hanami/router/recognition_spec.rb | 4 ++-- spec/unit/hanami/router/leaf_spec.rb | 6 +++--- spec/unit/hanami/router/node_spec.rb | 4 ++-- spec/unit/hanami/router/trie_spec.rb | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/integration/hanami/router/recognition_spec.rb b/spec/integration/hanami/router/recognition_spec.rb index 8025c45..337ea93 100644 --- a/spec/integration/hanami/router/recognition_spec.rb +++ b/spec/integration/hanami/router/recognition_spec.rb @@ -267,7 +267,7 @@ it "recognizes route(s)" do runner.run!([ [:variable_one, "/one", {foo: "one"}], - [:variable_two, "/two/baz", {bar: "two"}], + [:variable_two, "/two/baz", {bar: "two"}] ]) end end @@ -283,7 +283,7 @@ it "recognizes route(s)" do runner.run!([ [:variable_one, "/one", {foo: "one"}], - [:variable_two, "/two/baz", {bar: "two"}], + [:variable_two, "/two/baz", {bar: "two"}] ]) end end diff --git a/spec/unit/hanami/router/leaf_spec.rb b/spec/unit/hanami/router/leaf_spec.rb index 4b4e0f6..0a8a81d 100644 --- a/spec/unit/hanami/router/leaf_spec.rb +++ b/spec/unit/hanami/router/leaf_spec.rb @@ -13,16 +13,16 @@ expect(subject).to be_kind_of(described_class) end end - + describe "#to" do it "returns the endpoint passed as 'to' when initialized" do expect(subject.to).to eq(to) end end - + describe "#match" do context "when path matches route" do - let(:matching_path) { route } + let(:matching_path) { route } it "returns true" do expect(subject.match(matching_path)).to be_truthy diff --git a/spec/unit/hanami/router/node_spec.rb b/spec/unit/hanami/router/node_spec.rb index 4a0db18..92329ac 100644 --- a/spec/unit/hanami/router/node_spec.rb +++ b/spec/unit/hanami/router/node_spec.rb @@ -91,7 +91,7 @@ segment = ":foo" route = "/:foo" to = double("to") - constraints = { foo: :digit } + constraints = {foo: :digit} path = "/bar" subject.put(segment).leaf!(route, to, constraints) @@ -104,7 +104,7 @@ segment = ":foo" route = "/:foo" to = double("to") - constraints = { foo: :digit } + constraints = {foo: :digit} path = "/123" subject.put(segment).leaf!(route, to, constraints) diff --git a/spec/unit/hanami/router/trie_spec.rb b/spec/unit/hanami/router/trie_spec.rb index 4c3ca0c..14a1b70 100644 --- a/spec/unit/hanami/router/trie_spec.rb +++ b/spec/unit/hanami/router/trie_spec.rb @@ -59,7 +59,7 @@ let(:bar) { double("bar") } let(:baz) { double("baz") } let(:foo_constraints) { {foo: :digit} } - let(:empty_constraints) { {} } + let(:empty_constraints) { {} } it "matches path with variable segment and matching constraints" do to, params = subject.find("/123") From 1c794bdaf7379004ee9cbf1a7a5a53184fe7fec5 Mon Sep 17 00:00:00 2001 From: "Damian C. Rossney" Date: Mon, 16 Sep 2024 20:38:09 -0400 Subject: [PATCH 7/7] rename Trie#split private method to Trie#segments_from --- lib/hanami/router/trie.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/hanami/router/trie.rb b/lib/hanami/router/trie.rb index b2fb7c1..360bf17 100644 --- a/lib/hanami/router/trie.rb +++ b/lib/hanami/router/trie.rb @@ -22,7 +22,7 @@ def initialize # @api private # @since 2.0.0 def add(route, to, constraints) - segments = split(route) + segments = segments_from(route) node = @root segments.each do |segment| @@ -35,7 +35,7 @@ def add(route, to, constraints) # @api private # @since 2.0.0 def find(path) - segments = split(path) + segments = segments_from(path) node = @root return unless segments.all? { |segment| node = node.get(segment) } @@ -52,7 +52,7 @@ def find(path) # @api private # @since 2.1.1 - def split(path) + def segments_from(path) _, *segments = path.split(SEGMENT_SEPARATOR) segments