A generic URI/IRI handling library compliant with RFC 3986/3987, ported from the Rust crate fluent-uri.
moon add marianoguerra/uriimport {
"marianoguerra/uri",
// Only needed to name an encoder or a table, see Packages below.
"marianoguerra/uri/enc",
}///|
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")
}///|
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",
)
}///|
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")
}
}///|
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é!")
}///|
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")
}///|
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")
}
}///|
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",
)
}///|
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")
}
}///|
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/",
)
}///|
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")
}///|
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")
}
}moon test # unit, ported, property and conformance tests
moon coverage analyze # coverage reportcargo run --release --manifest-path tools/oracle/Cargo.toml -- --out conformance_test.mbtcargo 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.mbtimpl Show for BuildErrorimpl Show for ConvertErrorimpl Show for DecodeErrorimpl Show for InvalidPortimpl Show for NormalizeErrorimpl Show for ParseErrortest {
let auth = @uri.Uri::parse("http://user@example.com:8080/").authority.unwrap()
inspect(auth.as_str(), content="user@example.com:8080")
}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")
}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 struct EStr[E] {
inner : String
}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é!")
}test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::new_or_panic("%C2%A1Hola%21")
inspect(
s.decode_to_bytes(),
content=(
#|b"\xc2\xa1Hola!"
),
)
}test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::new_or_panic("%C2%A1Hola%21")
inspect(s.decode_to_string(), content="¡Hola!")
}test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::new_or_panic("%C2Hi%FF")
inspect(s.decode_to_string_lossy(), content="�Hi�")
}test {
let s : @uri.EStr[@enc.Path] = @uri.EStr::force_encode_byte(b'/')
inspect(s, content="%2F")
}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)
}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)
}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"])
}pub struct EString[E] {
buf : String
}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")
}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")
}fn IriRef::normalize_strict(self : IriRef, default_port? : (String) -> Int?) -> IriRef raise NormalizeErrorfn IriRef::resolve_against(self : IriRef, base : Iri, allow_path_underflow? : Bool) -> Iri raise ResolveErrortest {
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 struct Scheme {
inner : String
}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")
}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")
}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",
)
}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/",
)
}test {
let uri = @uri.Uri::parse("http://example.com/")
inspect(uri, content="http://example.com/")
}test {
let uri = @uri.Uri::parse("http://example.com/#title")
inspect(uri.strip_fragment(), content="http://example.com/")
}fn UriRef::normalize_strict(self : UriRef, default_port? : (String) -> Int?) -> UriRef raise NormalizeErrorfn UriRef::resolve_against(self : UriRef, base : Uri, allow_path_underflow? : Bool) -> Uri raise ResolveErrortest {
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",
)
}A generic URI/IRI handling library compliant with RFC 3986/3987, ported from the Rust crate fluent-uri.