///|
/// URI/IRI components.
///|
/// A [scheme] component.
///
/// [scheme]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
///
/// # Comparison
///
/// `Scheme`s are compared case-insensitively. Compare
/// [`Scheme::as_str`] values instead for a case-sensitive comparison.
///
/// ```mbt check
/// 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")
/// }
/// ```
pub struct Scheme {
inner : String
}
///|
/// 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].
///
/// [scheme]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
pub fn Scheme::new(s : String) -> Scheme? {
if s.is_empty() {
return None
}
let first = code_at(s, 0)
if !@enc.table_alpha.allows_ascii(first) {
return None
}
if !table_validate(@enc.table_scheme, slice(s, 1, s.length())) {
return None
}
Some({ inner: s })
}
///|
/// 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`].
pub fn Scheme::new_or_panic(s : String) -> Scheme {
match Scheme::new(s) {
Some(v) => v
None => abort("invalid scheme: \{s}")
}
}
///|
/// Wraps a string that is known to be a valid scheme name.
fn scheme_validated(s : String) -> Scheme {
{ inner: s }
}
///|
/// Returns the scheme component as a string.
pub fn Scheme::as_str(self : Scheme) -> String {
self.inner
}
///|
/// Compares two schemes case-insensitively.
pub impl Eq for Scheme with fn equal(self, other) {
let (a, b) = (self.inner, other.inner)
if a.length() != b.length() {
return false
}
// The only characters allowed in a scheme are letters, digits, '+', '-'
// and '.'. Their ASCII codes let us simply set the sixth bit and compare.
for i in 0.. logger.write_string("Ipv4(\{addr})")
Ipv6(addr) => logger.write_string("Ipv6(\{addr})")
IpvFuture => logger.write_string("IpvFuture")
RegName(name) => logger.write_string("RegName(\{name})")
}
}
///|
/// Reinterprets the host with another registered name encoder.
fn[A, B] Host::cast(self : Host[A]) -> Host[B] {
match self {
Ipv4(addr) => Ipv4(addr)
Ipv6(addr) => Ipv6(addr)
IpvFuture => IpvFuture
RegName(name) => RegName(name.cast())
}
}
///|
/// An [authority] component.
///
/// [authority]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2
///
/// 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`.
pub struct Authority[U, R] {
/// The optional [userinfo] subcomponent.
///
/// [userinfo]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1
userinfo : EStr[U]?
/// The [host] subcomponent, verbatim.
///
/// [host]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2
host : String
/// The parsed host subcomponent.
host_parsed : Host[R]
/// The optional [port] subcomponent.
///
/// [port]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
port : EStr[@enc.Port]?
} derive(Eq, Debug)
///|
/// The internal, IRI-permissive form of an authority.
type IAuth = Authority[@enc.IUserinfo, @enc.IRegName]
///|
/// An empty authority component.
pub fn[U, R] Authority::empty() -> Authority[U, R] {
{ userinfo: None, host: "", host_parsed: RegName(EStr::empty()), port: None }
}
///|
/// Reinterprets the authority with other userinfo and registered name
/// encoders.
fn[U, R, U2, R2] Authority::cast(self : Authority[U, R]) -> Authority[U2, R2] {
{
userinfo: match self.userinfo {
Some(v) => Some(v.cast())
None => None
},
host: self.host,
host_parsed: self.host_parsed.cast(),
port: self.port,
}
}
///|
/// Returns the authority component as a string.
///
/// ```mbt check
/// test {
/// let auth = @uri.Uri::parse("http://user@example.com:8080/").authority.unwrap()
/// inspect(auth.as_str(), content="user@example.com:8080")
/// }
/// ```
pub fn[U, R] Authority::as_str(self : Authority[U, R]) -> String {
let sb = StringBuilder::new(size_hint=self.host.length() + 16)
if self.userinfo is Some(v) {
sb.write_string(v.as_str())
sb.write_char('@')
}
sb.write_string(self.host)
if self.port is Some(v) {
sb.write_char(':')
sb.write_string(v.as_str())
}
sb.to_string()
}
///|
pub impl[U, R] Show for Authority[U, R] with fn output(self, logger) {
logger.write_string(self.as_str())
}
///|
/// An error raised when a port cannot be converted to an integer.
pub(all) suberror InvalidPort {
/// The port that failed to convert.
InvalidPort(port~ : String)
} derive(Eq, Debug)
///|
pub impl Show for InvalidPort with fn output(self, logger) {
let InvalidPort(port~) = self
logger.write_string("invalid port value: \{port}")
}
///|
/// 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.
///
/// [port]: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3
///
/// # Errors
///
/// Raises [`InvalidPort`] if the port does not fit in a `u16`.
///
/// ```mbt check
/// 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)
/// }
/// ```
pub fn[U, R] Authority::port_to_u16(
self : Authority[U, R],
) -> Int? raise InvalidPort {
guard self.port is Some(port) else { return None }
let s = port.as_str()
if s.is_empty() {
return None
}
let mut value = 0
for i in 0.. 65535 {
raise InvalidPort(port=s)
}
}
Some(value)
}
///|
/// Checks whether a userinfo subcomponent is present.
pub fn[U, R] Authority::has_userinfo(self : Authority[U, R]) -> Bool {
self.userinfo is Some(_)
}
///|
/// Checks whether a port subcomponent is present.
pub fn[U, R] Authority::has_port(self : Authority[U, R]) -> Bool {
self.port is Some(_)
}