Skip to content
Permalink
Browse files

URI.merge/2 (#4644)

* Add URI.merge/2

* Changes based on RFC 3986. More tests
  • Loading branch information...
andrewtimberlake authored and josevalim committed May 20, 2016
1 parent 3dcb1b7 commit ede00a1460c9ea845c8360933da5a414ca8f0c2d
Showing with 119 additions and 0 deletions.
  1. +63 −0 lib/elixir/lib/uri.ex
  2. +56 −0 lib/elixir/test/elixir/uri_test.exs
@@ -373,6 +373,69 @@ defmodule URI do
"http://google.com"
"""
defdelegate to_string(uri), to: String.Chars.URI

@doc ~S"""
Merges two URIs as per RFC 3986 §5.2
## Examples
iex> URI.merge(URI.parse("http://google.com"), "/query") |> to_string
"http://google.com/query"
iex> URI.merge("http://example.com", "http://google.com")|> to_string
"http://google.com"
"""
def merge(%URI{authority: nil}, _rel), do: raise ArgumentError, "you must merge onto an absolute URI"
def merge(%URI{}, %URI{scheme: scheme} = rel) when scheme != nil do
rel
end
def merge(%URI{} = base, %URI{path: "", query: query, fragment: fragment} = rel) do
merge(base, %{rel | path: nil, query: query, fragment: fragment})
end
def merge(%URI{} = base, %URI{path: nil, query: query, fragment: fragment}) do
%{base | path: base.path, query: query || base.query, fragment: fragment}
end
def merge(%URI{path: base_path} = base, %URI{path: rel_path, query: query, fragment: fragment}) do
%{base | path: merge_paths(base_path, rel_path), query: query, fragment: fragment}
end
# Convert binary to URI
def merge(base, rel), do: merge(parse(base), parse(rel))

defp merge_paths(nil, rel_path), do: merge_paths("/", rel_path)
defp merge_paths(_, "/" <> _ = rel_path), do: rel_path
defp merge_paths(base_path, rel_path) do
[_ | base_segments] = path_to_segments(base_path)
rel_segments = path_to_segments(rel_path)
rel_segments ++ base_segments
|> remove_dot_segments([])
|> Enum.join("/")
end

defp remove_dot_segments([], [head, ".." | acc]),
do: remove_dot_segments([], [head | acc])
defp remove_dot_segments([], acc), do: acc
defp remove_dot_segments(["." | tail], acc),
do: remove_dot_segments(tail, acc)
defp remove_dot_segments([head | tail], ["..", ".." | _] = acc),
do: remove_dot_segments(tail, [head | acc])
defp remove_dot_segments(segments, [_, ".." | acc]),
do: remove_dot_segments(segments, acc)
defp remove_dot_segments([head | tail], acc),
do: remove_dot_segments(tail, [head | acc])

def path_to_segments(path) do
[h | t] = String.split(path, "/")
reverse_and_discard_empty(t, [h])
end

defp reverse_and_discard_empty([], acc),
do: acc
defp reverse_and_discard_empty([h], acc),
do: [h | acc]
defp reverse_and_discard_empty(["" | t], acc),
do: reverse_and_discard_empty(t, acc)
defp reverse_and_discard_empty([h | t], acc),
do: reverse_and_discard_empty(t, [h | acc])
end

defimpl String.Chars, for: URI do
@@ -225,4 +225,60 @@ defmodule URITest do
assert URI.to_string(URI.parse("http://google.com")) == "http://google.com"
assert URI.to_string(URI.parse("//user:password@google.com/")) == "//user:password@google.com/"
end

test "merge/2" do
assert_raise ArgumentError, fn ->
URI.merge("/relative", "")
end

assert URI.merge("http://google.com/foo", "http://example.com/baz") |> to_string == "http://example.com/baz"

assert URI.merge("http://example.com", URI.parse("/foo")) |> to_string == "http://example.com/foo"

base = URI.parse("http://example.com/foo/bar")
assert URI.merge(base, "") |> to_string == "http://example.com/foo/bar"
assert URI.merge(base, "#fragment") |> to_string == "http://example.com/foo/bar#fragment"
assert URI.merge(base, "?query") |> to_string == "http://example.com/foo/bar?query"
assert URI.merge(base, %URI{path: ""}) |> to_string == "http://example.com/foo/bar"
assert URI.merge(base, %URI{path: "", fragment: "fragment"}) |> to_string == "http://example.com/foo/bar#fragment"

base = URI.parse("http://example.com")
assert URI.merge(base, "/foo") |> to_string == "http://example.com/foo"
assert URI.merge(base, "foo") |> to_string == "http://example.com/foo"

base = URI.parse("http://example.com/foo/bar")
assert URI.merge(base, "/baz") |> to_string == "http://example.com/baz"
assert URI.merge(base, "baz") |> to_string == "http://example.com/foo/baz"
assert URI.merge(base, "../baz") |> to_string == "http://example.com/baz"
assert URI.merge(base, ".././baz") |> to_string == "http://example.com/baz"
assert URI.merge(base, "./baz") |> to_string == "http://example.com/foo/baz"
assert URI.merge(base, "bar/./baz") |> to_string == "http://example.com/foo/bar/baz"

base = URI.parse("http://example.com/foo/bar/")
assert URI.merge(base, "/baz") |> to_string == "http://example.com/baz"
assert URI.merge(base, "baz") |> to_string == "http://example.com/foo/bar/baz"
assert URI.merge(base, "../baz") |> to_string == "http://example.com/foo/baz"
assert URI.merge(base, ".././baz") |> to_string == "http://example.com/foo/baz"
assert URI.merge(base, "./baz") |> to_string == "http://example.com/foo/bar/baz"
assert URI.merge(base, "bar/./baz") |> to_string == "http://example.com/foo/bar/bar/baz"

base = URI.parse("http://example.com/foo/bar/baz")
assert URI.merge(base, "../../foobar") |> to_string == "http://example.com/foobar"
assert URI.merge(base, "../../../foobar") |> to_string == "http://example.com/foobar"
assert URI.merge(base, "../../../../../../foobar") |> to_string == "http://example.com/foobar"

base = URI.parse("http://example.com/foo/../bar")
assert URI.merge(base, "baz") |> to_string == "http://example.com/baz"

base = URI.parse("http://example.com/foo/./bar")
assert URI.merge(base, "baz") |> to_string == "http://example.com/foo/baz"

base = URI.parse("http://example.com/foo?query1")
assert URI.merge(base, "?query2") |> to_string == "http://example.com/foo?query2"
assert URI.merge(base, "") |> to_string == "http://example.com/foo?query1"

base = URI.parse("http://example.com/foo#fragment1")
assert URI.merge(base, "#fragment2") |> to_string == "http://example.com/foo#fragment2"
assert URI.merge(base, "") |> to_string == "http://example.com/foo"
end
end

0 comments on commit ede00a1

Please sign in to comment.
You can’t perform that action at this time.