#![forbid(unsafe_code)]
#![deny(
missing_copy_implementations,
rustdoc::missing_crate_level_docs,
missing_debug_implementations,
missing_docs,
nonstandard_style,
unused_qualifications
)]
use serde::{de::DeserializeOwned, Serialize};
pub use serde_json::{json, Value};
use std::{fmt::Debug, future::Future, marker::PhantomData};
use trillium::{async_trait, conn_try, Conn, Handler, KnownHeaderName::ContentType};
#[derive(Default, Debug)]
pub struct ApiHandler<F, BodyType> {
handler_fn: F,
body_type: PhantomData<BodyType>,
}
pub fn api<F, Fut, BodyType>(handler_fn: F) -> ApiHandler<F, BodyType>
where
BodyType: DeserializeOwned + Send + Sync + 'static,
F: Fn(Conn, BodyType) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Conn> + Send + 'static,
{
ApiHandler::new(handler_fn)
}
impl<F, Fut, BodyType> ApiHandler<F, BodyType>
where
BodyType: DeserializeOwned + Send + Sync + 'static,
F: Fn(Conn, BodyType) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Conn> + Send + 'static,
{
pub fn new(handler_fn: F) -> Self {
Self {
handler_fn,
body_type: PhantomData::default(),
}
}
}
#[async_trait]
impl<F, Fut, BodyType> Handler for ApiHandler<F, BodyType>
where
BodyType: DeserializeOwned + Send + Sync + 'static,
F: Fn(Conn, BodyType) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Conn> + Send + 'static,
{
async fn run(&self, mut conn: Conn) -> Conn {
match conn.deserialize::<BodyType>().await {
Ok(b) => (self.handler_fn)(conn, b).await,
Err(e) => conn.with_json(&e).with_status(422).halt(),
}
}
}
#[trillium::async_trait]
pub trait ApiConnExt {
fn with_json(self, response: &impl Serialize) -> Self;
async fn deserialize<T>(&mut self) -> Result<T, Value>
where
T: DeserializeOwned;
}
#[trillium::async_trait]
impl ApiConnExt for Conn {
fn with_json(self, response: &impl Serialize) -> Self {
let body = conn_try!(serde_json::to_string(&response), self);
self.ok(body).with_header(ContentType, "application/json")
}
async fn deserialize<T>(&mut self) -> Result<T, Value>
where
T: DeserializeOwned,
{
let body = self
.request_body_string()
.await
.map_err(|e| json!({ "errorType": "io error", "message": e.to_string() }))?;
let content_type = self
.headers()
.get_str(ContentType)
.and_then(|c| c.parse().ok())
.unwrap_or(mime::APPLICATION_JSON);
match content_type.subtype().as_str() {
"json" => serde_json::from_str::<T>(&body).map_err(|e| {
json!({
"input": body,
"line": e.line(),
"column": e.column(),
"message": e.to_string()
})
}),
#[cfg(feature = "forms")]
"x-www-form-urlencoded" => serde_urlencoded::from_str::<T>(&body)
.map_err(|e| json!({ "input": body, "message": e.to_string() })),
_ => Err(json!({
"errorType": format!("unknown content type {}", content_type)
})),
}
}
}