///|
#derive(Eq)
priv enum UriPosition {
Path
QueryKey
QueryValue
Fragment
}
///|
/// An interpolation builder for URI strings. Literal syntax is preserved;
/// interpolated values are percent-encoded as individual components. Query
/// parameters are staged until their separator so None can omit the whole
/// parameter without retracting or repeatedly copying the preceding URL.
pub struct UriBuilder {
priv out : StringBuilder
priv mut param : StringBuilder
priv mut position : UriPosition
priv mut query_items : Bool
priv mut omit : Bool
}
///|
/// Creates an empty URI builder; use uri for interpolation and finalization.
pub fn UriBuilder::new() -> UriBuilder {
{
out: StringBuilder(),
param: StringBuilder(),
position: Path,
query_items: false,
omit: false,
}
}
///|
fn UriBuilder::commit_param(self : UriBuilder) -> Unit {
if !self.omit && !self.param.is_empty() {
self.out.write_char(if self.query_items { '&' } else { '?' })
self.out.write_string(self.param.to_string())
self.query_items = true
}
self.param = StringBuilder()
self.omit = false
}
///|
/// Writes URI syntax literally. Reserved characters in interpolated data
/// should instead go through interpolation or write_encoded.
pub fn UriBuilder::write_string(self : UriBuilder, text : String) -> Unit {
for c in text {
match (self.position, c) {
(Path, '?') => self.position = QueryKey
(QueryKey | QueryValue, '&') => {
self.commit_param()
self.position = QueryKey
}
(QueryKey | QueryValue, '#') => {
self.commit_param()
self.out.write_char(c)
self.position = Fragment
}
(Path, '#') => {
self.out.write_char(c)
self.position = Fragment
}
(QueryKey, '=') => {
if !self.omit {
self.param.write_char(c)
}
self.position = QueryValue
}
(QueryKey | QueryValue, _) => if !self.omit { self.param.write_char(c) }
_ => self.out.write_char(c)
}
}
}
///|
/// Writes a component, escaping every character outside RFC 3986 unreserved
/// syntax. Even at the start, a plain String cannot inject a scheme or host.
pub fn UriBuilder::write_encoded(self : UriBuilder, value : String) -> Unit {
let encoded = percent_encode(value)
match self.position {
QueryKey | QueryValue => if !self.omit { self.param.write_string(encoded) }
Path | Fragment => self.out.write_string(encoded)
}
}
///|
/// Writes an interpolated value using its UriPart implementation.
pub fn[T : UriPart] UriBuilder::write_string_interpolation(
self : UriBuilder,
value : T,
) -> Unit {
value.write_uri(self)
}
///|
/// Returns the current URI without mutating the builder. Empty query syntax
/// and parameters omitted by None do not leave stray separators.
pub fn UriBuilder::to_string(self : UriBuilder) -> String {
if (self.position == QueryKey || self.position == QueryValue) &&
!self.omit &&
!self.param.is_empty() {
self.out.to_string() +
(if self.query_items { "&" } else { "?" }) +
self.param.to_string()
} else {
self.out.to_string()
}
}
///|
/// Types which can contribute URI data. Custom implementations should call
/// write_encoded, or delegate to another UriPart, for untrusted values.
pub(open) trait UriPart {
fn write_uri(Self, UriBuilder) -> Unit
}
///|
/// Interpolates one encoded string component.
pub impl UriPart for String with fn write_uri(self, builder) {
builder.write_encoded(self)
}
///|
/// Interpolates one decimal integer component.
pub impl UriPart for Int32 with fn write_uri(self, builder) {
builder.write_encoded(self.to_string())
}
///|
/// Interpolates one decimal 64-bit integer component.
pub impl UriPart for Int64 with fn write_uri(self, builder) {
builder.write_encoded(self.to_string())
}
///|
/// Interpolates true or false.
pub impl UriPart for Bool with fn write_uri(self, builder) {
builder.write_encoded(self.to_string())
}
///|
/// Interpolates the ordinary Double representation as one encoded component.
pub impl UriPart for Double with fn write_uri(self, builder) {
builder.write_encoded(self.to_string())
}
///|
/// Some delegates; None omits the current query value's entire parameter,
/// or contributes no data outside a query value.
pub impl[T : UriPart] UriPart for T? with fn write_uri(self, builder) {
match self {
Some(value) => value.write_uri(builder)
None =>
if builder.position == QueryValue {
builder.omit = true
builder.param = StringBuilder()
}
}
}
///|
/// At an empty query-key position, writes ordered key/value pairs (including
/// duplicates). Elsewhere the form representation is encoded as one component.
pub impl UriPart for Array[(String, String)] with fn write_uri(self, builder) {
if builder.position != QueryKey || !builder.param.is_empty() {
builder.write_encoded(form_urlencode(self))
return
}
for i, (key, value) in self {
if i > 0 {
builder.write_string("&")
}
builder.write_encoded(key)
builder.write_string("=")
builder.write_encoded(value)
}
}
///|
/// Explicit, already-encoded URI syntax. Unlike String interpolation, this
/// may introduce separators, a scheme or an authority; only use trusted data.
pub(all) struct Trusted(String)
///|
/// Writes the trusted text while tracking any URI separators it introduces.
pub impl UriPart for Trusted with fn write_uri(self, builder) {
builder.write_string(self.0)
}
///|
/// Writes this explicitly trusted fragment to a URI builder.
pub extend Trusted with UriPart::{write_uri}
///|
/// Builds a URI string without parsing it. Send-time validation still applies.
///
/// ```mbt check
/// test {
/// let id = "a/b"
/// let q : String? = None
/// assert_eq(
/// @http.uri(b => b <+ "https://example.test/\{id}?q=\{q}&n=\{3}"),
/// "https://example.test/a%2Fb?n=3",
/// )
/// }
/// ```
pub fn uri(build : (UriBuilder) -> Unit) -> String {
let builder = UriBuilder::new()
build(builder)
builder.to_string()
}