Skip to content

Commit e414cc0

Browse files
authored
Add URI.new/1 and URI.new!/1 (#11341)
Closes #10865.
1 parent 95357ff commit e414cc0

File tree

2 files changed

+491
-199
lines changed

2 files changed

+491
-199
lines changed

lib/elixir/lib/uri.ex

Lines changed: 170 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule URI do
1212
"""
1313

1414
defstruct scheme: nil,
15-
path: nil,
15+
path: "",
1616
query: nil,
1717
fragment: nil,
1818
authority: nil,
@@ -21,16 +21,27 @@ defmodule URI do
2121
port: nil
2222

2323
@type t :: %__MODULE__{
24-
scheme: nil | binary,
25-
path: nil | binary,
26-
query: nil | binary,
24+
authority: authority,
2725
fragment: nil | binary,
28-
authority: nil | binary,
29-
userinfo: nil | binary,
3026
host: nil | binary,
31-
port: nil | :inet.port_number()
27+
path: binary,
28+
port: nil | :inet.port_number(),
29+
query: nil | binary,
30+
scheme: nil | binary,
31+
userinfo: nil | binary
3232
}
3333

34+
@typedoc deprecated: "The authority field is deprecated"
35+
@opaque authority :: nil | binary
36+
37+
defmodule Error do
38+
defexception [:action, :reason, :part]
39+
40+
def message(%Error{action: action, reason: reason, part: part}) do
41+
"cannot #{action} due to reason #{reason}: #{inspect(part)}"
42+
end
43+
end
44+
3445
import Bitwise
3546

3647
@reserved_characters ':/?#[]@!$&\'()*+,;='
@@ -465,89 +476,202 @@ defmodule URI do
465476
defp hex_to_dec(_n), do: nil
466477

467478
@doc """
468-
Parses a well-formed URI into its components.
479+
Creates a new URI struct from a URI or a string.
480+
481+
If a `%URI{}` struct is given, it returns `{:ok, uri}`. If a string is
482+
given, it will parse it and returns `{:ok, uri}`. If the string is
483+
invalid, it returns `{:error, part}` instead, with the invalid part of the URI.
469484
470485
This function can parse both absolute and relative URLs. You can check
471486
if a URI is absolute or relative by checking if the `scheme` field is
472-
nil or not. Furthermore, this function expects both absolute and
473-
relative URIs to be well-formed and does not perform any validation.
474-
See the "Examples" section below.
475-
476-
When a URI is given without a port, the value returned by
477-
`URI.default_port/1` for the URI's scheme is used for the `:port` field.
487+
`nil` or not. All fields may be `nil`, except for the `path`.
478488
479-
When a URI hostname is an IPv6 literal, it has the `[]` unwrapped before
480-
being stored in the `:host` field. Note this doesn't match the formal
481-
grammar for hostnames, which preserves the `[]` around the IP. You can
482-
parse the IP address by calling `:inet.parse_address/1` (remember to
483-
call `String.to_charlist/1` to convert the host to a charlist before
484-
calling `:inet`).
485-
486-
If a `%URI{}` struct is given to this function, this function returns it
487-
unmodified.
489+
When a URI is given without a port, the value returned by `URI.default_port/1`
490+
for the URI's scheme is used for the `:port` field. The scheme is also
491+
normalized to lowercase.
488492
489493
## Examples
490494
491-
iex> URI.parse("https://elixir-lang.org/")
492-
%URI{
493-
authority: "elixir-lang.org",
495+
iex> URI.new("https://elixir-lang.org/")
496+
{:ok, %URI{
494497
fragment: nil,
495498
host: "elixir-lang.org",
496499
path: "/",
497500
port: 443,
498501
query: nil,
499502
scheme: "https",
500503
userinfo: nil
501-
}
504+
}}
502505
503-
iex> URI.parse("//elixir-lang.org/")
504-
%URI{
505-
authority: "elixir-lang.org",
506+
iex> URI.new("//elixir-lang.org/")
507+
{:ok, %URI{
506508
fragment: nil,
507509
host: "elixir-lang.org",
508510
path: "/",
509511
port: nil,
510512
query: nil,
511513
scheme: nil,
512514
userinfo: nil
513-
}
515+
}}
514516
515-
iex> URI.parse("/foo/bar")
516-
%URI{
517-
authority: nil,
517+
iex> URI.new("/foo/bar")
518+
{:ok, %URI{
518519
fragment: nil,
519520
host: nil,
520521
path: "/foo/bar",
521522
port: nil,
522523
query: nil,
523524
scheme: nil,
524525
userinfo: nil
525-
}
526+
}}
526527
527-
iex> URI.parse("foo/bar")
528-
%URI{
529-
authority: nil,
528+
iex> URI.new("foo/bar")
529+
{:ok, %URI{
530530
fragment: nil,
531531
host: nil,
532532
path: "foo/bar",
533533
port: nil,
534534
query: nil,
535535
scheme: nil,
536536
userinfo: nil
537-
}
537+
}}
538538
539-
iex> URI.parse("//[fe80::]/")
540-
%URI{
541-
authority: "[fe80::]",
539+
iex> URI.new("//[fe80::]/")
540+
{:ok, %URI{
542541
fragment: nil,
543542
host: "fe80::",
544543
path: "/",
545544
port: nil,
546545
query: nil,
547546
scheme: nil,
548547
userinfo: nil
548+
}}
549+
550+
iex> URI.new("https:?query")
551+
{:ok, %URI{
552+
fragment: nil,
553+
host: nil,
554+
path: "",
555+
port: 443,
556+
query: "query",
557+
scheme: "https",
558+
userinfo: nil
559+
}}
560+
561+
iex> URI.new("/invalid_greater_than_in_path/>")
562+
{:error, ">"}
563+
564+
Giving an existing URI simply returns it wrapped in a tuple:
565+
566+
iex> {:ok, uri} = URI.new("https://elixir-lang.org/")
567+
iex> URI.new(uri)
568+
{:ok, %URI{
569+
fragment: nil,
570+
host: "elixir-lang.org",
571+
path: "/",
572+
port: 443,
573+
query: nil,
574+
scheme: "https",
575+
userinfo: nil
576+
}}
577+
"""
578+
@doc since: "1.13.0"
579+
@spec new(t() | String.t()) :: {:ok, t()} | {:error, String.t()}
580+
def new(%URI{} = uri), do: {:ok, uri}
581+
582+
def new(binary) when is_binary(binary) do
583+
case :uri_string.parse(binary) do
584+
%{} = map -> {:ok, uri_from_map(map)}
585+
{:error, :invalid_uri, term} -> {:error, Kernel.to_string(term)}
586+
end
587+
end
588+
589+
@doc """
590+
Similar to `new/0` but raises `URI.Error` if an invalid string is given.
591+
592+
## Examples
593+
594+
iex> URI.new!("https://elixir-lang.org/")
595+
%URI{
596+
fragment: nil,
597+
host: "elixir-lang.org",
598+
path: "/",
599+
port: 443,
600+
query: nil,
601+
scheme: "https",
602+
userinfo: nil
603+
}
604+
605+
iex> URI.new!("/invalid_greater_than_in_path/>")
606+
** (URI.Error) cannot parse due to reason invalid_uri: ">"
607+
608+
Giving an existing URI simply returns it:
609+
610+
iex> uri = URI.new!("https://elixir-lang.org/")
611+
iex> URI.new!(uri)
612+
%URI{
613+
fragment: nil,
614+
host: "elixir-lang.org",
615+
path: "/",
616+
port: 443,
617+
query: nil,
618+
scheme: "https",
619+
userinfo: nil
549620
}
550621
"""
622+
@doc since: "1.13.0"
623+
@spec new!(t() | String.t()) :: t()
624+
def new!(%URI{} = uri), do: uri
625+
626+
def new!(binary) when is_binary(binary) do
627+
case :uri_string.parse(binary) do
628+
%{} = map ->
629+
uri_from_map(map)
630+
631+
{:error, reason, part} ->
632+
raise Error, action: :parse, reason: reason, part: Kernel.to_string(part)
633+
end
634+
end
635+
636+
defp uri_from_map(map) do
637+
uri = Map.merge(%URI{}, map)
638+
639+
case map do
640+
%{scheme: scheme} ->
641+
scheme = String.downcase(scheme, :ascii)
642+
643+
case map do
644+
%{port: _} ->
645+
%{uri | scheme: scheme}
646+
647+
%{} ->
648+
case default_port(scheme) do
649+
nil -> %{uri | scheme: scheme}
650+
port -> %{uri | scheme: scheme, port: port}
651+
end
652+
end
653+
654+
%{} ->
655+
uri
656+
end
657+
end
658+
659+
@doc """
660+
Parses a well-formed URI into its components.
661+
662+
This function is deprecated as it fails to raise in case of invalid URIs.
663+
Use `URI.new!/1` or `URI.new/1` instead. In case you want to mimic the
664+
behaviour of this function, you can do:
665+
666+
case URI.new(path) do
667+
{:ok, uri} -> uri
668+
{:error, _, _} -> %URI{path: path}
669+
end
670+
671+
Also note this function sets the authority field, but the field has been
672+
deprecated and it is not set by `URI.new!/1` and `URI.new/1`.
673+
"""
674+
@doc deprecated: "Use URI.new/1 or URI.new!/1 instead"
551675
@spec parse(t | binary) :: t
552676
def parse(%URI{} = uri), do: uri
553677

@@ -582,7 +706,6 @@ defmodule URI do
582706
parts
583707

584708
scheme = nillify(scheme)
585-
path = nillify(path)
586709
query = nillify_query(query_with_question_mark)
587710
{authority, userinfo, host, port} = split_authority(authority_with_slashes)
588711

@@ -646,24 +769,6 @@ defmodule URI do
646769
iex> URI.to_string(uri)
647770
"foo://bar.baz"
648771
649-
Note that when creating this string representation, the `:authority` value will be
650-
used if the `:host` is `nil`. Otherwise, the `:userinfo`, `:host`, and `:port` will
651-
be used.
652-
653-
iex> URI.to_string(%URI{authority: "[email protected]:80"})
654-
655-
656-
iex> URI.to_string(%URI{userinfo: "bar", host: "example.org", port: 81})
657-
658-
659-
iex> URI.to_string(%URI{
660-
...> authority: "[email protected]:80",
661-
...> userinfo: "bar",
662-
...> host: "example.org",
663-
...> port: 81
664-
...> })
665-
666-
667772
"""
668773
@spec to_string(t) :: binary
669774
defdelegate to_string(uri), to: String.Chars.URI
@@ -711,7 +816,7 @@ defmodule URI do
711816
merge(parse(base), parse(rel))
712817
end
713818

714-
defp merge_paths(nil, rel_path), do: merge_paths("/", rel_path)
819+
defp merge_paths("", rel_path), do: merge_paths("/", rel_path)
715820
defp merge_paths(_, "/" <> _ = rel_path), do: remove_dot_segments_from_path(rel_path)
716821

717822
defp merge_paths(base_path, rel_path) do
@@ -750,12 +855,10 @@ defmodule URI do
750855
end
751856

752857
defimpl String.Chars, for: URI do
753-
def to_string(%{host: host, authority: authority, path: path} = uri)
754-
when (host != nil or authority != nil) and is_binary(path) and
755-
path != "" and binary_part(path, 0, 1) != "/" do
858+
def to_string(%{host: host, path: path} = uri)
859+
when host != nil and path != "" and binary_part(path, 0, 1) != "/" do
756860
raise ArgumentError,
757-
":path in URI must be nil or an absolute path if :host or :authority are given, " <>
758-
"got: #{inspect(uri)}"
861+
":path in URI must be empty or an absolute path if URL has a :host, got: #{inspect(uri)}"
759862
end
760863

761864
def to_string(%{scheme: scheme, port: port, path: path, query: query, fragment: fragment} = uri) do

0 commit comments

Comments
 (0)