///|
/// Minimal URL helpers used when building request paths with query strings.
///
/// These are intentionally small and dependency-free; they cover the cases
/// the SDK needs (joining a base URL with a path, appending query parameters,
/// and percent-encoding parameter values).
///|
/// Join a base URL and a path, collapsing a duplicated `/` at the boundary and
/// inserting one if neither side provides it.
pub fn join_url(base : String, path : String) -> String {
let base_slash = base.has_suffix("/")
let path_slash = path.has_prefix("/")
if base_slash && path_slash {
base + path[1:].to_owned()
} else if base_slash || path_slash {
base + path
} else {
base + "/" + path
}
}
///|
/// Percent-encode a string for use in a URL query value. Unreserved
/// characters (`A-Z a-z 0-9 - _ . ~`) pass through; everything else is encoded
/// as `%XX` using UTF-8 bytes.
pub fn percent_encode(s : String) -> String {
let out = StringBuilder::new()
for byte in @utf8.encode(s) {
let b = byte.to_int()
if is_unreserved(b) {
out.write_char(Int::unsafe_to_char(b))
} else {
out.write_char('%')
out.write_char(hex_digit(b / 16))
out.write_char(hex_digit(b % 16))
}
}
out.to_string()
}
///|
/// Whether a byte is an RFC 3986 unreserved character.
fn is_unreserved(b : Int) -> Bool {
(b >= 0x41 && b <= 0x5A) || // A-Z
(b >= 0x61 && b <= 0x7A) || // a-z
(b >= 0x30 && b <= 0x39) || // 0-9
b == 0x2D ||
b == 0x5F ||
b == 0x2E ||
b == 0x7E // - _ . ~
}
///|
/// The uppercase hex digit for a value 0..15.
fn hex_digit(n : Int) -> Char {
if n < 10 {
Int::unsafe_to_char(0x30 + n)
} else {
Int::unsafe_to_char(0x41 + (n - 10))
}
}
///|
/// Build a query string (without the leading `?`) from key/value pairs,
/// percent-encoding both keys and values. Returns an empty string when there
/// are no parameters.
pub fn build_query(params : Array[(String, String)]) -> String {
if params.length() == 0 {
return ""
}
let out = StringBuilder::new()
let mut first = true
for pair in params {
let (k, v) = pair
if !first {
out.write_char('&')
}
first = false
out.write_string(percent_encode(k))
out.write_char('=')
out.write_string(percent_encode(v))
}
out.to_string()
}
///|
/// Append a query string to a path, choosing `?` or `&` appropriately.
pub fn with_query(path : String, params : Array[(String, String)]) -> String {
let query = build_query(params)
if query == "" {
path
} else if path.contains("?") {
path + "&" + query
} else {
path + "?" + query
}
}