Skip to main content

rustio_admin/
http.rs

1//! The HTTP primitives. `Request` and `Response` are thin wrappers around
2//! hyper's types that carry a typed context and a few conveniences.
3
4use std::any::{Any, TypeId};
5use std::collections::HashMap;
6
7use bytes::Bytes;
8use hyper::{Method, StatusCode};
9
10use crate::error::{Error, Result};
11
12// public:
13/// A per-request typed store. Middleware attaches things here
14/// (the authenticated user, the DB handle, etc.) and handlers read them.
15#[derive(Default)]
16pub struct Context {
17    inner: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
18}
19
20impl Context {
21    // public:
22    pub fn insert<T: Any + Send + Sync>(&mut self, value: T) {
23        self.inner.insert(TypeId::of::<T>(), Box::new(value));
24    }
25
26    // public:
27    pub fn get<T: Any + Send + Sync>(&self) -> Option<&T> {
28        self.inner
29            .get(&TypeId::of::<T>())
30            .and_then(|b| b.downcast_ref::<T>())
31    }
32
33    // public:
34    pub fn get_mut<T: Any + Send + Sync>(&mut self) -> Option<&mut T> {
35        self.inner
36            .get_mut(&TypeId::of::<T>())
37            .and_then(|b| b.downcast_mut::<T>())
38    }
39}
40
41// public:
42pub struct Request {
43    method: Method,
44    path: String,
45    query: String,
46    headers: HashMap<String, String>,
47    params: HashMap<String, String>,
48    body: Bytes,
49    ctx: Context,
50}
51
52impl Request {
53    pub(crate) fn new(
54        method: Method,
55        path: String,
56        query: String,
57        headers: HashMap<String, String>,
58        body: Bytes,
59    ) -> Self {
60        Self {
61            method,
62            path,
63            query,
64            headers,
65            params: HashMap::new(),
66            body,
67            ctx: Context::default(),
68        }
69    }
70
71    // public:
72    pub fn method(&self) -> &Method {
73        &self.method
74    }
75
76    // public:
77    pub fn path(&self) -> &str {
78        &self.path
79    }
80
81    // public:
82    pub fn query_string(&self) -> &str {
83        &self.query
84    }
85
86    // public:
87    pub fn query(&self) -> FormData {
88        FormData::from_urlencoded(&self.query)
89    }
90
91    // public:
92    pub fn header(&self, name: &str) -> Option<&str> {
93        self.headers
94            .get(&name.to_ascii_lowercase())
95            .map(|s| s.as_str())
96    }
97
98    // public:
99    pub fn param(&self, name: &str) -> Option<&str> {
100        self.params.get(name).map(|s| s.as_str())
101    }
102
103    // public:
104    pub fn body(&self) -> &[u8] {
105        &self.body
106    }
107
108    // public:
109    pub fn body_text(&self) -> Result<&str> {
110        std::str::from_utf8(&self.body)
111            .map_err(|_| Error::BadRequest("body is not valid utf-8".into()))
112    }
113
114    // public:
115    pub fn form(&self) -> Result<FormData> {
116        let text = self.body_text()?;
117        Ok(FormData::from_urlencoded(text))
118    }
119
120    // public:
121    pub fn ctx(&self) -> &Context {
122        &self.ctx
123    }
124
125    // public:
126    pub fn ctx_mut(&mut self) -> &mut Context {
127        &mut self.ctx
128    }
129
130    pub(crate) fn set_params(&mut self, params: HashMap<String, String>) {
131        self.params = params;
132    }
133
134    // internal: test-only constructor, doc-hidden + cfg-gated
135    /// Test-only minimal-Request constructor. Doc-hidden, gated by
136    /// the `integration-test` feature so it does not appear on the
137    /// public API surface of a regular build. Used by
138    /// `crate::__integration::fake_request()` in the testcontainers
139    /// integration suite — see `DESIGN_R2_ORGANISATIONAL.md` §10.3.
140    #[doc(hidden)]
141    #[cfg(feature = "integration-test")]
142    pub fn __integration_test_fake(path: String, headers: HashMap<String, String>) -> Self {
143        Self::new(
144            hyper::Method::POST,
145            path,
146            String::new(),
147            headers,
148            bytes::Bytes::new(),
149        )
150    }
151}
152
153// public:
154/// Parsed form body (application/x-www-form-urlencoded) or query string.
155/// Values are owned so handlers can move them into DB calls freely.
156#[derive(Debug, Default, Clone)]
157pub struct FormData {
158    fields: HashMap<String, String>,
159}
160
161impl FormData {
162    // public:
163    pub fn from_urlencoded(input: &str) -> Self {
164        let mut fields = HashMap::new();
165        for pair in input.split('&') {
166            if pair.is_empty() {
167                continue;
168            }
169            let (raw_key, raw_val) = match pair.split_once('=') {
170                Some((k, v)) => (k, v),
171                None => (pair, ""),
172            };
173            let key = decode(raw_key);
174            let val = decode(raw_val);
175            fields.insert(key, val);
176        }
177        Self { fields }
178    }
179
180    // public:
181    pub fn get(&self, key: &str) -> Option<&str> {
182        self.fields.get(key).map(|s| s.as_str())
183    }
184
185    // public:
186    pub fn required(&self, key: &str) -> Result<&str> {
187        self.get(key)
188            .ok_or_else(|| Error::BadRequest(format!("field {key} is required")))
189    }
190
191    // public:
192    pub fn bool_flag(&self, key: &str) -> bool {
193        // HTML checkboxes: present means true, absent means false.
194        matches!(self.get(key), Some("on" | "true" | "1" | "yes"))
195    }
196
197    // public:
198    pub fn contains(&self, key: &str) -> bool {
199        self.fields.contains_key(key)
200    }
201
202    // public:
203    pub fn as_map(&self) -> &HashMap<String, String> {
204        &self.fields
205    }
206}
207
208fn decode(s: &str) -> String {
209    let spaced = s.replace('+', " ");
210    urlencoding::decode(&spaced)
211        .map(|c| c.into_owned())
212        .unwrap_or(spaced)
213}
214
215// public:
216/// An outbound HTTP response.
217pub struct Response {
218    pub status: StatusCode,
219    pub headers: Vec<(String, String)>,
220    pub body: Bytes,
221}
222
223impl Response {
224    // public:
225    pub fn new(status: StatusCode, body: impl Into<Bytes>) -> Self {
226        Self {
227            status,
228            headers: Vec::new(),
229            body: body.into(),
230        }
231    }
232
233    // public:
234    pub fn ok(body: impl Into<Bytes>) -> Self {
235        Self::new(StatusCode::OK, body)
236    }
237
238    // public:
239    pub fn html(body: impl Into<String>) -> Self {
240        let text = body.into();
241        Self {
242            status: StatusCode::OK,
243            headers: vec![("content-type".into(), "text/html; charset=utf-8".into())],
244            body: Bytes::from(text),
245        }
246    }
247
248    // public:
249    pub fn json_raw(body: impl Into<String>) -> Self {
250        let text = body.into();
251        Self {
252            status: StatusCode::OK,
253            headers: vec![("content-type".into(), "application/json".into())],
254            body: Bytes::from(text),
255        }
256    }
257
258    // public:
259    pub fn redirect(to: impl Into<String>) -> Self {
260        let url = to.into();
261        Self {
262            status: StatusCode::SEE_OTHER,
263            headers: vec![("location".into(), url)],
264            body: Bytes::new(),
265        }
266    }
267
268    // public:
269    pub fn text(body: impl Into<String>) -> Self {
270        let text = body.into();
271        Self {
272            status: StatusCode::OK,
273            headers: vec![("content-type".into(), "text/plain; charset=utf-8".into())],
274            body: Bytes::from(text),
275        }
276    }
277
278    // public:
279    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
280        self.headers.push((name.into(), value.into()));
281        self
282    }
283
284    // public:
285    pub fn with_status(mut self, status: StatusCode) -> Self {
286        self.status = status;
287        self
288    }
289}
290
291pub(crate) fn response_from_error(err: &Error) -> Response {
292    let status = StatusCode::from_u16(err.status()).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
293    let body = err.client_message().to_string();
294    Response {
295        status,
296        headers: vec![("content-type".into(), "text/plain; charset=utf-8".into())],
297        body: Bytes::from(body),
298    }
299}