use std::collections::BTreeMap;
use std::time::Duration;
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Method {
Get,
Post,
}
impl Method {
pub fn as_str(self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpRequest {
pub method: Method,
pub url: String,
pub headers: BTreeMap<String, String>,
pub connect_timeout: Duration,
pub request_timeout: Duration,
pub body: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HttpResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: String,
}
impl HttpResponse {
pub fn new(status: u16, body: impl Into<String>) -> Self {
Self { status, headers: Vec::new(), body: body.into() }
}
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.iter().find(|(key, _)| key.eq_ignore_ascii_case(name)).map(|(_, value)| value.as_str())
}
pub(crate) fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
}
pub trait HttpTransport: Send + Sync {
fn send(&self, request: &HttpRequest) -> Result<HttpResponse>;
}
impl<T: HttpTransport + ?Sized> HttpTransport for std::sync::Arc<T> {
fn send(&self, request: &HttpRequest) -> Result<HttpResponse> {
(**self).send(request)
}
}
#[cfg(feature = "ureq-transport")]
pub use self::ureq_transport::UreqTransport;
#[cfg(feature = "ureq-transport")]
mod ureq_transport {
use std::io::Read;
use super::{HttpRequest, HttpResponse, HttpTransport, Method};
use crate::error::{ApiError, Result};
const MAX_BODY_BYTES: u64 = 4 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct UreqTransport {
agent: ureq::Agent,
}
impl Default for UreqTransport {
fn default() -> Self {
Self::new()
}
}
impl UreqTransport {
pub fn new() -> Self {
let agent = ureq::Agent::config_builder()
.http_status_as_error(false)
.build()
.new_agent();
Self { agent }
}
}
fn unreachable(error: ureq::Error) -> crate::Error {
ApiError::unreachable(format!("Could not reach the Moonbase API: {error}")).into()
}
impl HttpTransport for UreqTransport {
fn send(&self, request: &HttpRequest) -> Result<HttpResponse> {
let response = match request.method {
Method::Get => {
let mut builder = self.agent.get(&request.url);
for (name, value) in &request.headers {
builder = builder.header(name, value);
}
builder
.config()
.timeout_connect(Some(request.connect_timeout))
.timeout_global(Some(request.request_timeout))
.build()
.call()
}
Method::Post => {
let mut builder = self.agent.post(&request.url);
for (name, value) in &request.headers {
builder = builder.header(name, value);
}
builder
.config()
.timeout_connect(Some(request.connect_timeout))
.timeout_global(Some(request.request_timeout))
.build()
.send(request.body.as_bytes())
}
}
.map_err(unreachable)?;
let status = response.status().as_u16();
let headers = response
.headers()
.iter()
.map(|(name, value)| (name.as_str().to_owned(), String::from_utf8_lossy(value.as_bytes()).into_owned()))
.collect();
let mut bytes = Vec::new();
response
.into_body()
.into_reader()
.take(MAX_BODY_BYTES)
.read_to_end(&mut bytes)
.map_err(|error| ApiError::unreachable(format!("Could not read the Moonbase API response: {error}")))?;
Ok(HttpResponse { status, headers, body: String::from_utf8_lossy(&bytes).into_owned() })
}
}
}