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