use alux_http::{HttpErrorAlg, HttpStatus};
use core::error::Error;
use core::fmt::{self, Debug, Display};
use core::pin::Pin;
use futures::{Stream, StreamExt};
use std::io::Error as IoError;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DirectRequest {
method: Option<alux_http::HttpMethod>,
path: String,
query: String,
headers: Vec<(String, String)>,
body: Vec<u8>,
}
impl DirectRequest {
pub fn new(method: alux_http::HttpMethod, path: &str) -> Self {
Self { method: Some(method), path: path.to_owned(), ..Self::default() }
}
#[must_use]
pub fn with_query(mut self, query: &str) -> Self {
query.trim_start_matches('?').clone_into(&mut self.query);
self
}
#[must_use]
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.headers.push((name.to_lowercase(), value.to_owned()));
self
}
#[must_use]
pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = body.into();
self
}
pub fn method(&self) -> Option<alux_http::HttpMethod> {
self.method
}
pub fn path(&self) -> &str {
&self.path
}
pub fn query(&self) -> &str {
&self.query
}
pub fn header(&self, name: &str) -> Option<&str> {
let name = name.to_lowercase();
self.headers.iter().find(|(header, _)| *header == name).map(|(_, value)| value.as_str())
}
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
self.headers.iter().map(|(name, value)| (name.as_str(), value.as_str()))
}
pub fn body(&self) -> &[u8] {
&self.body
}
}
pub enum DirectBody {
Stated(Vec<u8>),
Produced(Chunks),
}
impl Debug for DirectBody {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Stated(body) => formatter.debug_tuple("Stated").field(body).finish(),
Self::Produced(_) => formatter.write_str("Produced(..)"),
}
}
}
impl Default for DirectBody {
fn default() -> Self {
Self::Stated(Vec::new())
}
}
pub type Chunks = Pin<Box<dyn Stream<Item = Result<Vec<u8>, IoError>> + Send>>;
#[derive(Debug)]
pub struct DirectResponse {
status: HttpStatus,
headers: Vec<(String, String)>,
body: DirectBody,
}
impl DirectResponse {
pub fn new(status: HttpStatus) -> Self {
Self { status, headers: Vec::new(), body: DirectBody::default() }
}
pub fn content(status: HttpStatus, content_type: &str, body: impl Into<Vec<u8>>) -> Self {
Self::new(status).with_header("content-type", content_type).with_body(body)
}
#[must_use]
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.headers.push((name.to_lowercase(), value.to_owned()));
self
}
#[must_use]
pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = DirectBody::Stated(body.into());
self
}
#[must_use]
pub fn with_chunks(mut self, chunks: Chunks) -> Self {
self.body = DirectBody::Produced(chunks);
self
}
pub fn into_body(self) -> DirectBody {
self.body
}
pub async fn collected(mut self) -> Self {
let DirectBody::Produced(mut chunks) = self.body else {
return self;
};
let mut collected = Vec::new();
while let Some(chunk) = chunks.next().await {
match chunk {
Ok(chunk) => collected.extend(chunk),
Err(error) => {
self.body = DirectBody::Stated(error.to_string().into_bytes());
return self.with_status(HttpStatus::INTERNAL);
}
}
}
self.body = DirectBody::Stated(collected);
self
}
#[must_use]
pub fn with_status(mut self, status: HttpStatus) -> Self {
self.status = status;
self
}
pub fn status(&self) -> HttpStatus {
self.status
}
pub fn header(&self, name: &str) -> Option<&str> {
let name = name.to_lowercase();
self.headers.iter().find(|(header, _)| *header == name).map(|(_, value)| value.as_str())
}
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
self.headers.iter().map(|(name, value)| (name.as_str(), value.as_str()))
}
pub fn body(&self) -> &[u8] {
match &self.body {
DirectBody::Stated(body) => body,
DirectBody::Produced(_) => &[],
}
}
pub fn text(&self) -> String {
String::from_utf8_lossy(self.body()).into_owned()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirectError {
status: HttpStatus,
message: String,
}
impl DirectError {
pub fn new(status: HttpStatus, message: impl Into<String>) -> Self {
Self { status, message: message.into() }
}
pub fn not_found(path: &str) -> Self {
Self::new(HttpStatus::NOT_FOUND, format!("nothing answers at `{path}`"))
}
pub fn method_not_allowed(path: &str) -> Self {
Self::new(HttpStatus::METHOD_NOT_ALLOWED, format!("`{path}` does not answer this method"))
}
pub fn unreadable(role: &str, reason: &str) -> Self {
Self::new(HttpStatus::BAD_REQUEST, format!("the {role} could not be read: {reason}"))
}
}
impl HttpErrorAlg for DirectError {
const HTTP_STATUSES: &'static [HttpStatus] =
&[HttpStatus::BAD_REQUEST, HttpStatus::NOT_FOUND, HttpStatus::METHOD_NOT_ALLOWED];
fn http_status(&self) -> HttpStatus {
self.status
}
fn http_message(&self) -> String {
self.message.clone()
}
}
impl Display for DirectError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{} ({})", self.message, self.status.code())
}
}
impl Error for DirectError {}
impl From<DirectError> for DirectResponse {
fn from(error: DirectError) -> Self {
Self::content(error.status, "text/plain; charset=utf-8", error.message)
}
}