// The seam between this library and whatever sends bytes.
//
// Taken from `slack/api`'s `Transport` unchanged, because the shape is right:
// one async method that sends, one sync method that says where it points. No
// retries, no rate limiting, no JSON. Those are decided above, where they can
// be tested without a socket.
///|
/// One outbound call, already reduced to bytes.
pub(all) struct HttpRequest {
url : String
/// Spelled `http_method` because `method` is a reserved word.
http_method : String
headers : Map[String, String]
body : Bytes
} derive(Eq, Debug)
///|
/// The UTF-8 byte count, which is what `Content-Length` must be -- and is not
/// the string length, because MoonBit strings are UTF-16.
pub fn HttpRequest::content_length(self : Self) -> Int {
self.body.length()
}
///|
/// The body as text, for logging and tests. Lossy by design: a blob upload is
/// not text and printing it should not fail.
pub fn HttpRequest::body_text(self : Self) -> String {
@utf8.decode_lossy(self.body[:])
}
///|
pub(all) struct HttpResponse {
status : Int
/// Header names MUST be lower-cased by the transport. Everything that reads
/// one -- `retry-after`, `ratelimit-reset`, `content-type` -- assumes it.
headers : Map[String, String]
body : Bytes
} derive(Eq, Debug)
///|
pub fn HttpResponse::header(self : Self, name : String) -> String? {
self.headers.get(name)
}
///|
/// The body as text. Lossy, because a malformed body must still be reportable
/// rather than turning into a second, less useful error.
pub fn HttpResponse::body_text(self : Self) -> String {
@utf8.decode_lossy(self.body[:])
}
///|
/// The media type, without parameters -- `application/json` from
/// `application/json; charset=utf-8`.
pub fn HttpResponse::content_type(self : Self) -> String? {
guard self.header("content-type") is Some(value) else { return None }
let media = match value.split_once(";") {
Some((head, _)) => head.to_owned()
None => value
}
Some(media.trim().to_owned().to_lower())
}
///|
/// A thing that can send an XRPC request and return what came back.
///
/// Deliberately dumb. An implementation has exactly three obligations, and all
/// three are things a hand-written one gets wrong:
///
/// - Lower-case the response header names.
/// - Do not forward `content-length`; most HTTP clients compute their own,
/// and sending both risks two conflicting headers on the wire.
/// - Do not let the underlying client's errors escape. Translate them, or a
/// caller's `catch` sees a type from a library it never imported.
pub(open) trait Transport {
async fn send(Self, HttpRequest) -> HttpResponse
/// Human-readable target, for error messages. A transport that cannot say
/// where it was pointed makes a connection failure much harder to place.
fn describe(Self) -> String
}