trailbase_wasm/
http.rs

1use futures_util::future::LocalBoxFuture;
2use serde::de::DeserializeOwned;
3use trailbase_wasm_common::HttpContext;
4use wstd::http::server::{Finished, Responder};
5use wstd::io::{Cursor, Empty, empty};
6
7pub use http::{HeaderMap, HeaderValue, Method, StatusCode, Version, header};
8pub use trailbase_wasm_common::HttpContextUser as User;
9
10pub type Response<T = BoundedBody<Vec<u8>>> = http::Response<T>;
11
12#[derive(Clone, Debug)]
13pub struct HttpError {
14  pub status: StatusCode,
15  pub message: Option<String>,
16}
17
18impl HttpError {
19  pub fn status(status: StatusCode) -> Self {
20    return Self {
21      status,
22      message: None,
23    };
24  }
25
26  pub fn message(status: StatusCode, message: impl std::string::ToString) -> Self {
27    return Self {
28      status,
29      message: Some(message.to_string()),
30    };
31  }
32}
33
34impl From<HttpError> for Response {
35  fn from(value: HttpError) -> Self {
36    return value.into_response();
37  }
38}
39
40type HttpHandler = Box<
41  dyn (FnOnce(
42    HttpContext,
43    http::Request<wstd::http::body::IncomingBody>,
44    wstd::http::server::Responder,
45  ) -> LocalBoxFuture<'static, Finished>),
46>;
47
48pub struct HttpRoute {
49  pub method: Method,
50  pub path: String,
51  pub handler: HttpHandler,
52}
53
54impl HttpRoute {
55  pub fn new<F, R, B>(method: Method, path: impl std::string::ToString, f: F) -> Self
56  where
57    // NOTE: Send + Sync aren't strictly needed. We could also accept AsyncFnOnce, however let's
58    // start more constraint and see where it takes us.
59    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
60    R: IntoResponse<B>,
61    B: wstd::http::body::Body,
62  {
63    return Self {
64      method,
65      path: path.to_string(),
66      handler: Box::new(
67        move |context: HttpContext,
68              req: http::Request<wstd::http::body::IncomingBody>,
69              responder: Responder| {
70          let (head, body) = req.into_parts();
71          let Ok(url) = to_url(head.uri) else {
72            return Box::pin(responder.respond(empty_error_response(StatusCode::BAD_REQUEST)));
73          };
74
75          let req = Request {
76            head: Parts {
77              method: head.method,
78              uri: url,
79              version: head.version,
80              headers: head.headers,
81              user: context.user,
82              path_params: context.path_params,
83            },
84            body,
85          };
86
87          return Box::pin(async move {
88            let response = responder.respond(f(req).await.into_response()).await;
89
90            // TODO: Poll tasks.
91
92            response
93          });
94        },
95      ),
96    };
97  }
98}
99
100pub mod routing {
101  use super::{HttpRoute, IntoResponse, Method, Request};
102
103  pub fn get<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
104  where
105    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
106    R: IntoResponse<B>,
107    B: wstd::http::body::Body,
108  {
109    return HttpRoute::new(Method::GET, path, f);
110  }
111
112  pub fn post<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
113  where
114    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
115    R: IntoResponse<B>,
116    B: wstd::http::body::Body,
117  {
118    return HttpRoute::new(Method::POST, path, f);
119  }
120
121  pub fn patch<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
122  where
123    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
124    R: IntoResponse<B>,
125    B: wstd::http::body::Body,
126  {
127    return HttpRoute::new(Method::PATCH, path, f);
128  }
129
130  pub fn delete<F, R, B>(path: impl std::string::ToString, f: F) -> HttpRoute
131  where
132    F: (AsyncFn(Request) -> R) + Send + Sync + 'static,
133    R: IntoResponse<B>,
134    B: wstd::http::body::Body,
135  {
136    return HttpRoute::new(Method::DELETE, path, f);
137  }
138}
139
140// Disallow external construction.
141#[non_exhaustive]
142#[derive(Clone, Debug)]
143pub struct Parts {
144  /// The request's method
145  pub method: Method,
146
147  /// The request's URI
148  pub uri: url::Url,
149
150  /// The request's version
151  pub version: Version,
152
153  /// The request's headers
154  pub headers: HeaderMap<HeaderValue>,
155
156  /// User metadata
157  pub user: Option<User>,
158
159  /// Path params, e.g. /test/{param}/.
160  pub path_params: Vec<(String, String)>,
161}
162
163#[derive(Debug)]
164pub struct Request {
165  head: Parts,
166  body: wstd::http::body::IncomingBody,
167}
168
169impl Request {
170  #[inline]
171  pub fn body(&mut self) -> &mut wstd::http::body::IncomingBody {
172    return &mut self.body;
173  }
174
175  #[inline]
176  pub fn url(&self) -> &url::Url {
177    return &self.head.uri;
178  }
179
180  pub fn query_parse<T: DeserializeOwned>(&self) -> Result<T, HttpError> {
181    let query = self.head.uri.query().unwrap_or_default();
182    let deserializer =
183      serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query.as_bytes()));
184    return serde_path_to_error::deserialize(deserializer)
185      .map_err(|err| HttpError::message(StatusCode::BAD_REQUEST, err));
186  }
187
188  // pub fn query_pairs(&self) -> url::form_urlencoded::Parse<'_> {
189  //   self.head.uri.query_pairs()
190  // }
191
192  pub fn query_param(&self, param: &str) -> Option<String> {
193    return self
194      .head
195      .uri
196      .query_pairs()
197      .find(|(p, _v)| p == param)
198      .map(|(_p, v)| v.to_string());
199  }
200
201  pub fn path_param(&self, param: &str) -> Option<&str> {
202    return self
203      .head
204      .path_params
205      .iter()
206      .find(|(p, _v)| p == param)
207      .map(|(_p, v)| v.as_str());
208  }
209
210  #[inline]
211  pub fn method(&self) -> &Method {
212    return &self.head.method;
213  }
214
215  #[inline]
216  pub fn version(&self) -> &Version {
217    return &self.head.version;
218  }
219
220  #[inline]
221  pub fn header(&self, key: &str) -> Option<&HeaderValue> {
222    return self.head.headers.get(key);
223  }
224
225  #[inline]
226  pub fn user(&self) -> Option<&User> {
227    return self.head.user.as_ref();
228  }
229}
230
231fn to_url(uri: http::Uri) -> Result<url::Url, url::ParseError> {
232  let http::uri::Parts {
233    scheme,
234    authority,
235    path_and_query,
236    ..
237  } = uri.into_parts();
238
239  return match (scheme, authority, path_and_query) {
240    (Some(s), Some(a), Some(p)) => url::Url::parse(&format!("{s}://{a}/{p}")),
241    (_, _, Some(p)) => url::Url::parse(p.as_str()),
242    _ => Err(url::ParseError::RelativeUrlWithCannotBeABaseBase),
243  };
244}
245
246/// An HTTP body with a known length
247#[derive(Debug, Default)]
248pub struct BoundedBody<T>(Cursor<T>);
249
250impl<T: AsRef<[u8]>> wstd::io::AsyncRead for BoundedBody<T> {
251  async fn read(&mut self, buf: &mut [u8]) -> wstd::io::Result<usize> {
252    self.0.read(buf).await
253  }
254}
255
256impl<T: AsRef<[u8]>> wstd::http::body::Body for BoundedBody<T> {
257  fn len(&self) -> Option<usize> {
258    Some(self.0.get_ref().as_ref().len())
259  }
260}
261
262/// Conversion into a `Body`.
263///
264/// NOTE: We have our own trait over wstd::http::body::IntoBody to avoid possible future conflicts
265/// when implementing IntoResponse for Result<B: IntoBody, HttpError>.
266pub trait IntoBody {
267  /// What type of `Body` are we turning this into?
268  type IntoBody: wstd::http::body::Body;
269  /// Convert into `Body`.
270  fn into_body(self) -> Self::IntoBody;
271}
272
273impl IntoBody for () {
274  type IntoBody = wstd::io::Empty;
275
276  fn into_body(self) -> Self::IntoBody {
277    return wstd::io::empty();
278  }
279}
280
281impl IntoBody for String {
282  type IntoBody = BoundedBody<Vec<u8>>;
283  fn into_body(self) -> Self::IntoBody {
284    BoundedBody(Cursor::new(self.into_bytes()))
285  }
286}
287
288impl IntoBody for &str {
289  type IntoBody = BoundedBody<Vec<u8>>;
290  fn into_body(self) -> Self::IntoBody {
291    BoundedBody(Cursor::new(self.to_owned().into_bytes()))
292  }
293}
294
295impl IntoBody for Vec<u8> {
296  type IntoBody = BoundedBody<Vec<u8>>;
297  fn into_body(self) -> Self::IntoBody {
298    BoundedBody(Cursor::new(self))
299  }
300}
301
302impl IntoBody for &[u8] {
303  type IntoBody = BoundedBody<Vec<u8>>;
304  fn into_body(self) -> Self::IntoBody {
305    BoundedBody(Cursor::new(self.to_owned()))
306  }
307}
308
309pub trait IntoResponse<B> {
310  fn into_response(self) -> http::Response<B>;
311}
312
313impl<B: wstd::http::body::Body> IntoResponse<B> for Response<B> {
314  fn into_response(self) -> http::Response<B> {
315    return self;
316  }
317}
318
319impl<B: wstd::http::body::Body, Err: IntoResponse<B>> IntoResponse<B> for Result<Response<B>, Err> {
320  fn into_response(self) -> http::Response<B> {
321    return match self {
322      Ok(resp) => resp,
323      Err(err) => err.into_response(),
324    };
325  }
326}
327
328impl<B: IntoBody> IntoResponse<B::IntoBody> for B {
329  fn into_response(self) -> http::Response<B::IntoBody> {
330    return http::Response::new(self.into_body());
331  }
332}
333
334impl<B: IntoBody<IntoBody = BoundedBody<Vec<u8>>>> IntoResponse<BoundedBody<Vec<u8>>>
335  for Result<B, HttpError>
336{
337  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
338    return match self {
339      Ok(body) => http::Response::new(body.into_body()),
340      Err(err) => build_response(err.status, err.message.unwrap_or_default().into_body()),
341    };
342  }
343}
344
345impl IntoResponse<BoundedBody<Vec<u8>>> for HttpError {
346  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
347    return build_response(self.status, self.message.unwrap_or_default().into_body());
348  }
349}
350
351impl IntoResponse<BoundedBody<Vec<u8>>> for Result<(), HttpError> {
352  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
353    return match self {
354      Ok(_) => http::Response::new("".into_body()),
355      Err(err) => err.into_response(),
356    };
357  }
358}
359
360#[derive(Debug, Clone, Copy, Default)]
361#[must_use]
362pub struct Json<T>(pub T);
363
364impl<T> IntoResponse<BoundedBody<Vec<u8>>> for Json<T>
365where
366  T: serde::Serialize,
367{
368  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
369    return build_json_response(StatusCode::OK, self.0);
370  }
371}
372
373impl<T> IntoResponse<BoundedBody<Vec<u8>>> for std::result::Result<Json<T>, HttpError>
374where
375  T: serde::Serialize,
376{
377  fn into_response(self) -> http::Response<BoundedBody<Vec<u8>>> {
378    return match self {
379      Ok(json) => {
380        return build_json_response(StatusCode::OK, json.0);
381      }
382      Err(err) => build_response(err.status, err.message.unwrap_or_default().into_body()),
383    };
384  }
385}
386
387/// An HTML response.
388///
389/// Will automatically get `Content-Type: text/html`.
390#[derive(Clone, Copy, Debug)]
391#[must_use]
392pub struct Html<T>(pub T);
393
394impl<T> IntoResponse<BoundedBody<Vec<u8>>> for Html<T>
395where
396  T: IntoResponse<BoundedBody<Vec<u8>>>,
397{
398  fn into_response(self) -> Response {
399    let mut r = self.0.into_response();
400    r.headers_mut().insert(
401      http::header::CONTENT_TYPE,
402      http::HeaderValue::from_static(mime::TEXT_HTML_UTF_8.as_ref()),
403    );
404    return r;
405  }
406}
407
408#[derive(Debug, Clone)]
409#[must_use = "needs to be returned from a handler or otherwise turned into a Response to be useful"]
410pub struct Redirect {
411  status_code: StatusCode,
412  location: http::header::HeaderValue,
413}
414
415impl Redirect {
416  pub fn to(uri: &str) -> Self {
417    Self::with_status_code(StatusCode::SEE_OTHER, uri)
418  }
419
420  pub fn temporary(uri: &str) -> Self {
421    Self::with_status_code(StatusCode::TEMPORARY_REDIRECT, uri)
422  }
423
424  pub fn permanent(uri: &str) -> Self {
425    Self::with_status_code(StatusCode::PERMANENT_REDIRECT, uri)
426  }
427
428  fn with_status_code(status_code: StatusCode, uri: &str) -> Self {
429    assert!(
430      status_code.is_redirection(),
431      "not a redirection status code"
432    );
433
434    Self {
435      status_code,
436      location: HeaderValue::try_from(uri).expect("URI isn't a valid header value"),
437    }
438  }
439}
440
441impl<B: wstd::http::body::Body + Default> IntoResponse<B> for Redirect {
442  fn into_response(self) -> http::Response<B> {
443    let mut response = http::Response::<B>::default();
444    *response.status_mut() = self.status_code;
445    response
446      .headers_mut()
447      .insert(http::header::LOCATION, self.location);
448    return response;
449  }
450}
451
452pub(crate) fn empty_error_response(status: StatusCode) -> http::Response<Empty> {
453  let mut response = http::Response::new(empty());
454  *response.status_mut() = status;
455  return response;
456}
457
458fn internal_error_response() -> http::Response<BoundedBody<Vec<u8>>> {
459  return build_response(StatusCode::INTERNAL_SERVER_ERROR, "".into_body());
460}
461
462#[inline]
463fn build_response(
464  status: StatusCode,
465  body: BoundedBody<Vec<u8>>,
466) -> http::Response<BoundedBody<Vec<u8>>> {
467  let mut response = http::Response::new(body);
468  *response.status_mut() = status;
469  return response;
470}
471
472#[inline]
473fn build_json_response<T: serde::Serialize>(
474  status: StatusCode,
475  value: T,
476) -> http::Response<BoundedBody<Vec<u8>>> {
477  let Ok(bytes) = serde_json::to_vec(&value) else {
478    return internal_error_response();
479  };
480
481  let mut response = build_response(status, bytes.into_body());
482  response.headers_mut().insert(
483    http::header::CONTENT_TYPE,
484    HeaderValue::from_static("application/json"),
485  );
486
487  return response;
488}