A comprehensive RFC3986 compliant URI parsing and manipulation library for MoonBit, ported from the OCaml ocaml-uri library with enhanced features and extensive testing.
- RFC3986 Compliant: Full compliance with the URI specification
- Comprehensive Parsing: Parse all URI components (scheme, authority, path, query, fragment)
- Error Handling: Robust error handling with detailed error types
- URI Manipulation: Builder pattern for creating and modifying URIs
- Normalization: URI normalization including path normalization and default port removal
- Resolution: Resolve relative URIs against base URIs
- Query Parsing: Parse and build query strings
- URL Encoding: Basic URL encoding support
- IPv6 Support: Handle IPv6 addresses in URIs
- Extensive Testing: Comprehensive test suite with edge cases
Add this library to your MoonBit project by including it in your moon.mod.json:
{
"deps": {
"username/uri": "^0.1.0"
}
}test "quick_start_example" {
// Parse a URI
let uri = @uri.parse("https://example.com:8080/path?query=value#fragment")
inspect(uri.scheme(), content="Some(\"https\")")
inspect(uri.host(), content="Some(\"example.com\")")
inspect(uri.port(), content="Some(8080)")
inspect(uri.path(), content="/path")
inspect(uri.query(), content="Some(\"query=value\")")
inspect(uri.fragment(), content="Some(\"fragment\")")
// Build a URI using the builder methods
let built_uri = @uri.empty()
.with_scheme(Some("https"))
.with_host(Some("api.example.com"))
.with_path("/v1/users")
.with_query(Some("limit=10&offset=0"))
inspect(built_uri.to_string(), content="https://api.example.com/v1/users?limit=10&offset=0")
}The main URI data structure containing all URI components:
scheme: String?- The URI scheme (e.g., "http", "https")authority: Authority?- The authority componentpath: String- The path componentquery: String?- The query stringfragment: String?- The fragment identifier
The authority component of a URI:
userinfo: String?- User information (username:password)host: String- The host (domain name or IP address)port: Int?- The port number
Error types for URI operations:
InvalidScheme(String)- Invalid scheme formatInvalidAuthority(String)- Invalid authority formatInvalidPath(String)- Invalid path formatInvalidQuery(String)- Invalid query formatInvalidFragment(String)- Invalid fragment formatInvalidPort(String)- Invalid port numberEmptyUri- Empty URI string
Parse a URI string into a Uri structure.
test "of_string_example" {
let uri = @uri.parse("https://example.com/path")
inspect(uri.host(), content="Some(\"example.com\")")
}Convert a Uri structure back to a string representation.
test "to_string_example" {
let uri = @uri.parse("https://example.com/path")
let uri_string = uri.to_string()
inspect(uri_string, content="https://example.com/path")
}Get the scheme component.
Get the host component.
Get the port component.
Get the path component.
Get the query component.
Get the fragment component.
Create an empty URI with default values.
Create a new URI with the specified scheme.
Create a new URI with the specified host.
Create a new URI with the specified port.
Create a new URI with the specified path.
Create a new URI with the specified query.
Create a new URI with the specified fragment.
Check if the URI is absolute (has a scheme).
Check if the URI is relative (no scheme).
Get the effective port (explicit port or default port for scheme).
Normalize a URI by removing default ports and normalizing the path.
test "normalize_example" {
let uri = @uri.parse("https://example.com:443/path")
let normalized = uri.normalize()
// Default HTTPS port (443) should be removed
inspect(normalized.port(), content="None")
inspect(normalized.host(), content="Some(\"example.com\")")
}Resolve a relative URI against a base URI.
test "resolve_example" {
let base = @uri.parse("https://example.com/dir/")
let relative = @uri.parse("../other/file.html")
let resolved = @uri.resolve(base, relative)
inspect(resolved.to_string(), content="https://example.com/other/file.html")
}test "parse_http_uri" {
let uri = @uri.parse("https://user:pass@example.com:8080/path?query=value#section")
inspect(uri.scheme(), content="Some(\"https\")")
inspect(uri.host(), content="Some(\"example.com\")")
inspect(uri.port(), content="Some(8080)")
inspect(uri.path(), content="/path")
inspect(uri.query(), content="Some(\"query=value\")")
inspect(uri.fragment(), content="Some(\"section\")")
}test "build_api_uri" {
let api_uri = @uri.empty()
.with_scheme(Some("https"))
.with_host(Some("api.github.com"))
.with_path("/repos/owner/repo/issues")
.with_query(Some("state=open&per_page=50"))
inspect(api_uri.to_string(), content="https://api.github.com/repos/owner/repo/issues?state=open&per_page=50")
}test "resolve_relative_uri" {
let base = @uri.parse("https://example.com/docs/guide/")
let relative = @uri.parse("../api/reference.html")
let resolved = @uri.resolve(base, relative)
inspect(resolved.to_string(), content="https://example.com/docs/api/reference.html")
}test "query_parameters" {
let uri = @uri.parse("https://search.example.com/?q=moonbit&lang=en&safe=on")
match uri.query() {
Some(query_str) => {
inspect(query_str, content="q=moonbit&lang=en&safe=on")
// Use built-in query parameter methods
let q_param = uri.get_query_param("q")
inspect(q_param, content="Some(\"moonbit\")")
let lang_param = uri.get_query_param("lang")
inspect(lang_param, content="Some(\"en\")")
}
None => inspect("Should have query", content="\"Should have query\"")
}
}test "ipv6_uri" {
let uri = @uri.parse("http://[2001:db8::1]:8080/path")
inspect(uri.scheme(), content="Some(\"http\")")
inspect(uri.host(), content="Some(\"[2001:db8::1]\")")
inspect(uri.port(), content="Some(8080)")
inspect(uri.path(), content="/path")
}test "uri_normalization" {
let uri = @uri.parse("https://example.com:443/./path/../other/./file.html")
let normalized = uri.normalize()
// Default HTTPS port (443) should be removed
inspect(normalized.port(), content="None")
// Path should be normalized
inspect(normalized.path(), content="/other/file.html")
inspect(normalized.to_string(), content="https://example.com/other/file.html")
}The library recognizes default ports for common schemes:
http→ 80https→ 443ftp→ 21ssh→ 22telnet→ 23smtp→ 25dns→ 53pop3→ 110imap→ 143ldap→ 389imaps→ 993pop3s→ 995
The library uses MoonBit's Result type for error handling. All parsing operations return Result[Uri, UriError] where UriError provides detailed information about parsing failures:
test "error_handling_example" {
// Test with a valid but unusual URI
let uri = @uri.parse("custom://example.com")
inspect(uri.scheme(), content="Some(\"custom\")")
inspect(uri.host(), content="Some(\"example.com\")")
}This library implements the URI specification as defined in RFC3986. Key compliance features include:
- Proper parsing of all URI components
- Scheme validation (must start with letter, contain only alphanumeric, +, -, .)
- Authority parsing with IPv6 support
- Path normalization (resolving . and .. segments)
- Query and fragment handling
- Percent-encoding support
- Relative URI resolution
The library includes comprehensive tests covering:
- Basic URI parsing and serialization
- All URI components (scheme, authority, path, query, fragment)
- Edge cases (empty components, special characters)
- IPv6 addresses
- URI normalization
- Relative URI resolution
- Query parameter parsing
- Error conditions
- RFC3986 compliance
Run tests with:
moon testContributions are welcome! Please ensure that:
- All tests pass
- New features include comprehensive tests
- Code follows MoonBit style guidelines
- Documentation is updated for new features
This project is licensed under the Apache-2.0 License - see the LICENSE file for details.
This library is ported from the excellent OCaml URI library by the Mirage team, with enhancements for MoonBit's type system and additional utility functions.