use std::fmt;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use ferrin_spec::BoxFuture;
use ferrin_spec::BoxStream;
use ferrin_spec::Headers;
use ferrin_spec::JsonValue;
use http::Method;
use http::StatusCode;
use serde_json::json;
use tokio_util::sync::CancellationToken;
use url::Url;
pub type BodyStream = BoxStream<'static, Result<Bytes, TransportError>>;
pub type SharedTransport = Arc<dyn HttpTransport>;
pub trait HttpTransport: Send + Sync + 'static {
fn execute(&self, request: HttpRequest) -> BoxFuture<'_, Result<HttpResponse, TransportError>>;
}
impl<T: HttpTransport + ?Sized> HttpTransport for Arc<T> {
fn execute(&self, request: HttpRequest) -> BoxFuture<'_, Result<HttpResponse, TransportError>> {
(**self).execute(request)
}
}
#[derive(Debug)]
pub struct HttpRequest {
pub method: Method,
pub url: Url,
pub headers: Headers,
pub body: RequestBody,
pub cancellation: CancellationToken,
pub timeout: Option<Duration>,
pub pinned_addresses: Vec<SocketAddr>,
}
impl HttpRequest {
#[must_use]
pub fn new(method: Method, url: Url) -> Self {
Self {
method,
url,
headers: Headers::new(),
body: RequestBody::Empty,
cancellation: CancellationToken::new(),
timeout: None,
pinned_addresses: Vec::new(),
}
}
#[must_use]
pub fn get(url: Url) -> Self {
Self::new(Method::GET, url)
}
#[must_use]
pub fn post(url: Url) -> Self {
Self::new(Method::POST, url)
}
#[must_use]
pub fn with_headers(mut self, headers: Headers) -> Self {
self.headers = headers;
self
}
#[must_use]
pub fn with_body(mut self, body: RequestBody) -> Self {
self.body = body;
self
}
#[must_use]
pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
self.cancellation = cancellation;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
#[must_use]
pub fn with_pinned_addresses(mut self, addresses: Vec<SocketAddr>) -> Self {
self.pinned_addresses = addresses;
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum RequestBody {
Empty,
Bytes {
content_type: String,
data: Bytes,
},
Multipart(MultipartForm),
}
impl RequestBody {
#[must_use]
pub fn json(data: Bytes) -> Self {
Self::Bytes {
content_type: "application/json".to_owned(),
data,
}
}
#[must_use]
pub fn content_type(&self) -> Option<String> {
match self {
Self::Empty => None,
Self::Bytes { content_type, .. } => Some(content_type.clone()),
Self::Multipart(form) => Some(form.content_type()),
}
}
#[must_use]
pub fn to_bytes(&self) -> Bytes {
match self {
Self::Empty => Bytes::new(),
Self::Bytes { data, .. } => data.clone(),
Self::Multipart(form) => form.encode(),
}
}
}
#[derive(Debug, Clone)]
pub struct MultipartForm {
parts: Vec<MultipartPart>,
boundary: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MultipartPart {
Field {
name: String,
value: String,
},
File {
name: String,
filename: Option<String>,
media_type: Option<String>,
data: Bytes,
},
}
impl Default for MultipartForm {
fn default() -> Self {
Self::new()
}
}
impl MultipartForm {
#[must_use]
pub fn new() -> Self {
Self {
parts: Vec::new(),
boundary: format!("ferrin-multipart-{}", crate::ids::generate_id()),
}
}
#[must_use]
pub fn with_boundary(boundary: impl Into<String>) -> Self {
Self {
parts: Vec::new(),
boundary: boundary.into(),
}
}
#[must_use]
pub fn field(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.parts.push(MultipartPart::Field {
name: name.into(),
value: value.into(),
});
self
}
#[must_use]
pub fn file(
mut self,
name: impl Into<String>,
filename: Option<String>,
media_type: Option<String>,
data: Bytes,
) -> Self {
self.parts.push(MultipartPart::File {
name: name.into(),
filename,
media_type,
data,
});
self
}
#[must_use]
pub fn parts(&self) -> &[MultipartPart] {
&self.parts
}
#[must_use]
pub fn boundary(&self) -> &str {
&self.boundary
}
#[must_use]
pub fn content_type(&self) -> String {
format!("multipart/form-data; boundary={}", self.boundary)
}
#[must_use]
pub fn values(&self) -> JsonValue {
let mut map = serde_json::Map::new();
for part in &self.parts {
match part {
MultipartPart::Field { name, value } => {
map.insert(name.clone(), json!(value));
}
MultipartPart::File { name, filename, .. } => {
let label = filename.as_deref().unwrap_or(name);
map.insert(name.clone(), json!(format!("<file:{label}>")));
}
}
}
JsonValue::Object(map)
}
#[must_use]
pub fn encode(&self) -> Bytes {
let mut out = Vec::new();
for part in &self.parts {
out.extend_from_slice(b"--");
out.extend_from_slice(self.boundary.as_bytes());
out.extend_from_slice(b"\r\nContent-Disposition: form-data; name=\"");
match part {
MultipartPart::Field { name, value } => {
out.extend_from_slice(escape_header_value(name).as_bytes());
out.extend_from_slice(b"\"\r\n\r\n");
out.extend_from_slice(value.as_bytes());
out.extend_from_slice(b"\r\n");
}
MultipartPart::File {
name,
filename,
media_type,
data,
} => {
out.extend_from_slice(escape_header_value(name).as_bytes());
out.extend_from_slice(b"\"; filename=\"");
out.extend_from_slice(
escape_header_value(filename.as_deref().unwrap_or("blob")).as_bytes(),
);
out.extend_from_slice(b"\"\r\nContent-Type: ");
let media_type = media_type.as_deref().unwrap_or("application/octet-stream");
out.extend(
media_type
.bytes()
.filter(|byte| *byte != b'\r' && *byte != b'\n'),
);
out.extend_from_slice(b"\r\n\r\n");
out.extend_from_slice(data);
out.extend_from_slice(b"\r\n");
}
}
}
out.extend_from_slice(b"--");
out.extend_from_slice(self.boundary.as_bytes());
out.extend_from_slice(b"--\r\n");
Bytes::from(out)
}
}
fn escape_header_value(value: &str) -> String {
value
.chars()
.filter(|ch| *ch != '\r' && *ch != '\n')
.flat_map(|ch| match ch {
'\\' => vec!['\\', '\\'],
'"' => vec!['\\', '"'],
other => vec![other],
})
.collect()
}
#[derive(Debug, Clone)]
pub struct ResponseHead {
pub status: StatusCode,
pub headers: Headers,
}
pub struct HttpResponse {
pub status: StatusCode,
pub headers: Headers,
pub body: BodyStream,
}
impl fmt::Debug for HttpResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpResponse")
.field("status", &self.status)
.field("headers", &self.headers)
.finish_non_exhaustive()
}
}
impl HttpResponse {
#[must_use]
pub fn from_bytes(status: StatusCode, headers: Headers, body: Bytes) -> Self {
Self {
status,
headers,
body: Box::pin(futures_util::stream::once(std::future::ready(Ok(body)))),
}
}
#[must_use]
pub fn from_stream(status: StatusCode, headers: Headers, body: BodyStream) -> Self {
Self {
status,
headers,
body,
}
}
#[must_use]
pub fn head(&self) -> ResponseHead {
ResponseHead {
status: self.status,
headers: self.headers.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TransportErrorKind {
Connect,
Timeout,
Reset,
Io,
Tls,
InvalidUrl,
InvalidRequest,
Body,
BodyTooLarge,
Cancelled,
Other,
}
#[derive(Debug, thiserror::Error)]
#[error("{kind:?}: {message}")]
pub struct TransportError {
pub kind: TransportErrorKind,
pub message: String,
#[source]
pub cause: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl TransportError {
#[must_use]
pub fn new(kind: TransportErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
cause: None,
}
}
#[must_use]
pub fn with_cause(mut self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
self.cause = Some(Box::new(cause));
self
}
#[must_use]
pub fn cancelled() -> Self {
Self::new(TransportErrorKind::Cancelled, "request cancelled")
}
#[must_use]
pub fn is_retryable(&self) -> bool {
matches!(
self.kind,
TransportErrorKind::Connect
| TransportErrorKind::Timeout
| TransportErrorKind::Reset
| TransportErrorKind::Io
)
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.kind == TransportErrorKind::Cancelled
}
}