///|
/// Conversions between URIs, URI references, IRIs and IRI references.
///|
/// An error raised when converting a URI/IRI (reference) to a narrower type.
pub(all) suberror ConvertError {
/// The input is not ASCII. The index points to the first non-ASCII code
/// unit.
NotAscii(index~ : Int)
/// The input has no scheme.
NoScheme
} derive(Eq, Debug)
///|
pub impl Show for ConvertError with fn output(self, logger) {
match self {
NotAscii(index~) =>
logger.write_string("non-ASCII character at index \{index}")
NoScheme => logger.write_string("scheme not present")
}
}
///|
/// Returns the index of the first non-ASCII code unit of `s`, if any.
fn first_non_ascii(s : String) -> Int? {
for i in 0..= 128 {
return Some(i)
}
}
None
}
///|
fn ensure_ascii(s : String) -> Unit raise ConvertError {
if first_non_ascii(s) is Some(index) {
raise NotAscii(index~)
}
}
// Widening conversions, which never fail.
///|
/// Converts the URI to a URI reference with the same contents.
pub fn Uri::to_uri_ref(self : Uri) -> UriRef {
uri_ref_of_ri(self.to_ri())
}
///|
/// Converts the URI to an IRI with the same contents.
pub fn Uri::to_iri(self : Uri) -> Iri {
iri_of_ri(self.to_ri())
}
///|
/// Converts the URI to an IRI reference with the same contents.
pub fn Uri::to_iri_ref(self : Uri) -> IriRef {
iri_ref_of_ri(self.to_ri())
}
///|
/// Converts the URI reference to an IRI reference with the same contents.
pub fn UriRef::to_iri_ref(self : UriRef) -> IriRef {
iri_ref_of_ri(self.to_ri())
}
///|
/// Converts the IRI to an IRI reference with the same contents.
pub fn Iri::to_iri_ref(self : Iri) -> IriRef {
iri_ref_of_ri(self.to_ri())
}
// Narrowing conversions, which keep the contents but may fail.
///|
/// Converts the URI reference to a URI if it has a scheme.
///
/// # Errors
///
/// Raises [`ConvertError`] if the URI reference has no scheme.
pub fn UriRef::as_uri(self : UriRef) -> Uri raise ConvertError {
guard self.scheme is Some(_) else { raise NoScheme }
uri_of_ri(self.to_ri())
}
///|
/// 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.
pub fn Iri::as_uri(self : Iri) -> Uri raise ConvertError {
ensure_ascii(self.text)
uri_of_ri(self.to_ri())
}
///|
/// 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.
pub fn IriRef::as_uri(self : IriRef) -> Uri raise ConvertError {
guard self.scheme is Some(_) else { raise NoScheme }
ensure_ascii(self.text)
uri_of_ri(self.to_ri())
}
///|
/// 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.
pub fn IriRef::as_uri_ref(self : IriRef) -> UriRef raise ConvertError {
ensure_ascii(self.text)
uri_ref_of_ri(self.to_ri())
}
///|
/// Converts the IRI reference to an IRI if it has a scheme.
///
/// # Errors
///
/// Raises [`ConvertError`] if the IRI reference has no scheme.
pub fn IriRef::as_iri(self : IriRef) -> Iri raise ConvertError {
guard self.scheme is Some(_) else { raise NoScheme }
iri_of_ri(self.to_ri())
}
// Lossless conversions that percent-encode non-ASCII characters.
///|
/// Percent-encodes the non-ASCII characters of `s`.
fn encode_non_ascii_str(s : String) -> String {
guard first_non_ascii(s) is Some(_) else { return s }
let sb = StringBuilder::new(size_hint=s.length() * 2)
let scratch : Array[Byte] = []
let len = s.length()
let mut i = 0
while i < len {
let (cp, w) = next_code_point(s, i)
if cp < 128 {
sb.write_substring(s, i, w)
} else {
scratch.clear()
utf8_encode_code_point(scratch, cp)
for x in scratch {
push_encoded_byte(sb, x.to_int())
}
}
i += w
}
sb.to_string()
}
///|
/// Percent-encodes the non-ASCII characters of every component.
fn encode_non_ascii(r : Ri) -> Ri {
let authority = match r.authority {
None => None
Some(auth) => {
let userinfo = opt_map(auth.userinfo, v => {
estr(encode_non_ascii_str(v.as_str()))
})
let (host, host_parsed) : (String, Host[@enc.IRegName]) = match
auth.host_parsed {
RegName(_) => {
let host = encode_non_ascii_str(auth.host)
(host, RegName(estr(host)))
}
other => (auth.host, other)
}
Some({ userinfo, host, host_parsed, port: auth.port })
}
}
ri(
scheme=r.scheme,
authority~,
path=encode_non_ascii_str(r.path),
query=opt_map(r.query, encode_non_ascii_str),
fragment=opt_map(r.fragment, encode_non_ascii_str),
)
}
///|
/// Converts the IRI to a URI by percent-encoding non-ASCII characters.
///
/// Punycode encoding is **not** performed during conversion.
///
/// ```mbt check
/// 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")
/// }
/// ```
pub fn Iri::to_uri(self : Iri) -> Uri {
uri_of_ri(encode_non_ascii(self.to_ri()))
}
///|
/// Converts the IRI reference to a URI reference by percent-encoding
/// non-ASCII characters.
///
/// Punycode encoding is **not** performed during conversion.
///
/// ```mbt check
/// 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")
/// }
/// ```
pub fn IriRef::to_uri_ref(self : IriRef) -> UriRef {
uri_ref_of_ri(encode_non_ascii(self.to_ri()))
}