URL.t is an absolute URL. Parsing follows the WHATWG URL Standard, the rules browsers use for href, and the value keeps only the canonical form: the scheme and host are lowercased, a default port is dropped, dot segments are resolved, and characters that cannot appear raw are percent-encoded. Two URLs are equal when their canonical forms are.
let docsdocs: URL = must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ](
"HTTPS://Example.COM:443/guide/./start/../install?step=1#top""HTTPS://Example.COM:443/guide/./start/../install?step=1#top": String,
);
Canonical Form
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(docsdocs: URL) == "https://example.com/guide/install?step=1#top""https://example.com/guide/install?step=1#top": String;
expect ✓docsdocs: URL == must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("https://example.com/guide/install?step=1#top""https://example.com/guide/install?step=1#top": String);URL.parse accepts only absolute URLs. Anything else fails with :parse, like the other stdlib parsers; resolve a relative reference against a base with URL.join instead.
let parse_messageparse_message: fn(text: String) -> String = (texttext: String) =>
match URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ](texttext: String) {
| urlurl: URL -> "parsed ""parsed ": String ++ URL.to_stringto_string: pure fn!(url: URL) -> String(urlurl: URL)
| error :parse(messagemessage: String) -> messagemessage: String
};
Parse Failures
expect ✓parse_messageparse_message: fn(text: String) -> String("/guide""/guide": String) == "invalid URL `/guide`""invalid URL `/guide`": String;
expect ✓parse_messageparse_message: fn(text: String) -> String("https://example.com:99999""https://example.com:99999": String) == "invalid URL `https://example.com:99999`""invalid URL `https://example.com:99999`": String;
expect ✓parse_messageparse_message: fn(text: String) -> String("http://[::1""http://[::1": String) == "invalid URL `http://[::1`""invalid URL `http://[::1`": String;Reading the parts
Accessors read the canonical form. path is the serialized path, still percent-encoded. segments, query, and fragment decode. The query is a list of name and value pairs in order, with repeated names kept, decoded the way HTML forms encode them, so + reads as a space.
let searchsearch: URL = must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ](
"https://example.com/docs/caf%C3%A9 menu/?q=ship+lang&tag=a&tag=b#see%20also""https://example.com/docs/caf%C3%A9 menu/?q=ship+lang&tag=a&tag=b#see%20also": String,
);
Components
expect ✓URL.schemescheme: pure fn!(url: URL) -> String(searchsearch: URL) == "https""https": String;
expect ✓URL.hosthost: pure fn!(url: URL) -> Option(String)(searchsearch: URL) == SomeSome: fn(String) -> Option(String)("example.com""example.com": String);
expect ✓URL.portport: pure fn!(url: URL) -> Option(Int)(searchsearch: URL) == NoneNone: Option(Int);
expect ✓URL.pathpath: pure fn!(url: URL) -> String(searchsearch: URL) == "/docs/caf%C3%A9%20menu/""/docs/caf%C3%A9%20menu/": String;
expect ✓URL.segmentssegments: pure fn!(url: URL) -> Vector(String)(searchsearch: URL) == ["docs""docs": String, "café menu""café menu": String, """": String];
expect ✓URL.queryquery: pure fn!(url: URL) -> Vector((String, String))(searchsearch: URL) == [("q""q": String, "ship lang""ship lang": String), ("tag""tag": String, "a""a": String), ("tag""tag": String, "b""b": String)];
expect ✓URL.query_valuequery_value: pure fn!(url: URL, key: String) -> Option(String)(searchsearch: URL, "tag""tag": String) == SomeSome: fn(String) -> Option(String)("a""a": String);
expect ✓URL.query_valuequery_value: pure fn!(url: URL, key: String) -> Option(String)(searchsearch: URL, "page""page": String) == NoneNone: Option(String);
expect ✓URL.fragmentfragment: pure fn!(url: URL) -> Option(String)(searchsearch: URL) == SomeSome: fn(String) -> Option(String)("see also""see also": String);The root path / has no segments, and a trailing slash leaves an empty last segment. A port is reported only when it is not the scheme's default.
Paths And Ports
expect ✓URL.segmentssegments: pure fn!(url: URL) -> Vector(String)(must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("https://example.com""https://example.com": String)) == [];
expect ✓URL.segmentssegments: pure fn!(url: URL) -> Vector(String)(must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("https://example.com/docs/""https://example.com/docs/": String)) == ["docs""docs": String, """": String];
expect ✓URL.portport: pure fn!(url: URL) -> Option(Int)(must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("http://localhost:8080/""http://localhost:8080/": String)) == SomeSome: fn(Int) -> Option(Int)(80808080: Int);
expect ✓URL.portport: pure fn!(url: URL) -> Option(Int)(must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("ws://example.com:80/""ws://example.com:80/": String)) == NoneNone: Option(Int);Hosts are serialized the way a browser sends them: international domains become ASCII, IPv4 addresses are normalized, and IPv6 addresses keep their brackets. A URL such as mailto: has no host and an opaque path, which has no segments.
Hosts
expect ✓URL.hosthost: pure fn!(url: URL) -> Option(String)(must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("https://bücher.example/""https://bücher.example/": String)) == SomeSome: fn(String) -> Option(String)("xn--bcher-kva.example""xn--bcher-kva.example": String);
expect ✓URL.hosthost: pure fn!(url: URL) -> Option(String)(must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("http://0x7f.1/""http://0x7f.1/": String)) == SomeSome: fn(String) -> Option(String)("127.0.0.1""127.0.0.1": String);
expect ✓URL.hosthost: pure fn!(url: URL) -> Option(String)(must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("http://[0:0::1]:8080/""http://[0:0::1]:8080/": String)) == SomeSome: fn(String) -> Option(String)("[::1]""[::1]": String);
let mailmail: URL = must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("mailto:ada@example.com?subject=hello%20there""mailto:ada@example.com?subject=hello%20there": String);
expect ✓URL.hosthost: pure fn!(url: URL) -> Option(String)(mailmail: URL) == NoneNone: Option(String);
expect ✓URL.pathpath: pure fn!(url: URL) -> String(mailmail: URL) == "ada@example.com""ada@example.com": String;
expect ✓URL.segmentssegments: pure fn!(url: URL) -> Vector(String)(mailmail: URL) == [];
expect ✓URL.queryquery: pure fn!(url: URL) -> Vector((String, String))(mailmail: URL) == [("subject""subject": String, "hello there""hello there": String)];Building URLs
URL.make takes decoded parts as labeled arguments. Omitted parts are empty. Each part is percent-encoded for its position, so a / inside a segment or a & inside a query value stays data, and the result is parsed like any other URL: the host is validated and normalized and a default port disappears.
Building
let urlurl: URL = must URL.makemake: pure fn(~scheme: String, ~host: String, ~port: Option(Int), ~segments: Vector(String) = Vector.empty(), ~query: Vector((String, String)) = Vector.empty(), ~fragment: Option(String)) -> URL, exn [ :parse(String) ](
scheme = "https""https": String,
host = "Example.com""Example.com": String,
segments = ["files""files": String, "50%/off""50%/off": String, "ü""ü": String],
query = [("q""q": String, "ship & go""ship & go": String), ("tag""tag": String, "a+b""a+b": String)],
fragment = Some("part 2""part 2": String),
);
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(urlurl: URL)
== "https://example.com/files/50%25%2Foff/%C3%BC?q=ship+%26+go&tag=a%2Bb#part%202""https://example.com/files/50%25%2Foff/%C3%BC?q=ship+%26+go&tag=a%2Bb#part%202": String;
expect ✓URL.segmentssegments: pure fn!(url: URL) -> Vector(String)(urlurl: URL) == ["files""files": String, "50%/off""50%/off": String, "ü""ü": String];
expect ✓URL.queryquery: pure fn!(url: URL) -> Vector((String, String))(urlurl: URL) == [("q""q": String, "ship & go""ship & go": String), ("tag""tag": String, "a+b""a+b": String)];
let locallocal: URL = must URL.makemake: pure fn(~scheme: String, ~host: String, ~port: Option(Int), ~segments: Vector(String) = Vector.empty(), ~query: Vector((String, String)) = Vector.empty(), ~fragment: Option(String)) -> URL, exn [ :parse(String) ](scheme = "http""http": String, host = "[::1]""[::1]": String, port = Some(80808080: Int));
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(locallocal: URL) == "http://[::1]:8080/""http://[::1]:8080/": String;
let default_portdefault_port: URL = must URL.makemake: pure fn(~scheme: String, ~host: String, ~port: Option(Int), ~segments: Vector(String) = Vector.empty(), ~query: Vector((String, String)) = Vector.empty(), ~fragment: Option(String)) -> URL, exn [ :parse(String) ](
scheme = "https""https": String,
host = "example.com""example.com": String,
port = Some(443443: Int),
);
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(default_portdefault_port: URL) == "https://example.com/""https://example.com/": String;Building fails with :parse when a part cannot be what it claims: a scheme that is not a scheme, a host that does not parse, or a . or .. segment, which parsing would remove.
let make_messagemake_message: fn(~scheme: String = "https", ~host: String = "example.com", ~segments: Vector(String) = Vector.empty()) -> String = fn(~schemescheme: String = "https""https": String, ~hosthost: String = "example.com""example.com": String, ~segmentssegments: Vector(String) = Vector.emptyempty: pure fn!('a)() -> Vector(String)()) {
match URL.makemake: pure fn(~scheme: String, ~host: String, ~port: Option(Int), ~segments: Vector(String) = Vector.empty(), ~query: Vector((String, String)) = Vector.empty(), ~fragment: Option(String)) -> URL, exn [ :parse(String) ](scheme = schemescheme: String, host = hosthost: String, segments = segmentssegments: Vector(String)) {
| urlurl: URL -> "made ""made ": String ++ URL.to_stringto_string: pure fn!(url: URL) -> String(urlurl: URL)
| error :parse(messagemessage: String) -> messagemessage: String
}
};
Building Failures
expect ✓make_messagemake_message: fn(~scheme: String = "https", ~host: String = "example.com", ~segments: Vector(String) = Vector.empty()) -> String(scheme = "a:b""a:b": String) == "invalid URL scheme `a:b`""invalid URL scheme `a:b`": String;
expect ✓make_messagemake_message: fn(~scheme: String = "https", ~host: String = "example.com", ~segments: Vector(String) = Vector.empty()) -> String(host = "example.com/admin""example.com/admin": String) == "invalid URL `https://example.com%2Fadmin/`""invalid URL `https://example.com%2Fadmin/`": String;
expect ✓make_messagemake_message: fn(~scheme: String = "https", ~host: String = "example.com", ~segments: Vector(String) = Vector.empty()) -> String(segments = ["docs""docs": String, "..""..": String]) == "path segment `..` is removed by URL parsing""path segment `..` is removed by URL parsing": String;Updating URLs
with_segments, with_query, and with_fragment replace one part and keep the rest. An empty query or None fragment removes that part. Replacing the query or fragment cannot fail. Replacing the path fails on dot segments and on a URL whose path is opaque.
Updates
let pagepage: URL = must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("https://example.com/docs/intro?draft=1#top""https://example.com/docs/intro?draft=1#top": String);
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(must URL.with_segmentswith_segments: pure fn(url: URL, segments: Vector(String)) -> URL, exn [ :parse(String) ](pagepage: URL, ["docs""docs": String, "a b""a b": String]))
== "https://example.com/docs/a%20b?draft=1#top""https://example.com/docs/a%20b?draft=1#top": String;
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(URL.with_querywith_query: pure fn!(url: URL, query: Vector((String, String))) -> URL(pagepage: URL, [("page""page": String, "2""2": String)]))
== "https://example.com/docs/intro?page=2#top""https://example.com/docs/intro?page=2#top": String;
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(URL.with_querywith_query: pure fn!(url: URL, query: Vector((String, String))) -> URL(pagepage: URL, [])) == "https://example.com/docs/intro#top""https://example.com/docs/intro#top": String;
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(URL.with_fragmentwith_fragment: pure fn!(url: URL, fragment: Option(String)) -> URL(pagepage: URL, None)) == "https://example.com/docs/intro?draft=1""https://example.com/docs/intro?draft=1": String;
let opaqueopaque: String = match URL.with_segmentswith_segments: pure fn(url: URL, segments: Vector(String)) -> URL, exn [ :parse(String) ](
must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("mailto:ada@example.com""mailto:ada@example.com": String),
["x""x": String],
) {
| urlurl: URL -> URL.to_stringto_string: pure fn!(url: URL) -> String(urlurl: URL)
| error :parse(messagemessage: String) -> messagemessage: String
};
expect ✓opaqueopaque: String == "URL `mailto:ada@example.com` has an opaque path""URL `mailto:ada@example.com` has an opaque path": String;Resolving references
URL.join resolves a reference against a base exactly as a browser resolves a link on the base page: a relative path, a query, a fragment, a scheme-relative //host reference, or a whole absolute URL.
let basebase: URL = must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("https://example.com/docs/guide/intro?x=1#top""https://example.com/docs/guide/intro?x=1#top": String);
let joinedjoined: fn(reference: String) -> String = (referencereference: String) =>
match URL.joinjoin: pure fn(base: URL, reference: String) -> URL, exn [ :parse(String) ](basebase: URL, referencereference: String) {
| urlurl: URL -> URL.to_stringto_string: pure fn!(url: URL) -> String(urlurl: URL)
| error :parse(messagemessage: String) -> messagemessage: String
};
Resolving References
expect ✓joinedjoined: fn(reference: String) -> String("../api""../api": String) == "https://example.com/docs/api""https://example.com/docs/api": String;
expect ✓joinedjoined: fn(reference: String) -> String("setup""setup": String) == "https://example.com/docs/guide/setup""https://example.com/docs/guide/setup": String;
expect ✓joinedjoined: fn(reference: String) -> String("?page=2""?page=2": String) == "https://example.com/docs/guide/intro?page=2""https://example.com/docs/guide/intro?page=2": String;
expect ✓joinedjoined: fn(reference: String) -> String("#install""#install": String) == "https://example.com/docs/guide/intro?x=1#install""https://example.com/docs/guide/intro?x=1#install": String;
expect ✓joinedjoined: fn(reference: String) -> String("//cdn.example.com/app.js""//cdn.example.com/app.js": String) == "https://cdn.example.com/app.js""https://cdn.example.com/app.js": String;
expect ✓joinedjoined: fn(reference: String) -> String("mailto:ada@example.com""mailto:ada@example.com": String) == "mailto:ada@example.com""mailto:ada@example.com": String;
let mailmail: URL = must URL.parseparse: pure fn(string: String) -> URL, exn [ :parse(String) ]("mailto:ada@example.com""mailto:ada@example.com": String);
expect ✓URL.to_stringto_string: pure fn!(url: URL) -> String(must URL.joinjoin: pure fn(base: URL, reference: String) -> URL, exn [ :parse(String) ](mailmail: URL, "#note""#note": String)) == "mailto:ada@example.com#note""mailto:ada@example.com#note": String;
let refusedrefused: String = match URL.joinjoin: pure fn(base: URL, reference: String) -> URL, exn [ :parse(String) ](mailmail: URL, "other""other": String) {
| urlurl: URL -> URL.to_stringto_string: pure fn!(url: URL) -> String(urlurl: URL)
| error :parse(messagemessage: String) -> messagemessage: String
};
expect ✓refusedrefused: String == "invalid URL reference `other`""invalid URL reference `other`": String;Serialization
A URL encodes as its canonical string. Decoding parses, so a document with a relative or malformed URL is rejected at the boundary.
type Link = {
label: String;
target: URL;
}
Serialization
let linklink: Book.URLs.Link = Link { label = "Install""Install": String; target = docsdocs: URL; };
let bodybody: Bytes = must Json.encodeencode: pure fn('a: Encode)(value: Book.URLs.Link) -> Bytes, exn [ :encode(Wire.EncodeError) ](linklink: Book.URLs.Link);
expect ✓must String.of_bytesof_bytes: pure fn(bytes: Bytes) -> String, exn [ :utf8(Int) ](bodybody: Bytes)
== "{\"label\":\"Install\",\"target\":\"https://example.com/guide/install?step=1#top\"}""{\"label\":\"Install\",\"target\":\"https://example.com/guide/install?step=1#top\"}": String;
expect ✓must Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.URLs.Link), ~max_depth: Int = 128) -> Book.URLs.Link, exn [ :decode(Wire.DecodeError) ](bodybody: Bytes, Link) == linklink: Book.URLs.Link;
expect ✓Display.displaydisplay: pure fn!('a: Display)(value: URL) -> String(docsdocs: URL) == "https://example.com/guide/install?step=1#top""https://example.com/guide/install?step=1#top": String;
let rejectedrejected: String = match Json.decodedecode: pure fn('target: Decode)(body: Bytes, target: Struct(Book.URLs.Link), ~max_depth: Int = 128) -> Book.URLs.Link, exn [ :decode(Wire.DecodeError) ](
b"{\"label\":\"x\",\"target\":\"/install\"}"b"{\"label\":\"x\",\"target\":\"/install\"}": Bytes,
Link,
) {
| _ -> "decoded""decoded": String
| error :decode(failurefailure: Wire.DecodeError) -> failurefailure: Wire.DecodeError.messagemessage: String
};
expect ✓rejectedrejected: String == "invalid URL: invalid URL `/install`""invalid URL: invalid URL `/install`": String;In the browser
Browser builds parse with the platform's URL and run the same accessors and builders, so a URL reads, builds, and resolves the same way on both targets.
URL values agree in the interpreter and in browser programs: WHATWG parsing and canonical serialization, component reads, percent-decoding, relative resolution, building, updates, failures, and JSON.
$ship run main.shp
parse:https://User:Pw@example.com/a/c%20d/?q=1+2&q=x%20y#Top%20it scheme=https host=Some("example.com") port=None path=/a/c%20d/ segments=vector ["a", "c d", ""] query=vector [("q", "1 2"), ("q", "x y")] fragment=Some("Top it")
parse:https://xn--bcher-kva.example/stra%C3%9Fe?%C3%A4=%C3%B6#%C3%BC scheme=https host=Some("xn--bcher-kva.example") port=None path=/stra%C3%9Fe segments=vector ["straße"] query=vector [("ä", "ö")] fragment=Some("ü")
parse:http://127.0.0.1/ scheme=http host=Some("127.0.0.1") port=None path=/ segments=vector [] query=vector [] fragment=None
parse:http://[::1]:8080/x scheme=http host=Some("[::1]") port=Some(8080) path=/x segments=vector ["x"] query=vector [] fragment=None
parse:https://example.com/a/b scheme=https host=Some("example.com") port=None path=/a/b segments=vector ["a", "b"] query=vector [] fragment=None
parse:foo://Host/a%20b?c%20d#e%20f scheme=foo host=Some("Host") port=None path=/a%20b segments=vector ["a b"] query=vector [("c d", "")] fragment=Some("e f")
parse:mailto:Ada@Example.com?subject=hi%20there scheme=mailto host=None port=None path=Ada@Example.com segments=vector [] query=vector [("subject", "hi there")] fragment=None
parse:web+demo:/.//not-a-host/ scheme=web+demo host=None port=None path=//not-a-host/ segments=vector ["", "not-a-host", ""] query=vector [] fragment=None
parse:file:///C:/x.txt scheme=file host=Some("") port=None path=/C:/x.txt segments=vector ["C:", "x.txt"] query=vector [] fragment=None
parse:https://example.com/?q=%27%22%3C%3E` scheme=https host=Some("example.com") port=None path=/ segments=vector [] query=vector [("q", "'\"<>`")] fragment=None
parse:https://example.com/x scheme=https host=Some("example.com") port=None path=/x segments=vector ["x"] query=vector [] fragment=None
parse:https://example.com/bar scheme=https host=Some("example.com") port=None path=/bar segments=vector ["bar"] query=vector [] fragment=None
parse:ws://example.com/ scheme=ws host=Some("example.com") port=None path=/ segments=vector [] query=vector [] fragment=None
parse:https://example.com/%FFa%E2%82?q=%F0%9F%A6%80%C3 scheme=https host=Some("example.com") port=None path=/%FFa%E2%82 segments=vector ["�a�"] query=vector [("q", "🦀�")] fragment=None
fail:error invalid URL `/relative`
fail:error invalid URL `http://[::1`
fail:error invalid URL `https://example.com:99999`
fail:error invalid URL `https://`
fail:error invalid URL `http://1.2.3.4.5`
join:https://example.com/docs/api
join:https://example.com/docs/guide/intro?y=2
join:https://example.com/docs/guide/intro?x=1#install
join:https://cdn.example.com/a.js
join:https://example.com/x%20y
join:https://example.com/docs/guide/intro?x=1
join:mailto:ada@example.com#note
join:error invalid URL reference `other`
make:https://xn--bcher-kva.example:8443/docs/a%20b/50%25%2Foff/%C3%BC/@me:x?q=ship+%26+go&tag=a%2Bb&tag=%C3%A9#sec%201/2?
make:https://example.com/
make:http://[::1]/
make:error invalid URL `https://evil.com%2Fx/`
make:error invalid URL scheme `a:b`
make:error path segment `..` is removed by URL parsing
update:https://example.com/c%20d/?x=1#top
update:web+demo:/.//y
update:error URL `mailto:ada@example.com` has an opaque path
update:https://example.com/docs/guide/intro?page=2&q=a+b#top
update:https://example.com/docs/guide/intro#top
update:https://example.com/docs/guide/intro?x=1#a%20b
update:https://example.com/docs/guide/intro?x=1
json:{"label":"Docs","target":"https://example.com/docs/guide/intro?x=1#top"}
decoded:TrueURL values agree in the interpreter and in browser programs: WHATWG parsing and canonical serialization, component reads, percent-decoding, relative resolution, building, updates, failures, and JSON.
$mkdir -p build/test && rm -f build/test/browser.txt && BROWSER_OUTPUT="$PWD/build/test/browser.txt" ship test --browser --spec browser-output.spec.js . >/dev/null && cat build/test/browser.txt
parse:https://User:Pw@example.com/a/c%20d/?q=1+2&q=x%20y#Top%20it scheme=https host=Some("example.com") port=None path=/a/c%20d/ segments=vector ["a", "c d", ""] query=vector [("q", "1 2"), ("q", "x y")] fragment=Some("Top it")
parse:https://xn--bcher-kva.example/stra%C3%9Fe?%C3%A4=%C3%B6#%C3%BC scheme=https host=Some("xn--bcher-kva.example") port=None path=/stra%C3%9Fe segments=vector ["straße"] query=vector [("ä", "ö")] fragment=Some("ü")
parse:http://127.0.0.1/ scheme=http host=Some("127.0.0.1") port=None path=/ segments=vector [] query=vector [] fragment=None
parse:http://[::1]:8080/x scheme=http host=Some("[::1]") port=Some(8080) path=/x segments=vector ["x"] query=vector [] fragment=None
parse:https://example.com/a/b scheme=https host=Some("example.com") port=None path=/a/b segments=vector ["a", "b"] query=vector [] fragment=None
parse:foo://Host/a%20b?c%20d#e%20f scheme=foo host=Some("Host") port=None path=/a%20b segments=vector ["a b"] query=vector [("c d", "")] fragment=Some("e f")
parse:mailto:Ada@Example.com?subject=hi%20there scheme=mailto host=None port=None path=Ada@Example.com segments=vector [] query=vector [("subject", "hi there")] fragment=None
parse:web+demo:/.//not-a-host/ scheme=web+demo host=None port=None path=//not-a-host/ segments=vector ["", "not-a-host", ""] query=vector [] fragment=None
parse:file:///C:/x.txt scheme=file host=Some("") port=None path=/C:/x.txt segments=vector ["C:", "x.txt"] query=vector [] fragment=None
parse:https://example.com/?q=%27%22%3C%3E` scheme=https host=Some("example.com") port=None path=/ segments=vector [] query=vector [("q", "'\"<>`")] fragment=None
parse:https://example.com/x scheme=https host=Some("example.com") port=None path=/x segments=vector ["x"] query=vector [] fragment=None
parse:https://example.com/bar scheme=https host=Some("example.com") port=None path=/bar segments=vector ["bar"] query=vector [] fragment=None
parse:ws://example.com/ scheme=ws host=Some("example.com") port=None path=/ segments=vector [] query=vector [] fragment=None
parse:https://example.com/%FFa%E2%82?q=%F0%9F%A6%80%C3 scheme=https host=Some("example.com") port=None path=/%FFa%E2%82 segments=vector ["�a�"] query=vector [("q", "🦀�")] fragment=None
fail:error invalid URL `/relative`
fail:error invalid URL `http://[::1`
fail:error invalid URL `https://example.com:99999`
fail:error invalid URL `https://`
fail:error invalid URL `http://1.2.3.4.5`
join:https://example.com/docs/api
join:https://example.com/docs/guide/intro?y=2
join:https://example.com/docs/guide/intro?x=1#install
join:https://cdn.example.com/a.js
join:https://example.com/x%20y
join:https://example.com/docs/guide/intro?x=1
join:mailto:ada@example.com#note
join:error invalid URL reference `other`
make:https://xn--bcher-kva.example:8443/docs/a%20b/50%25%2Foff/%C3%BC/@me:x?q=ship+%26+go&tag=a%2Bb&tag=%C3%A9#sec%201/2?
make:https://example.com/
make:http://[::1]/
make:error invalid URL `https://evil.com%2Fx/`
make:error invalid URL scheme `a:b`
make:error path segment `..` is removed by URL parsing
update:https://example.com/c%20d/?x=1#top
update:web+demo:/.//y
update:error URL `mailto:ada@example.com` has an opaque path
update:https://example.com/docs/guide/intro?page=2&q=a+b#top
update:https://example.com/docs/guide/intro#top
update:https://example.com/docs/guide/intro?x=1#a%20b
update:https://example.com/docs/guide/intro?x=1
json:{"label":"Docs","target":"https://example.com/docs/guide/intro?x=1#top"}
decoded:True