Skip to main content

alux_http_parts/
message.rs

1//! States a request and an answer without naming a transport.
2
3use alux_http::{HttpErrorAlg, HttpStatus};
4use core::error::Error;
5use core::fmt::{self, Debug, Display};
6use core::pin::Pin;
7use futures::{Stream, StreamExt};
8use std::io::Error as IoError;
9
10/// What a caller sent, stated without a transport.
11///
12/// Whatever moves bytes decides how a request arrives; this is only what arrived.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct DirectRequest {
15    method: Option<alux_http::HttpMethod>,
16    path: String,
17    query: String,
18    headers: Vec<(String, String)>,
19    body: Vec<u8>,
20}
21
22impl DirectRequest {
23    /// States a request for one path under one method.
24    pub fn new(method: alux_http::HttpMethod, path: &str) -> Self {
25        Self { method: Some(method), path: path.to_owned(), ..Self::default() }
26    }
27
28    /// States the query string the caller sent, without its leading separator.
29    #[must_use]
30    pub fn with_query(mut self, query: &str) -> Self {
31        query.trim_start_matches('?').clone_into(&mut self.query);
32        self
33    }
34
35    /// States one header the caller sent.
36    #[must_use]
37    pub fn with_header(mut self, name: &str, value: &str) -> Self {
38        self.headers.push((name.to_lowercase(), value.to_owned()));
39        self
40    }
41
42    /// States the body the caller sent.
43    #[must_use]
44    pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
45        self.body = body.into();
46        self
47    }
48
49    /// Returns the method this request was sent under.
50    pub fn method(&self) -> Option<alux_http::HttpMethod> {
51        self.method
52    }
53
54    /// Returns the path this request asks for.
55    pub fn path(&self) -> &str {
56        &self.path
57    }
58
59    /// Returns the query string this request carries.
60    pub fn query(&self) -> &str {
61        &self.query
62    }
63
64    /// Returns the value of one header, matched without regard to case.
65    pub fn header(&self, name: &str) -> Option<&str> {
66        let name = name.to_lowercase();
67
68        self.headers.iter().find(|(header, _)| *header == name).map(|(_, value)| value.as_str())
69    }
70
71    /// Returns every header this request carries, in the order it states them.
72    pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
73        self.headers.iter().map(|(name, value)| (name.as_str(), value.as_str()))
74    }
75
76    /// Returns the body this request carries.
77    pub fn body(&self) -> &[u8] {
78        &self.body
79    }
80}
81
82/// What an answer carries, which is either bytes already in hand or bytes still to come.
83///
84/// This interpretation carries no transport, so a body produced over time is carried as what
85/// produces it. Whatever moves bytes drives it, and anything reading the answer whole collects it.
86pub enum DirectBody {
87    /// Bytes that are already there.
88    Stated(Vec<u8>),
89    /// Bytes produced over time, and what produces them.
90    Produced(Chunks),
91}
92
93impl Debug for DirectBody {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            Self::Stated(body) => formatter.debug_tuple("Stated").field(body).finish(),
97            Self::Produced(_) => formatter.write_str("Produced(..)"),
98        }
99    }
100}
101
102impl Default for DirectBody {
103    fn default() -> Self {
104        Self::Stated(Vec::new())
105    }
106}
107
108/// What produces a body over time, once an interpretation has chosen how to carry it.
109pub type Chunks = Pin<Box<dyn Stream<Item = Result<Vec<u8>, IoError>> + Send>>;
110
111/// What a surface answered, stated without a transport.
112#[derive(Debug)]
113pub struct DirectResponse {
114    status: HttpStatus,
115    headers: Vec<(String, String)>,
116    body: DirectBody,
117}
118
119impl DirectResponse {
120    /// Answers with a status and nothing else.
121    pub fn new(status: HttpStatus) -> Self {
122        Self { status, headers: Vec::new(), body: DirectBody::default() }
123    }
124
125    /// Answers with a status, a content type, and a body.
126    pub fn content(status: HttpStatus, content_type: &str, body: impl Into<Vec<u8>>) -> Self {
127        Self::new(status).with_header("content-type", content_type).with_body(body)
128    }
129
130    /// States one header on this answer.
131    #[must_use]
132    pub fn with_header(mut self, name: &str, value: &str) -> Self {
133        self.headers.push((name.to_lowercase(), value.to_owned()));
134        self
135    }
136
137    /// States the body of this answer.
138    #[must_use]
139    pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
140        self.body = DirectBody::Stated(body.into());
141        self
142    }
143
144    /// States a body this answer produces over time.
145    #[must_use]
146    pub fn with_chunks(mut self, chunks: Chunks) -> Self {
147        self.body = DirectBody::Produced(chunks);
148        self
149    }
150
151    /// Returns what this answer carries, which is either bytes or what produces them.
152    pub fn into_body(self) -> DirectBody {
153        self.body
154    }
155
156    /// Answers the same thing with every chunk of its body taken.
157    ///
158    /// A body already in hand is already collected. One produced over time is read to its end, which
159    /// is what anything reading an answer whole has to do.
160    pub async fn collected(mut self) -> Self {
161        let DirectBody::Produced(mut chunks) = self.body else {
162            return self;
163        };
164        let mut collected = Vec::new();
165        while let Some(chunk) = chunks.next().await {
166            match chunk {
167                Ok(chunk) => collected.extend(chunk),
168                Err(error) => {
169                    self.body = DirectBody::Stated(error.to_string().into_bytes());
170
171                    return self.with_status(HttpStatus::INTERNAL);
172                }
173            }
174        }
175        self.body = DirectBody::Stated(collected);
176
177        self
178    }
179
180    /// Answers the same thing under a different status.
181    #[must_use]
182    pub fn with_status(mut self, status: HttpStatus) -> Self {
183        self.status = status;
184        self
185    }
186
187    /// Returns the status this answer carries.
188    pub fn status(&self) -> HttpStatus {
189        self.status
190    }
191
192    /// Returns the value of one header, matched without regard to case.
193    pub fn header(&self, name: &str) -> Option<&str> {
194        let name = name.to_lowercase();
195
196        self.headers.iter().find(|(header, _)| *header == name).map(|(_, value)| value.as_str())
197    }
198
199    /// Returns every header this answer carries, in the order it states them.
200    pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
201        self.headers.iter().map(|(name, value)| (name.as_str(), value.as_str()))
202    }
203
204    /// Returns the bytes this answer already carries, which a body still to come has none of.
205    pub fn body(&self) -> &[u8] {
206        match &self.body {
207            DirectBody::Stated(body) => body,
208            DirectBody::Produced(_) => &[],
209        }
210    }
211
212    /// Returns the body read as text, however it was encoded.
213    pub fn text(&self) -> String {
214        String::from_utf8_lossy(self.body()).into_owned()
215    }
216}
217
218/// What the interpretation itself answers when no handler can be reached.
219///
220/// A domain states its own failures; these are the ones routing and reading a request produce.
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct DirectError {
223    status: HttpStatus,
224    message: String,
225}
226
227impl DirectError {
228    /// States a failure with the status it is answered with.
229    pub fn new(status: HttpStatus, message: impl Into<String>) -> Self {
230        Self { status, message: message.into() }
231    }
232
233    /// States that nothing is declared at a path.
234    pub fn not_found(path: &str) -> Self {
235        Self::new(HttpStatus::NOT_FOUND, format!("nothing answers at `{path}`"))
236    }
237
238    /// States that something is declared at a path, but not under this method.
239    pub fn method_not_allowed(path: &str) -> Self {
240        Self::new(HttpStatus::METHOD_NOT_ALLOWED, format!("`{path}` does not answer this method"))
241    }
242
243    /// States that an argument could not be read from where its role says it comes from.
244    pub fn unreadable(role: &str, reason: &str) -> Self {
245        Self::new(HttpStatus::BAD_REQUEST, format!("the {role} could not be read: {reason}"))
246    }
247}
248
249impl HttpErrorAlg for DirectError {
250    // Routing and reading a request state these; a domain failure carried here states its own.
251    const HTTP_STATUSES: &'static [HttpStatus] =
252        &[HttpStatus::BAD_REQUEST, HttpStatus::NOT_FOUND, HttpStatus::METHOD_NOT_ALLOWED];
253
254    fn http_status(&self) -> HttpStatus {
255        self.status
256    }
257
258    fn http_message(&self) -> String {
259        self.message.clone()
260    }
261}
262
263impl Display for DirectError {
264    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
265        write!(formatter, "{} ({})", self.message, self.status.code())
266    }
267}
268
269impl Error for DirectError {}
270
271impl From<DirectError> for DirectResponse {
272    fn from(error: DirectError) -> Self {
273        Self::content(error.status, "text/plain; charset=utf-8", error.message)
274    }
275}