From 5826e168d002314d9019a0e9d54bb54b67a8e776 Mon Sep 17 00:00:00 2001 From: Gareth Pendleton Date: Sun, 7 Sep 2025 17:59:43 +0100 Subject: [PATCH] feat: URI merging and tests --- src/uri.gleam | 81 ++++++++++++++++++++++++++++++++++----- test/uri_test.gleam | 93 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 10 deletions(-) diff --git a/src/uri.gleam b/src/uri.gleam index 9703b4a..c47c61c 100644 --- a/src/uri.gleam +++ b/src/uri.gleam @@ -39,6 +39,67 @@ pub fn to_string(uri: Uri) -> String { string.concat(parts) } +pub fn merge(base: Uri, relative: Uri) -> Result(Uri, Nil) { + use <- bool.guard(when: base.scheme == None, return: Error(Nil)) + let uri = case relative.scheme { + Some(_) -> { + Uri(..relative, path: remove_dot_segments(relative.path)) + } + None -> { + let scheme = base.scheme + case relative.host, relative.port, relative.userinfo { + Some(_), _, _ | _, Some(_), _ | _, _, Some(_) -> { + Uri(..relative, scheme:, path: remove_dot_segments(relative.path)) + } + _, _, _ -> { + case relative.path { + "" -> { + let query = case relative.query { + Some(_) -> relative.query + _ -> base.query + } + Uri(..base, query:) + } + "/" <> _ -> { + Uri( + ..base, + path: remove_dot_segments(relative.path), + query: relative.query, + ) + } + _ -> { + let path = merge_paths(base, relative) + Uri( + ..base, + path: remove_dot_segments(path), + query: relative.query, + ) + } + } + } + } + } + } + + Uri(..uri, fragment: relative.fragment) |> Ok +} + +fn has_authority(uri: Uri) -> Bool { + case uri.host, uri.userinfo, uri.port { + Some(_), _, _ | _, Some(_), _ | _, _, Some(_) -> True + _, _, _ -> False + } +} + +fn merge_paths(base: Uri, relative: Uri) -> String { + case has_authority(base), base.path { + True, "" -> "/" <> relative.path + _, _ -> { + remove_segment(base.path) <> "/" <> relative.path + } + } +} + pub fn normalize(uri: Uri) -> Uri { normalise(uri) } @@ -51,28 +112,28 @@ pub fn normalise(uri: Uri) -> Uri { let port = uri.port let host = uri.host |> option.map(string.lowercase) |> option.map(percent_normaliser) - let path = uri.path |> percent_normaliser |> normalise_path + let path = uri.path |> percent_normaliser |> remove_dot_segments let query = uri.query |> option.map(percent_normaliser) let fragment = uri.fragment |> option.map(percent_normaliser) Uri(scheme, userinfo, host, port, path, query, fragment) } -fn normalise_path(path: String) -> String { - do_normalise_path(path, "") +fn remove_dot_segments(path: String) -> String { + do_remove_dot_segments(path, "") } -fn do_normalise_path(path: String, acc: String) -> String { +fn do_remove_dot_segments(path: String, acc: String) -> String { case path { - "../" <> rest | "./" <> rest -> do_normalise_path(rest, acc) - "/./" <> rest -> do_normalise_path("/" <> rest, acc) - "/." -> do_normalise_path("/", acc) - "/../" <> rest -> do_normalise_path("/" <> rest, remove_segment(acc)) - "/.." -> do_normalise_path("/", remove_segment(acc)) + "../" <> rest | "./" <> rest -> do_remove_dot_segments(rest, acc) + "/./" <> rest -> do_remove_dot_segments("/" <> rest, acc) + "/." -> acc <> "/" + "/../" <> rest -> do_remove_dot_segments("/" <> rest, remove_segment(acc)) + "/.." -> remove_segment(acc) <> "/" "." | ".." | "" -> acc _ -> { let assert Ok(#(char, rest)) = string.pop_grapheme(path) - do_normalise_path(rest, acc <> char) + do_remove_dot_segments(rest, acc <> char) } } } diff --git a/test/uri_test.gleam b/test/uri_test.gleam index 2506a5f..0ec2c2a 100644 --- a/test/uri_test.gleam +++ b/test/uri_test.gleam @@ -802,6 +802,99 @@ pub fn parse_failure_tests() { }), ]) } + +pub fn merge_tests() { + describe("merging", [ + it("relative merge", fn() { + let uri1 = uri.parse("/relative") |> should.be_ok + let uri2 = uri.parse("") |> should.be_ok + uri.merge(uri1, uri2) |> should.be_error + }), + it("simple merge", fn() { + let uri1 = uri.parse("http://google.com/weebl") |> should.be_ok + let uri2 = uri.parse("http://example.com/baz") |> should.be_ok + uri.merge(uri1, uri2) |> should.equal(uri.parse("http://example.com/baz")) + }), + it("segments merge", fn() { + let uri1 = uri.parse("http://google.com/weebl") |> should.be_ok + let uri2 = + uri.parse("http://example.com/.././bob/../../baz") |> should.be_ok + uri.merge(uri1, uri2) |> should.equal(uri.parse("http://example.com/baz")) + }), + it("base with authority merge", fn() { + let uri1 = uri.parse("http://google.com/weebl") |> should.be_ok + let uri2 = uri.parse("//example.com/baz") |> should.be_ok + uri.merge(uri1, uri2) |> should.equal(uri.parse("http://example.com/baz")) + }), + it("base with authority segments merge", fn() { + let uri1 = uri.parse("http://google.com/weebl") |> should.be_ok + let uri2 = + uri.parse("//example.com/.././bob/../../../baz") |> should.be_ok + uri.merge(uri1, uri2) |> should.equal(uri.parse("http://example.com/baz")) + }), + it("base with absolute merge", fn() { + let uri1 = uri.parse("http://google.com/weebl/eh") |> should.be_ok + let uri2 = uri.parse("/baz") |> should.be_ok + uri.merge(uri1, uri2) |> should.equal(uri.parse("http://google.com/baz")) + }), + it("base with relative merge", fn() { + let uri1 = uri.parse("http://google.com/weebl/eh") |> should.be_ok + let uri2 = uri.parse("baz") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/weebl/baz")) + let uri1 = uri.parse("http://google.com/weebl/") |> should.be_ok + let uri2 = uri.parse("baz") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/weebl/baz")) + let uri1 = uri.parse("http://google.com") |> should.be_ok + let uri2 = uri.parse("baz") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/baz")) + }), + it("base with relative segments merge", fn() { + let uri1 = uri.parse("http://google.com") |> should.be_ok + let uri2 = uri.parse("/.././bob/../../../baz") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/baz")) + }), + it("base with empty uri merge", fn() { + let uri1 = uri.parse("http://google.com/weebl/bob") |> should.be_ok + let uri2 = uri.parse("") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/weebl/bob")) + }), + + it("base with fragment merge", fn() { + let uri1 = uri.parse("http://google.com/weebl/bob") |> should.be_ok + let uri2 = uri.parse("#fragment") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/weebl/bob#fragment")) + }), + it("base with query merge", fn() { + let uri1 = uri.parse("http://google.com/weebl/bob") |> should.be_ok + let uri2 = uri.parse("?query") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/weebl/bob?query")) + let uri1 = uri.parse("http://google.com/weebl/bob?query1") |> should.be_ok + let uri2 = uri.parse("?query2") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/weebl/bob?query2")) + let uri1 = uri.parse("http://google.com/weebl/bob?query1") |> should.be_ok + let uri2 = uri.parse("") |> should.be_ok + uri.merge(uri1, uri2) + |> echo + |> should.equal(uri.parse("http://google.com/weebl/bob?query1")) + }), + ]) +} // gleeunit test functions end in `_test` // pub fn uri_test() { // match("uri:")