///|
/// The Exa API client.
///
/// Construct one with a `Transport` and an API key, then call `search`,
/// `contents` or `answer` on it. A `Client` holds no mutable state, so a single
/// one can be shared across concurrent tasks (whether that is safe in practice
/// is up to the transport).
pub struct Client {
transport : &Transport
api_key : String
base_url : String
}
///|
/// The default Exa API endpoint.
pub let default_base_url : String = "https://api.exa.ai"
///|
/// Build a client. `base_url` only needs setting to point at a proxy or a test
/// server; it may carry a path prefix and a trailing slash is ignored.
pub fn Client::new(
transport : &Transport,
api_key : String,
base_url? : String = default_base_url,
) -> Client {
let trimmed = if base_url.has_suffix("/") {
base_url[:base_url.length() - 1].to_owned()
} else {
base_url
}
{ transport, api_key, base_url: trimmed, }
}
///|
/// Sent as `user-agent` on every request.
let user_agent : String = "marianoguerra-exa-moonbit/0.1.0"
///|
/// POST a JSON body to `path` and decode the JSON response.
async fn Client::post_json(self : Client, path : String, body : Json) -> Json {
let request = {
meth: "POST",
url: self.base_url + path,
headers: [
("x-api-key", self.api_key),
("content-type", "application/json"),
("accept", "application/json"),
("user-agent", user_agent),
],
body: body.stringify(),
}
let response = self.transport.send(request)
if response.status < 200 || response.status >= 300 {
raise api_error(response)
}
parse_body(response)
}
///|
fn parse_body(response : HttpResponse) -> Json raise ExaError {
@json.parse(response.body) catch {
_ =>
raise Decode(
"HTTP \{response.status}: response body is not valid JSON: \{truncate(response.body)}",
)
}
}
///|
/// Turn a non-2xx response into an `ExaError::Api`.
///
/// Exa answers errors with `{ requestId, error, tag }`, but a proxy or gateway
/// in front of it may not, so fall back to the raw body.
fn api_error(response : HttpResponse) -> ExaError {
let parsed = @json.parse(response.body) catch { _ => Json::null() }
let tag = get_str(parsed, "tag").unwrap_or("UNKNOWN")
let message = match get_str(parsed, "error") {
Some(m) => m
None => truncate(response.body)
}
let request_id = match get_str(parsed, "requestId") {
Some(id) => Some(id)
None => response.header("x-request-id")
}
Api(status=response.status, tag~, message~, request_id~)
}
///|
/// Keep error messages readable when the body is a whole HTML error page.
fn truncate(text : String) -> String {
if text.length() <= 512 {
text
} else {
text[:512].to_owned() + "..."
}
}