use std::future::Future;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::utils::address::Address;
use crate::utils::json::Json;
pub const INFO: &str = "/info";
pub const EXCHANGE: &str = "/exchange";
pub trait Transport {
fn post(&self, path: &str, body: String) -> impl Future<Output = Result<String, TransportError>>;
}
pub trait Http {
fn send(&self, request: Request) -> impl Future<Output = Result<Response, TransportError>>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
Get,
Post,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub method: Method,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl Request {
#[must_use]
pub fn get(url: impl Into<String>) -> Self {
Request { method: Method::Get, url: url.into(), headers: Vec::new(), body: Vec::new() }
}
#[must_use]
pub fn post_json(url: impl Into<String>, body: String) -> Self {
Request {
method: Method::Post,
url: url.into(),
headers: vec![("Content-Type".to_owned(), "application/json".to_owned())],
body: body.into_bytes(),
}
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
find_header(&self.headers, name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl Response {
#[must_use]
pub const fn is_success(&self) -> bool {
self.status >= 200 && self.status < 300
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
find_header(&self.headers, name)
}
#[must_use]
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
}
fn find_header<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
headers.iter().find(|(key, _)| key.eq_ignore_ascii_case(name)).map(|(_, value)| value.as_str())
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TransportError {
#[error("no answer from the venue: {0}")]
Unreachable(String),
#[error("the venue answered HTTP {status}: {body}")]
Status { status: u16, body: String },
#[error("rate limited; try again in {} ms", retry_after_ms.unwrap_or(0))]
RateLimited { retry_after_ms: Option<u64> },
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ApiError {
#[error(transparent)]
Transport(#[from] TransportError),
#[error("the venue's answer on {path} could not be read: {detail}")]
Unreadable { path: &'static str, detail: String },
#[error("{0}")]
Refused(String),
}
#[must_use]
pub fn info_request(kind: &str, fields: &[(&str, Json)]) -> String {
let mut object = Vec::with_capacity(fields.len() + 1);
object.push(("type".to_owned(), Json::String(kind.to_owned())));
object.extend(fields.iter().map(|(key, value)| ((*key).to_owned(), value.clone())));
Json::Object(object).compact()
}
#[must_use]
pub fn user_request(kind: &str, user: Address) -> String {
info_request(kind, &[("user", Json::String(user.to_hex()))])
}
pub async fn info<R: DeserializeOwned>(transport: &impl Transport, request: String) -> Result<R, ApiError> {
let text = transport.post(INFO, request).await?;
serde_json::from_str(&text).map_err(|error| ApiError::Unreadable { path: INFO, detail: unreadable(&error, &text) })
}
pub async fn exchange(transport: &impl Transport, body: String) -> Result<Accepted, ApiError> {
let text = transport.post(EXCHANGE, body).await?;
match ExchangeResponse::parse(&text)? {
ExchangeResponse::Ok(accepted) => Ok(accepted),
ExchangeResponse::Err(reason) => Err(ApiError::Refused(reason)),
}
}
fn unreadable(error: &serde_json::Error, text: &str) -> String {
let shown: String = text.chars().take(300).collect();
format!("{error} in {shown}")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExchangeResponse {
Ok(Accepted),
Err(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Accepted {
pub kind: String,
pub statuses: Vec<Status>,
pub data: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Status {
Ok(Value),
Error(String),
}
impl Accepted {
pub fn errors(&self) -> impl Iterator<Item = &str> {
self.statuses.iter().filter_map(|status| match status {
Status::Error(reason) => Some(reason.as_str()),
Status::Ok(_) => None,
})
}
}
impl ExchangeResponse {
pub fn parse(text: &str) -> Result<ExchangeResponse, ApiError> {
let bad = |detail: String| ApiError::Unreadable { path: EXCHANGE, detail };
let answer: Value = serde_json::from_str(text).map_err(|error| bad(unreadable(&error, text)))?;
let response = answer.get("response").cloned().unwrap_or(Value::Null);
match answer.get("status").and_then(Value::as_str) {
Some("ok") => {
let kind = response.get("type").and_then(Value::as_str).unwrap_or_default().to_owned();
let data = response.get("data");
let statuses = match (
data.and_then(|data| data.get("statuses")).and_then(Value::as_array),
data.and_then(|data| data.get("status")),
) {
(Some(many), _) => many.iter().map(status_of).collect(),
(None, Some(one)) => vec![status_of(one)],
(None, None) => Vec::new(),
};
Ok(ExchangeResponse::Ok(Accepted { kind, statuses, data: data.cloned() }))
}
Some(_) => Ok(ExchangeResponse::Err(text_of(&response))),
None => Err(bad(format!("no status in {}", text.chars().take(300).collect::<String>()))),
}
}
}
fn status_of(status: &Value) -> Status {
match status.get("error") {
Some(error) => Status::Error(text_of(error)),
None => Status::Ok(status.clone()),
}
}
fn text_of(value: &Value) -> String {
value.as_str().map_or_else(|| value.to_string(), ToOwned::to_owned)
}
#[cfg(test)]
#[path = "tests/transport.rs"]
mod tests;