Skip to main content

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