uri

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

    uri
    iri
    url
    parser
    rfc3986
    Download zip
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    14 days ago
    Downloads
    176

    #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.

    The generated corpus ships with the package, so moon test replays it anywhere. The oracle itself lives only in this repository, since regenerating needs cargo and a checkout of fluent-uri.

    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.

    Ipv4Addr::to_string

    fn Ipv4Addr::to_string(self : Ipv4Addr) -> String

    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.

    Ipv6Addr::to_string

    fn Ipv6Addr::to_string(self : Ipv6Addr) -> String

    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 ::.