uri

A generic URI/IRI handling library compliant with RFC 3986/3987, ported from the Rust crate fluent-uri.

uri
iri
url
parser
rfc3986
moon add marianoguerra/uri@0.1.0
Download zip
Version
0.1.0
License
Apache-2.0
Last updated
2 hours ago
Downloads
3
README

#marianoguerra/uri

A generic URI/IRI handling library for MoonBit, compliant with RFC 3986 and RFC 3987.

This is a port of the Rust crate fluent-uri (revision 76e10ae, the development version that follows 0.4.1), by Scallop Ye, which is MIT licensed. The parsing, normalization and resolution algorithms follow it closely, and a generated conformance test suite keeps the two in step; see Conformance testing.

#Installation

moon add marianoguerra/uri

Then import the package in the moon.pkg of the package that uses it:

import { "marianoguerra/uri", // Only needed to name an encoder or a table, see Packages below. "marianoguerra/uri/enc", }

Every backend is supported: the test suite runs on wasm, wasm-gc, js and native.

#Terminology

A URI reference is either a URI or a relative reference. If it starts with a scheme (like http, ftp or mailto) followed by a colon, it is a URI; http://example.com/ and mailto:user@example.com are URIs. Otherwise it is a relative reference; //example.org/, /index.html, ../, foo, ?bar and #baz are relative references.

An IRI (reference) is an internationalized URI (reference), which may contain non-ASCII characters.

The four types Uri, UriRef, Iri and IriRef cover these four combinations. They share the same API; the reference types have an optional scheme, and the IRI types allow non-ASCII characters.

#Parsing

Parsing splits a string into its components. Every component is a field, so no accessor call is needed to reach one:

///|
test "parsing" {
let uri = @uri.Uri::parse(
"foo://user@example.com:8042/over/there?name=ferret#nose",
)
assert_eq(uri.scheme, @uri.Scheme::new_or_panic("foo"))
let auth = uri.authority.unwrap()
assert_eq(auth.userinfo.unwrap().as_str(), "user")
assert_eq(auth.host, "example.com")
assert_eq(auth.port.unwrap().as_str(), "8042")
assert_eq(uri.path.as_str(), "/over/there")
assert_eq(uri.query.unwrap().as_str(), "name=ferret")
assert_eq(uri.fragment.unwrap().as_str(), "nose")
}

A parsed host tells an IP address apart from a registered name:

///|
test "hosts" {
let uri = @uri.Uri::parse("foo://127.0.0.1")
assert_true(uri.authority.unwrap().host_parsed is Ipv4(_))
let uri = @uri.Uri::parse("foo://[::1]")
assert_true(
uri.authority.unwrap().host_parsed is Ipv6(addr) &&
addr == @uri.ipv6_localhost,
)
let uri = @uri.Uri::parse("foo://localhost")
assert_true(
uri.authority.unwrap().host_parsed is RegName(name) &&
name.as_str() == "localhost",
)
}

Parsing raises a ParseError that says where and why it failed:

///|
test "parse errors" {
try @uri.Uri::parse("foo bar") catch {
e => {
assert_eq(e.index(), 3)
inspect(e, content="unexpected character or end of input at index 3")
}
} noraise {
_ => fail("expected a parse error")
}
}

#Percent-encoding

Components are exposed as [EStr] values, which are strings known to be properly percent-encoded for their component. The type parameter names the encoder, which carries the table of byte patterns the component allows.

Always split before decoding: decoding first would let the data be mistaken for delimiters.

///|
test "decoding a query string" {
let query = @uri.UriRef::parse(
"?name=%E5%BC%A0%E4%B8%89&speech=%C2%A1Ol%C3%A9%21",
).query.unwrap()
let map : Map[String, String] = Map([])
for pair in query.split('&') {
let (k, v) = pair.split_once('=').unwrap()
map[k.decode_to_string_lossy()] = v.decode_to_string_lossy()
}
assert_eq(map["name"], "张三")
assert_eq(map["speech"], "¡Olé!")
}

Percent-encoding goes the other way, through an [EString] buffer. Encode data with a data table (@enc.table_data or @enc.table_idata), never with the table of the component itself, or delimiters in the data would survive unencoded:

///|
test "encoding a query string" {
let buf : @uri.EString[@enc.Query] = @uri.EString::new()
for pair in [("name", "张三"), ("speech", "¡Olé!")] {
let (k, v) = pair
if !buf.is_empty() {
buf.push('&')
}
buf.encode(k, table=@enc.table_data)
buf.push('=')
buf.encode(v, table=@enc.table_data)
}
let uri_ref = @uri.UriRef::build(path=@uri.EStr::empty(), query=buf.to_estr())
inspect(uri_ref, content="?name=%E5%BC%A0%E4%B8%89&speech=%C2%A1Ol%C3%A9%21")
}

#Building

Components are passed as labeled arguments, in any order; the optional ones may be left out. Building fails when the components could not be put back together unambiguously:

///|
test "building" {
let uri = @uri.Uri::build(
scheme=@uri.Scheme::new_or_panic("foo"),
authority=@uri.Authority::make(
host=RegName(@uri.EStr::new_or_panic("example.com")),
port=@uri.EStr::new_or_panic("8042"),
),
path=@uri.EStr::new_or_panic("/over/there"),
)
inspect(uri, content="foo://example.com:8042/over/there")

// A relative reference whose first segment contains a colon would be
// mistaken for a scheme.
try @uri.UriRef::build(path=@uri.EStr::new_or_panic("foo:bar")) catch {
e => assert_eq(e, @uri.FirstPathSegmentContainsColon)
} noraise {
_ => fail("expected the build to fail")
}
}

#Reference resolution

///|
test "resolution" {
let base = @uri.Uri::parse("http://example.com/foo/bar")
inspect(
@uri.UriRef::parse("baz").resolve_against(base),
content="http://example.com/foo/baz",
)
inspect(
@uri.UriRef::parse("../baz").resolve_against(base),
content="http://example.com/baz",
)
inspect(
@uri.UriRef::parse("?baz").resolve_against(base),
content="http://example.com/foo/bar?baz",
)
}

By default a ".." segment that escapes the root of the base is dropped, as RFC 3986 prescribes. Pass allow_path_underflow=false to reject it instead:

///|
test "resolution underflow" {
let base = @uri.Uri::parse("http://example.com/foo/bar")
let r = @uri.UriRef::parse("../../baz")
inspect(r.resolve_against(base), content="http://example.com/baz")
try r.resolve_against(base, allow_path_underflow=false) catch {
e => assert_eq(e, @uri.PathUnderflow)
} noraise {
_ => fail("expected an underflow")
}
}

#Normalization

Normalization lowercases the scheme and the host, decodes percent-encoded octets that stand for unreserved characters, uppercases the octets it keeps, removes dot segments from an absolute path, and rewrites an IPv6 address in its canonical form:

///|
test "normalization" {
let uri = @uri.Uri::parse("eXAMPLE://a/./b/../b/%63/%7bfoo%7d")
inspect(uri.normalize(), content="example://a/b/c/%7Bfoo%7D")

// A scheme's default port is only removed if you say what it is.
let uri = @uri.Uri::parse("http://example.com:80/")
inspect(uri.normalize(), content="http://example.com:80/")
inspect(
uri.normalize(default_port=scheme => {
if scheme is "http" {
Some(80)
} else {
None
}
}),
content="http://example.com/",
)
}

#Converting between the four types

to_* methods convert the contents, and never fail:

///|
test "widening and encoding" {
let uri = @uri.Uri::parse("http://example.com/")
inspect(uri.to_uri_ref(), content="http://example.com/")
// An IRI becomes a URI by percent-encoding its non-ASCII characters.
let iri = @uri.Iri::parse("http://www.example.org/résumé.html")
inspect(iri.to_uri(), content="http://www.example.org/r%C3%A9sum%C3%A9.html")
}

as_* methods keep the contents and only retype the value, so they fail when the value does not meet the narrower type's requirements:

///|
test "narrowing" {
let uri_ref = @uri.UriRef::parse("http://example.com/")
inspect(uri_ref.as_uri(), content="http://example.com/")
let relative = @uri.UriRef::parse("relative/ref")
try relative.as_uri() catch {
e => assert_eq(e, @uri.NoScheme)
} noraise {
_ => fail("expected the conversion to fail")
}
let iri = @uri.Iri::parse("http://例え.jp/")
try iri.as_uri() catch {
e => assert_eq(e, @uri.NotAscii(index=7))
} noraise {
_ => fail("expected the conversion to fail")
}
}

#Packages

  • marianoguerra/uri holds the URI/IRI types, their components, and the percent-encoded string types EStr and EString.
  • marianoguerra/uri/enc holds the byte pattern Tables of RFC 3986/3987, the Encoder trait, and the encoder markers (Path, Query, RegName, …) that parameterize EStr. You only need to import it to name an encoder or a table.

#Differences from fluent-uri

The algorithms are ported as they are; the API is not, because Rust's and MoonBit's idioms differ.

  • No borrowed/owned split. fluent-uri has Uri<&str> and Uri<String> to let a parsed URI borrow its input. MoonBit strings are garbage collected, so there is a single Uri.
  • Components are fields, not accessors. Parsing decomposes eagerly into read-only fields, and the textual form is kept in text.
  • Labeled arguments instead of a typestate builder. Uri::build takes the components as labeled arguments, which enforces the same "set each component at most once, in order" discipline without the typestate machinery. The ordering constraints that the typestates encode are checked when building.
  • raise instead of Result. Fallible operations raise MoonBit errors.
  • Indexes count UTF-16 code units, since that is how MoonBit strings are indexed, whereas fluent-uri counts UTF-8 bytes. For ASCII input the two agree.
  • The index of a malformed percent-encoded octet is measured from the start of the component being read, exactly as the reference implementation does, so it can point before the offending octet. The conformance suite pins this behavior.
  • IP addresses have their own types. Ipv4Addr and Ipv6Addr are provided by this package rather than by the standard library, and Ipv6Addr formats itself in the canonical form of RFC 5952.
  • No socket_addrs and no serde. Name resolution and serialization frameworks are out of scope.

#Testing

moon test # unit, ported, property and conformance tests moon coverage analyze # coverage report

The suite has four layers:

  1. Ported tests. parse_test.mbt, parse_ip_test.mbt, normalize_test.mbt, resolve_test.mbt and convert_test.mbt are translations of the corresponding files in fluent-uri's tests/ directory, plus estr_test.mbt, build_test.mbt and misc_test.mbt for the rest of the API.
  2. Documentation tests. Every public function documents itself with a mbt check example that moon test runs.
  3. Property-based tests. property_test.mbt replays the properties of fluent-uri's fuzz targets against seeded generators (gen_test.mbt): parsing decomposes losslessly, normalization is stable and idempotent, resolution commutes with normalization, percent-encoding round-trips, the three decoding methods agree, and IPv6 addresses survive their canonical form.
  4. Conformance testing. See below.

#Conformance testing

tools/oracle is a Rust program that links against fluent-uri itself. It generates URI/IRI inputs, records what the reference implementation does with each of them, and writes conformance_test.mbt, which replays the same inputs against this port. Any divergence in parsing, in the decomposition into components, in normalization, in reference resolution, in fragment manipulation or in the conversion of an IRI to a URI shows up as a failing test.

Regenerate the checked-in suite with:

cargo run --release --manifest-path tools/oracle/Cargo.toml -- --out conformance_test.mbt

To fuzz against the oracle rather than replay the checked-in corpus, generate a much larger suite with a fresh seed, run it, and throw it away:

cargo run --release --manifest-path tools/oracle/Cargo.toml -- --seed 42 --count 50000 --out conformance_test.mbt moon test conformance_runner_test.mbt git checkout conformance_test.mbt

#License

This package is licensed under the Apache License 2.0, see LICENSE.

It is a derivative work of fluent-uri, copyright (c) 2024 Scallop Ye, which is licensed under the MIT license; that license is reproduced in THIRD-PARTY-NOTICES.md.

#
BuildError

pub(all) suberror BuildError {
NonemptyRootlessPath
PathStartsWithDoubleSlash
FirstPathSegmentContainsColon
} derive(Eq,
Debug
)

An error raised when building a URI/IRI (reference) from components that cannot be put back together unambiguously.
impl Show for BuildError

#
ConvertError

pub(all) suberror ConvertError {
NotAscii(index~ : Int)
NoScheme
} derive(Eq,
Debug
)

An error raised when converting a URI/IRI (reference) to a narrower type.

#
DecodeError

pub(all) suberror DecodeError {
NotUtf8(bytes~ : Bytes)
} derive(Eq,
Debug
)

An error raised when percent-decoded octets are not valid UTF-8.
impl Show for DecodeError

#
InvalidPort

pub(all) suberror InvalidPort {
InvalidPort(port~ : String)
} derive(Eq,
Debug
)

An error raised when a port cannot be converted to an integer.
impl Show for InvalidPort

#
NormalizeError

pub(all) suberror NormalizeError {
NormalizePathUnderflow
} derive(Eq,
Debug
)

An error raised when normalizing a URI/IRI (reference).

#
ParseError

pub(all) suberror ParseError {
UnexpectedCharOrEnd(index~ : Int)
InvalidIpv6Addr(index~ : Int)
} derive(Eq,
Debug
)

An error raised when parsing a URI/IRI (reference).

Indexes are counted in UTF-16 code units, i.e., they can be used directly to slice the input string.
impl Show for ParseError

#
ParseError::index

fn ParseError::index(self : ParseError) -> Int

Returns the index at which the error occurred.

#
ResolveError

pub(all) suberror ResolveError {
BaseWithFragment
InvalidReferenceAgainstOpaqueBase
PathUnderflow
} derive(Eq,
Debug
)

An error raised when resolving a URI/IRI reference.

#
Authority

pub struct Authority[U, R] {
userinfo : EStr[U]?
host : String
host_parsed : Host[R]
port : EStr[
Port
]?
} derive(Eq,
Debug
)

An authority component.

The host field holds the host subcomponent verbatim, including the square brackets that enclose an IPv6 or IPvFuture address; host_parsed holds its parsed form. Note that ASCII characters within a host are case-insensitive.

A port may be empty, have leading zeros, or be larger than 65535. It is up to you to decide whether to deny such ports, fall back to the scheme's default if it is empty, ignore the leading zeros, or use a special addressing mechanism that allows ports larger than 65535.
impl Show for Authority[U, R]

#
Authority::as_str

fn[U, R] Authority::as_str(self : Authority[U, R]) -> String

Returns the authority component as a string.

test {
let auth = @uri.Uri::parse("http://user@example.com:8080/").authority.unwrap()
inspect(auth.as_str(), content="user@example.com:8080")
}

#
Authority::empty

fn[U, R] Authority::empty() -> Authority[U, R]

An empty authority component.

#
Authority::has_port

fn[U, R] Authority::has_port(self : Authority[U, R]) -> Bool

Checks whether a port subcomponent is present.

#
Authority::has_userinfo

fn[U, R] Authority::has_userinfo(self : Authority[U, R]) -> Bool

Checks whether a userinfo subcomponent is present.

#
Authority::make

fn[U, R] Authority::make(host~ : Host[R], userinfo? : EStr[U], port? : EStr[
Port
]) -> Authority[U, R]

Creates an authority component from its subcomponents.

If the contents of a Host::RegName host match the IPv4address ABNF rule from Section 3.2.2 of RFC 3986, the resulting authority holds an [Host::Ipv4] host instead.

Note that ASCII characters within a host are case-insensitive. For consistency, you should only produce normalized hosts.

Panics

Panics if host is [Host::IpvFuture], which carries no address and so cannot be written back out.

test {
let auth : @uri.Authority[@enc.Userinfo, @enc.RegName] = @uri.Authority::make(
host=@uri.Host::RegName(@uri.EStr::new_or_panic("example.com")),
userinfo=@uri.EStr::new_or_panic("user"),
port=@uri.EStr::new_or_panic("8042"),
)
inspect(auth, content="user@example.com:8042")
}

#
Authority::port_to_u16

fn[U, R] Authority::port_to_u16(self : Authority[U, R]) -> Int? raise InvalidPort

Converts the port subcomponent to an integer, if present and nonempty.

Returns None if the port is not present or is empty. Leading zeros are ignored.

Errors

Raises [InvalidPort] if the port does not fit in a u16.

test {
let auth = @uri.Uri::parse("foo://localhost:4673/").authority.unwrap()
assert_eq(auth.port_to_u16(), Some(4673))
let auth = @uri.Uri::parse("foo://localhost:/").authority.unwrap()
assert_eq(auth.port_to_u16(), None)
}

#
EStr

pub struct EStr[E] {
inner : String
}

A percent-encoded string.

Type parameter

EStr[E] is parameterized over a type E that implements [@enc.Encoder]. The table E::table() specifies the byte patterns allowed in the string: an EStr[E] is formed by joining any number of

  • characters ch such that E::table().allows(ch), and
  • percent-encoded octets "%XX", if E::table().allows_pct_encoded().

Comparison

EStrs are compared lexicographically by their code units. Normalization is not performed prior to comparison.

Examples

Parse key-value pairs from a query string into a map:

test {
let s = "?name=%E5%BC%A0%E4%B8%89&speech=%C2%A1Ol%C3%A9%21"
let query = @uri.UriRef::parse(s).query.unwrap()
let map : Map[String, String] = Map([])
for pair in query.split('&') {
let (k, v) = pair.split_once('=').unwrap()
map[k.decode_to_string_lossy()] = v.decode_to_string_lossy()
}
assert_eq(map["name"], "张三")
assert_eq(map["speech"], "¡Olé!")
}
impl Compare for EStr[E]
impl Eq for EStr[E]
impl Hash for EStr[E]
impl Show for EStr[E]
impl Debug for EStr[E]

#
EStr::as_str

fn[E] EStr::as_str(self : EStr[E]) -> String

Returns the EStr as a string.

#
EStr::decode_to_bytes

fn[E :
Encoder
] EStr::decode_to_bytes(self : EStr[E]) -> Bytes

Percent-decodes the EStr into octets.

Always split before decoding, as otherwise the data may be mistaken for component delimiters.

Note that U+002B (+) is not decoded as 0x20 (space).

Panics

Panics if E::table() does not allow percent-encoded octets.

test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::new_or_panic("%C2%A1Hola%21")
inspect(
s.decode_to_bytes(),
content=(
#|b"\xc2\xa1Hola!"
),
)
}

#
EStr::decode_to_string

fn[E :
Encoder
] EStr::decode_to_string(self : EStr[E]) -> String raise DecodeError

Percent-decodes the EStr into a string.

Errors

Raises [DecodeError] if the decoded octets are not valid UTF-8.

Panics

Panics if E::table() does not allow percent-encoded octets.

test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::new_or_panic("%C2%A1Hola%21")
inspect(s.decode_to_string(), content="¡Hola!")
}

#
EStr::decode_to_string_lossy

fn[E :
Encoder
] EStr::decode_to_string_lossy(self : EStr[E]) -> String

Percent-decodes the EStr into a string, replacing any octets that are not valid UTF-8 with U+FFFD.

Panics

Panics if E::table() does not allow percent-encoded octets.

test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::new_or_panic("%C2Hi%FF")
inspect(s.decode_to_string_lossy(), content="�Hi�")
}

#
EStr::empty

fn[E] EStr::empty() -> EStr[E]

An empty EStr.

#
EStr::force_encode_byte

fn[E :
Encoder
] EStr::force_encode_byte(x : Byte) -> EStr[E]

Forcefully percent-encodes the given octet to an EStr.

Panics

Panics if E::table() does not allow percent-encoded octets.

test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::force_encode_byte(b'/')
inspect(s, content="%2F")
}

#
EStr::is_absolute

Checks whether the path is absolute, i.e., starting with '/'.

#
EStr::is_empty

fn[E] EStr::is_empty(self : EStr[E]) -> Bool

Checks whether the EStr is empty.

#
EStr::is_rootless

Checks whether the path is rootless, i.e., not starting with '/'.

#
EStr::length

fn[E] EStr::length(self : EStr[E]) -> Int

Returns the length of the EStr in code units.

#
EStr::new

fn[E :
Encoder
] EStr::new(s : String) -> EStr[E]?

Converts a string to an EStr, returning None if it is not properly encoded with E.

test {
let s : @uri.EStr[@enc.Path]? = @uri.EStr::new("a%20b")
assert_true(s is Some(_))
let bad : @uri.EStr[@enc.Path]? = @uri.EStr::new("a%2")
assert_true(bad is None)
}

#
EStr::new_or_panic

fn[E :
Encoder
] EStr::new_or_panic(s : String) -> EStr[E]

Converts a string to an EStr.

Panics

Panics if the string is not properly encoded with E. For a non-panicking variant, use [EStr::new].

#
EStr::rsplit_once

fn[E] EStr::rsplit_once(self : EStr[E], delim : Char) -> (EStr[E], EStr[E])?

Splits the EStr on the last occurrence of delim, returning the prefix before it and the suffix after it, or None if it is not found.

Panics

Panics if delim is not a reserved character.

#
EStr::segments_if_absolute

Returns an iterator over the path segments, separated by '/', or None if the path is rootless.

Note that the path can be empty when an authority is present, in which case this method returns None.

test {
let path = @uri.Uri::parse("file:///path/to//dir/").path
assert_eq(
path.segments_if_absolute().unwrap().map(v => v.as_str()).to_array(),
["path", "to", "", "dir", ""],
)
let path = @uri.Uri::parse("foo:bar/baz").path
assert_true(path.segments_if_absolute() is None)
}

#
EStr::split

fn[E] EStr::split(self : EStr[E], delim : Char) -> Iter[EStr[E]]

Returns an iterator over the subslices of the EStr separated by delim.

Panics

Panics if delim is not a reserved character.

test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::new_or_panic("a,b,c")
assert_eq(s.split(',').map(v => v.as_str()).to_array(), ["a", "b", "c"])
}

#
EStr::split_once

fn[E] EStr::split_once(self : EStr[E], delim : Char) -> (EStr[E], EStr[E])?

Splits the EStr on the first occurrence of delim, returning the prefix before it and the suffix after it, or None if it is not found.

Panics

Panics if delim is not a reserved character.

#
EString

pub struct EString[E] {
buf : String
}

A percent-encoded, growable string; the owned counterpart of [EStr].

Examples

Encode key-value pairs into a query string and use it to build a URI reference:

test {
let pairs = [("name", "张三"), ("speech", "¡Olé!")]
let buf : @uri.EString[@enc.Query] = @uri.EString::new()
for pair in pairs {
let (k, v) = pair
if !buf.is_empty() {
buf.push('&')
}
// WARNING: Absolutely do not confuse data with delimiters!
// Use `@enc.Data` (or `@enc.IData`) to encode data contained in a URI
// (or an IRI) unless you know what you're doing!
buf.encode(k, table=@enc.table_data)
buf.push('=')
buf.encode(v, table=@enc.table_data)
}
inspect(buf, content="name=%E5%BC%A0%E4%B8%89&speech=%C2%A1Ol%C3%A9%21")
}
impl Eq for EString[E]
impl Show for EString[E]
impl Debug for EString[E]

#
EString::clear

fn[E] EString::clear(self : EString[E]) -> Unit

Truncates the EString, removing all contents.

#
EString::encode

fn[E :
Encoder
] EString::encode(self : EString[E], s : String, table~ :
Table
) -> Unit

Percent-encodes s with table and appends the result.

table should be the table of a sub-encoder of E, typically [@enc.table_data] or [@enc.table_idata]: data contained in a URI/IRI must be encoded more strictly than the component that holds it, or it would be mistaken for delimiters.

Panics

Panics if table is not a subset of E::table(), or if table does not allow percent-encoded octets.

#
EString::is_empty

fn[E] EString::is_empty(self : EString[E]) -> Bool

Checks whether the EString is empty.

#
EString::new

fn[E] EString::new() -> EString[E]

Creates a new empty EString.

#
EString::push

fn[E :
Encoder
] EString::push(self : EString[E], ch : Char) -> Unit

Appends a character to the EString.

Panics

Panics if E::table() does not allow the character.

#
EString::push_estr

fn[E] EString::push_estr(self : EString[E], s : EStr[E]) -> Unit

Appends an [EStr] to the EString.

#
EString::to_estr

fn[E] EString::to_estr(self : EString[E]) -> EStr[E]

Returns the EString as an [EStr].

#
Host

pub(all) enum Host[R] {
Ipv4(Ipv4Addr)
Ipv6(Ipv6Addr)
IpvFuture
RegName(EStr[R])
} derive(Eq,
Debug
)

A parsed host subcomponent.

Note that ASCII characters within a host are case-insensitive.

impl Show for Host[R]

#
Ipv4Addr

pub struct Ipv4Addr(Byte, Byte, Byte, Byte) derive(Compare, Eq, Hash)

An IPv4 address.
impl Show for Ipv4Addr

#
Ipv4Addr::new

fn Ipv4Addr::new(a : Byte, b : Byte, c : Byte, d : Byte) -> Ipv4Addr

Creates an IPv4 address from its four octets.

#
Ipv4Addr::octets

fn Ipv4Addr::octets(self : Ipv4Addr) -> (Byte, Byte, Byte, Byte)

Returns the four octets of the address.

#
Ipv6Addr

pub struct Ipv6Addr(Bytes) derive(Compare, Eq, Hash)

An IPv6 address, stored as its sixteen octets.
impl Show for Ipv6Addr

#
Ipv6Addr::new

fn Ipv6Addr::new(a : Int, b : Int, c : Int, d : Int, e : Int, f : Int, g : Int, h : Int) -> Ipv6Addr

Creates an IPv6 address from its eight 16-bit segments.

#
Ipv6Addr::octets

fn Ipv6Addr::octets(self : Ipv6Addr) -> Bytes

Returns the sixteen octets of the address.

#
Ipv6Addr::segments

fn Ipv6Addr::segments(self : Ipv6Addr) -> Array[Int]

Returns the eight 16-bit segments of the address.

#
Iri

An IRI, i.e., an internationalized URI which may contain non-ASCII characters.
impl Compare for Iri
impl Eq for Iri
impl Hash for Iri
impl Show for Iri
impl Debug for Iri

#
Iri::as_uri

fn Iri::as_uri(self : Iri) -> Uri raise ConvertError

Converts the IRI to a URI if it is ASCII.

Use [Iri::to_uri] to percent-encode non-ASCII characters instead.

Errors

Raises [ConvertError] if the IRI is not ASCII.

#
Iri::normalize

fn Iri::normalize(self : Iri, default_port? : (String) -> Int?) -> Iri

Normalizes the IRI. See [Uri::normalize].

#
Iri::normalize_strict

fn Iri::normalize_strict(self : Iri, default_port? : (String) -> Int?) -> Iri raise NormalizeError

Normalizes the IRI, rejecting an underflow in path normalization. See [Uri::normalize_strict].

#
Iri::parse

fn Iri::parse(s : String) -> Iri raise ParseError

Parses an IRI from a string, matching the IRI ABNF rule from RFC 3987.

#
Iri::strip_fragment

fn Iri::strip_fragment(self : Iri) -> Iri

Returns the IRI with its fragment removed.

#
Iri::to_iri_ref

fn Iri::to_iri_ref(self : Iri) -> IriRef

Converts the IRI to an IRI reference with the same contents.

#
Iri::to_uri

fn Iri::to_uri(self : Iri) -> Uri

Converts the IRI to a URI by percent-encoding non-ASCII characters.

Punycode encoding is not performed during conversion.

test {
let iri = @uri.Iri::parse("http://www.example.org/résumé.html")
inspect(iri.to_uri(), content="http://www.example.org/r%C3%A9sum%C3%A9.html")
let iri = @uri.Iri::parse("http://résumé.example.org")
inspect(iri.to_uri(), content="http://r%C3%A9sum%C3%A9.example.org")
}

#
Iri::with_fragment

fn Iri::with_fragment(self : Iri, fragment : EStr[
IFragment
]?) -> Iri

Returns the IRI with its fragment replaced by fragment.

#
IriRef

An IRI reference, i.e., either an IRI or a relative reference.
impl Compare for IriRef
impl Eq for IriRef
impl Hash for IriRef
impl Show for IriRef

#
IriRef::as_iri

fn IriRef::as_iri(self : IriRef) -> Iri raise ConvertError

Converts the IRI reference to an IRI if it has a scheme.

Errors

Raises [ConvertError] if the IRI reference has no scheme.

#
IriRef::as_uri

fn IriRef::as_uri(self : IriRef) -> Uri raise ConvertError

Converts the IRI reference to a URI if it has a scheme and is ASCII.

Errors

Raises [ConvertError] if the IRI reference has no scheme or is not ASCII.

#
IriRef::as_uri_ref

fn IriRef::as_uri_ref(self : IriRef) -> UriRef raise ConvertError

Converts the IRI reference to a URI reference if it is ASCII.

Use [IriRef::to_uri_ref] to percent-encode non-ASCII characters instead.

Errors

Raises [ConvertError] if the IRI reference is not ASCII.

#
IriRef::build

Builds an IRI reference from its components. See [UriRef::build].

#
IriRef::normalize

fn IriRef::normalize(self : IriRef, default_port? : (String) -> Int?) -> IriRef

Normalizes the IRI reference. See [Uri::normalize].

#
IriRef::normalize_strict

fn IriRef::normalize_strict(self : IriRef, default_port? : (String) -> Int?) -> IriRef raise NormalizeError

Normalizes the IRI reference, rejecting an underflow in path normalization. See [Uri::normalize_strict].

#
IriRef::parse

fn IriRef::parse(s : String) -> IriRef raise ParseError

Parses an IRI reference from a string, matching the IRI-reference ABNF rule from RFC 3987.

#
IriRef::resolve_against

fn IriRef::resolve_against(self : IriRef, base : Iri, allow_path_underflow? : Bool) -> Iri raise ResolveError

Resolves the IRI reference against the given base IRI and returns the target IRI. See [UriRef::resolve_against] for the exact behavior.

#
IriRef::strip_fragment

fn IriRef::strip_fragment(self : IriRef) -> IriRef

Returns the IRI reference with its fragment removed.

#
IriRef::to_uri_ref

fn IriRef::to_uri_ref(self : IriRef) -> UriRef

Converts the IRI reference to a URI reference by percent-encoding non-ASCII characters.

Punycode encoding is not performed during conversion.

test {
let iri_ref = @uri.IriRef::parse("résumé.html")
inspect(iri_ref.to_uri_ref(), content="r%C3%A9sum%C3%A9.html")
let iri_ref = @uri.IriRef::parse("//résumé.example.org")
inspect(iri_ref.to_uri_ref(), content="//r%C3%A9sum%C3%A9.example.org")
}

#
IriRef::with_fragment

fn IriRef::with_fragment(self : IriRef, fragment : EStr[
IFragment
]?) -> IriRef

Returns the IRI reference with its fragment replaced by fragment.

#
Scheme

pub struct Scheme {
inner : String
}

A scheme component.

Comparison

Schemes are compared case-insensitively. Compare [Scheme::as_str] values instead for a case-sensitive comparison.

test {
let scheme = @uri.Uri::parse("HTTP://EXAMPLE.COM/").scheme
// Case-insensitive comparison.
assert_eq(scheme, @uri.Scheme::new_or_panic("http"))
// Case-sensitive comparison.
assert_eq(scheme.as_str(), "HTTP")
}
impl Eq for Scheme
impl Hash for Scheme
impl Show for Scheme

#
Scheme::as_str

fn Scheme::as_str(self : Scheme) -> String

Returns the scheme component as a string.

#
Scheme::new

fn Scheme::new(s : String) -> Scheme?

Converts a string to a Scheme, returning None if it is not a valid scheme name according to Section 3.1 of RFC 3986.

#
Scheme::new_or_panic

fn Scheme::new_or_panic(s : String) -> Scheme

Converts a string to a Scheme.

Panics

Panics if the string is not a valid scheme name. For a non-panicking variant, use [Scheme::new].

#
Uri

A URI.

Examples

test {
let uri = @uri.Uri::parse(
"foo://user@example.com:8042/over/there?name=ferret#nose",
)
assert_eq(uri.scheme, @uri.Scheme::new_or_panic("foo"))
let auth = uri.authority.unwrap()
assert_eq(auth.as_str(), "user@example.com:8042")
assert_eq(auth.userinfo.unwrap().as_str(), "user")
assert_eq(auth.host, "example.com")
assert_eq(auth.port.unwrap().as_str(), "8042")
assert_eq(uri.path.as_str(), "/over/there")
assert_eq(uri.query.unwrap().as_str(), "name=ferret")
assert_eq(uri.fragment.unwrap().as_str(), "nose")
}
impl Compare for Uri
impl Eq for Uri
impl Hash for Uri
impl Show for Uri
impl Debug for Uri

#
Uri::build

Builds a URI from its components.

Errors

Raises [BuildError] unless both of the following hold.

  • When an authority is present, the path is empty or starts with '/'.
  • When no authority is present, the path does not start with "//".

test {
let uri = @uri.Uri::build(
scheme=@uri.Scheme::new_or_panic("foo"),
authority=@uri.Authority::make(
host=@uri.Host::RegName(@uri.EStr::new_or_panic("example.com")),
userinfo=@uri.EStr::new_or_panic("user"),
port=@uri.EStr::new_or_panic("8042"),
),
path=@uri.EStr::new_or_panic("/over/there"),
query=@uri.EStr::new_or_panic("name=ferret"),
fragment=@uri.EStr::new_or_panic("nose"),
)
inspect(
uri,
content="foo://user@example.com:8042/over/there?name=ferret#nose",
)
}

#
Uri::normalize

fn Uri::normalize(self : Uri, default_port? : (String) -> Int?) -> Uri

Normalizes the URI.

The following normalizations are applied:

  • The scheme and the host are lowercased.
  • Percent-encoded octets that stand for unreserved characters are decoded, and the remaining ones are uppercased.
  • Dot segments are removed from an absolute path.
  • An empty port is removed, and so is a port equal to the scheme's default, as given by default_port. The scheme passed to default_port is already lowercased.
  • An IPv6 address is rewritten in its canonical form.

test {
let uri = @uri.Uri::parse("eXAMPLE://a/./b/../b/%63/%7bfoo%7d")
inspect(uri.normalize(), content="example://a/b/c/%7Bfoo%7D")
let uri = @uri.Uri::parse("http://example.com:80/")
inspect(
uri.normalize(default_port=scheme => {
if scheme is "http" {
Some(80)
} else {
None
}
}),
content="http://example.com/",
)
}

#
Uri::normalize_strict

fn Uri::normalize_strict(self : Uri, default_port? : (String) -> Int?) -> Uri raise NormalizeError

Normalizes the URI, rejecting an underflow in path normalization.

Errors

Raises [NormalizeError] if a ".." segment of an absolute path tries to escape the root, e.g. in "http://example.com/..".

#
Uri::parse

fn Uri::parse(s : String) -> Uri raise ParseError

Parses a URI from a string, matching the URI ABNF rule from RFC 3986.

test {
let uri = @uri.Uri::parse("http://example.com/")
inspect(uri, content="http://example.com/")
}

#
Uri::strip_fragment

fn Uri::strip_fragment(self : Uri) -> Uri

Returns the URI with its fragment removed.

test {
let uri = @uri.Uri::parse("http://example.com/#title")
inspect(uri.strip_fragment(), content="http://example.com/")
}

#
Uri::to_iri

fn Uri::to_iri(self : Uri) -> Iri

Converts the URI to an IRI with the same contents.

#
Uri::to_iri_ref

fn Uri::to_iri_ref(self : Uri) -> IriRef

Converts the URI to an IRI reference with the same contents.

#
Uri::to_uri_ref

fn Uri::to_uri_ref(self : Uri) -> UriRef

Converts the URI to a URI reference with the same contents.

#
Uri::with_fragment

fn Uri::with_fragment(self : Uri, fragment : EStr[
Fragment
]?) -> Uri

Returns the URI with its fragment replaced by fragment.

#
UriRef

A URI reference, i.e., either a URI or a relative reference.
impl Compare for UriRef
impl Eq for UriRef
impl Hash for UriRef
impl Show for UriRef

#
UriRef::as_uri

fn UriRef::as_uri(self : UriRef) -> Uri raise ConvertError

Converts the URI reference to a URI if it has a scheme.

Errors

Raises [ConvertError] if the URI reference has no scheme.

#
UriRef::build

Builds a URI reference from its components.

Errors

Raises [BuildError] unless all of the following hold.

  • When an authority is present, the path is empty or starts with '/'.
  • When no authority is present, the path does not start with "//".
  • When neither a scheme nor an authority is present, the first path segment does not contain ':'.

#
UriRef::normalize

fn UriRef::normalize(self : UriRef, default_port? : (String) -> Int?) -> UriRef

Normalizes the URI reference. See [Uri::normalize].

#
UriRef::normalize_strict

fn UriRef::normalize_strict(self : UriRef, default_port? : (String) -> Int?) -> UriRef raise NormalizeError

Normalizes the URI reference, rejecting an underflow in path normalization. See [Uri::normalize_strict].

#
UriRef::parse

fn UriRef::parse(s : String) -> UriRef raise ParseError

Parses a URI reference from a string, matching the URI-reference ABNF rule from RFC 3986.

#
UriRef::resolve_against

fn UriRef::resolve_against(self : UriRef, base : Uri, allow_path_underflow? : Bool) -> Uri raise ResolveError

Resolves the URI reference against the given base URI and returns the target URI.

The base URI must have no fragment. If it also has no authority and its path is rootless, the reference must either have a scheme, be empty, or start with '#'.

Setting allow_path_underflow to false deviates from RFC 3986 by rejecting references whose ".." segments escape the root of the base.

test {
let base = @uri.Uri::parse("http://example.com/foo/bar")
inspect(
@uri.UriRef::parse("baz").resolve_against(base),
content="http://example.com/foo/baz",
)
inspect(
@uri.UriRef::parse("../baz").resolve_against(base),
content="http://example.com/baz",
)
inspect(
@uri.UriRef::parse("?baz").resolve_against(base),
content="http://example.com/foo/bar?baz",
)
}

#
UriRef::strip_fragment

fn UriRef::strip_fragment(self : UriRef) -> UriRef

Returns the URI reference with its fragment removed.

#
UriRef::to_iri_ref

fn UriRef::to_iri_ref(self : UriRef) -> IriRef

Converts the URI reference to an IRI reference with the same contents.

#
UriRef::with_fragment

fn UriRef::with_fragment(self : UriRef, fragment : EStr[
Fragment
]?) -> UriRef

Returns the URI reference with its fragment replaced by fragment.

#
ipv4_localhost

let ipv4_localhost : Ipv4Addr

The address 127.0.0.1.

#
ipv4_unspecified

let ipv4_unspecified : Ipv4Addr

The address 0.0.0.0.

#
ipv6_localhost

let ipv6_localhost : Ipv6Addr

The address ::1.

#
ipv6_unspecified

let ipv6_unspecified : Ipv6Addr

The address ::.