"""
# URI Parsing and Resolution (RFC 3986)
This package parses URI-references into structured components and resolves
relative references against base URIs per RFC 3986.
## Entry Points
Use `ParseURI` for standard URI-references (absolute URIs and relative
references — covers origin-form, absolute-form, and asterisk-form HTTP
request-targets):
```pony
match ParseURI("/index.html?page=1")
| let u: URI val => // u.path, u.query, etc.
| let e: URIParseError val => // handle error
end
```
Use `ResolveURI` to resolve a relative reference against a base URI
(RFC 3986 section 5):
```pony
match (ParseURI("http://example.com/a/b"), ParseURI("../c"))
| (let base: URI val, let ref': URI val) =>
match ResolveURI(base, ref')
| let target: URI val => // target.string() == "http://example.com/a/c"
| let e: ResolveURIError val => // base was not absolute
end
end
```
Use `ParseURIAuthority` for HTTP CONNECT authority-form targets
(`host:port` without scheme or `//`):
```pony
match ParseURIAuthority("example.com:443")
| let a: URIAuthority val => // a.host, a.port
| let e: URIParseError val => // handle error
end
```
Use `NormalizeURI` to apply RFC 3986 section 6 normalization (case,
percent-encoding, dot segments, default port removal, empty path):
```pony
match ParseURI("HTTP://Example.COM:80/%7Euser/a/../b")
| let u: URI val =>
match NormalizeURI(u)
| let n: URI val => // n.string() == "http://example.com/~user/b"
| let e: InvalidPercentEncoding val => // malformed percent-encoding
end
end
```
Use `URIEquivalent` to test whether two URIs are equivalent under
normalization:
```pony
match (ParseURI("HTTP://Example.COM:80/path"),
ParseURI("http://example.com/path"))
| (let a: URI val, let b: URI val) =>
match URIEquivalent(a, b)
| let eq: Bool => // eq == true
end
end
```
Use `URIBuilder` to construct a URI from raw (unencoded) components with
automatic percent-encoding, or to modify an existing URI:
```pony
match URIBuilder
.set_scheme("https")
.set_host("example.com")
.set_path("/api/users")
.add_query_param("name", "Jane Doe")
.build()
| let u: URI val =>
// u.string() == "https://example.com/api/users?name=Jane%20Doe"
| let e: URIBuildError val =>
// handle error
end
```
To modify an existing URI, start from `URIBuilder.from(uri)` and change
only the components you need:
```pony
match ParseURI("https://example.com/old?x=1")
| let u: URI val =>
match URIBuilder.from(u)
.set_path("/new")
.add_query_param("y", "2")
.build()
| let modified: URI val => // "https://example.com/new?x=1&y=2"
end
end
```
For query parameter access, `URI.query_params()` is the simplest path —
it returns a `FormURLEncoded` collection with `get()`, `get_all()`, and
`contains()` methods for key-based lookup. Use `ParseFormURLEncoded`
directly on the `query` field when you need to distinguish "no query"
from "invalid percent-encoding." `ParseFormURLEncoded` also works on
HTTP POST request bodies — it parses any `application/x-www-form-urlencoded`
string. Use `PercentDecode`/`PercentEncode` for encoding operations,
`PathSegments` for decoded path segment access, and `RemoveDotSegments`
for standalone path normalization.
For URI template expansion (RFC 6570), use the `uri/template` submodule.
## IRI Support (RFC 3987)
`ParseURI`, `ResolveURI`, `PercentDecode`, and `PercentEncode` handle IRIs
natively — `ParseURI` only looks for ASCII structural delimiters, so
non-ASCII bytes pass through correctly, and `URI` stores components as
`String` (UTF-8).
The IRI-specific primitives handle conversion between IRI and URI forms:
Use `IRIToURI` to convert an IRI to a URI by percent-encoding all non-ASCII
bytes. Use `URIToIRI` for the reverse — selectively decoding percent-encoded
UTF-8 sequences that are valid `ucschar` (or `iprivate` in query).
Use `IRIPercentEncode` instead of `PercentEncode` when constructing IRIs
from unencoded text — it preserves `ucschar` codepoints as literal UTF-8
while applying the same ASCII encoding rules.
Use `NormalizeIRI` for IRI-aware normalization (applies `NormalizeURI` then
decodes `ucschar` sequences back to literal UTF-8). Use `IRIEquivalent`
to test equivalence across IRI and URI forms.
"""
class val URI is (Stringable & Equatable[URI])
"""
A parsed RFC 3986 URI-reference.
Components are stored in their percent-encoded form as they appeared in
the input. Delimiter characters (`:` after scheme, `//` before authority,
`?` before query, `#` before fragment) are stripped — use `string()` to
reconstruct the full URI with delimiters.
`string()` reconstruction follows RFC 3986 section 5.3. Components that
are `None` are omitted entirely (no delimiter). A component that is an
empty `String` (present but empty) includes its delimiter — e.g.,
`query` of `""` produces a trailing `?` with no value. This distinction
allows faithful reconstruction of inputs like `http://example.com/path?`
(empty query present, `query = ""`) vs. `http://example.com/path`
(no query at all, `query = None`).
Equality is structural: two URIs are equal when all their stored
(percent-encoded) components are equal. No normalization is applied
before comparison. Use `URIEquivalent` for normalization-aware
comparison (RFC 3986 section 6).
"""
let scheme: (String | None)
let authority: (URIAuthority | None)
let path: String
let query: (String | None)
let fragment: (String | None)
new val create(
scheme': (String | None),
authority': (URIAuthority | None),
path': String,
query': (String | None),
fragment': (String | None))
=>
"""
Build a URI from pre-encoded components.
All string values must already be percent-encoded as appropriate for
their component — no encoding is applied here. Most callers will obtain
a `URI` from `ParseURI` rather than constructing one directly.
"""
scheme = scheme'
authority = authority'
path = path'
query = query'
fragment = fragment'
fun string(): String iso^ =>
"""
Reconstruct the full URI string per RFC 3986 section 5.3.
"""
let out = recover iso String end
match scheme
| let s: String => out.append(s); out.push(':')
end
match authority
| let a: URIAuthority =>
out.append("//")
out.append(a.string())
end
out.append(path)
match query
| let q: String => out.push('?'); out.append(q)
end
match fragment
| let f: String => out.push('#'); out.append(f)
end
out
fun query_params(): (FormURLEncoded val | None) =>
"""
Parse the query string into a `FormURLEncoded` collection.
Returns the parsed parameters if the query is present and decodes
successfully, or `None` if no query is present or if the query
contains invalid percent-encoding. For fine-grained error handling
(distinguishing "no query" from "decode failure"), use
`ParseFormURLEncoded` directly on the `query` field.
"""
match \exhaustive\ query
| let q: String val =>
match \exhaustive\ ParseFormURLEncoded(q)
| let params: FormURLEncoded val => params
| let _: InvalidPercentEncoding => None
end
| None => None
end
fun eq(that: URI box): Bool =>
"""
Structural equality on percent-encoded components without normalization.
Use `URIEquivalent` for normalization-aware comparison.
"""
let scheme_eq =
match (scheme, that.scheme)
| (None, None) => true
| (let a: String, let b: String) => a == b
else
false
end
let authority_eq =
match (authority, that.authority)
| (None, None) => true
| (let a: URIAuthority, let b: URIAuthority) => a == b
else
false
end
let query_eq =
match (query, that.query)
| (None, None) => true
| (let a: String, let b: String) => a == b
else
false
end
let fragment_eq =
match (fragment, that.fragment)
| (None, None) => true
| (let a: String, let b: String) => a == b
else
false
end
scheme_eq and authority_eq and (path == that.path) and
query_eq and fragment_eq