use std::marker::PhantomData;
use std::sync::Arc;
use rainier_database::{Database, EntityRepository, Model, Repository};
use rainier_http::{FromRequest, Request};
use rainier_support::{str::snake, BoxedFuture, Error, Result};
pub struct Bound<M: Model>(pub M);
impl<M: Model> Bound<M> {
pub fn into_inner(self) -> M {
self.0
}
}
impl<M: Model> std::ops::Deref for Bound<M> {
type Target = M;
fn deref(&self) -> &M {
&self.0
}
}
impl<M: Model> FromRequest for Bound<M> {
fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
Box::pin(async move { resolve::<M>(&request, &snake(M::model_name())).await.map(Bound) })
}
}
pub struct BoundAs<M: Model, K: ParamKey>(pub M, PhantomData<fn() -> K>);
impl<M: Model, K: ParamKey> BoundAs<M, K> {
pub fn into_inner(self) -> M {
self.0
}
}
impl<M: Model, K: ParamKey> std::ops::Deref for BoundAs<M, K> {
type Target = M;
fn deref(&self) -> &M {
&self.0
}
}
impl<M: Model, K: ParamKey> FromRequest for BoundAs<M, K> {
fn from_request(request: Arc<Request>) -> BoxedFuture<Result<Self>> {
Box::pin(async move {
resolve::<M>(&request, K::NAME).await.map(|model| BoundAs(model, PhantomData))
})
}
}
pub trait ParamKey: Send + Sync + 'static {
const NAME: &'static str;
}
#[macro_export]
macro_rules! param_key {
($(#[$meta:meta])* $vis:vis $name:ident = $param:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy)]
$vis struct $name;
impl $crate::binding::ParamKey for $name {
const NAME: &'static str = $param;
}
};
}
async fn resolve<M: Model>(request: &Request, name: &str) -> Result<M> {
let Some(value) = request.route_param(name) else {
return Err(Error::internal(format!(
"the route has no `{{{name}}}` parameter to bind a {} from",
M::model_name()
)));
};
let database = rainier_container::facade_application().resolve::<Database>()?;
let repository = EntityRepository::<M>::new((*database).clone());
repository
.first_by(M::route_key_name(), value.into())
.await?
.ok_or_else(|| Error::not_found(format!("No {} matches the given key.", M::model_name())))
}
#[cfg(test)]
mod tests {
use super::*;
param_key!(
pub AuthorParam = "author"
);
#[test]
fn a_param_key_carries_its_name() {
assert_eq!(AuthorParam::NAME, "author");
}
#[test]
fn the_conventional_parameter_is_the_model_name_lower_cased() {
assert_eq!(snake("Post"), "post");
assert_eq!(snake("BlogPost"), "blog_post");
}
#[tokio::test]
async fn a_route_with_no_such_parameter_is_a_wiring_error_not_a_404() {
#[derive(Debug, Clone, PartialEq, rainier_orm::Entity)]
#[orm(table = "posts")]
struct Post {
#[orm(pk)]
id: u64,
}
impl Model for Post {}
let request = Request::builder().build();
let err = resolve::<Post>(&request, "post").await.unwrap_err();
assert_eq!(err.status(), 500, "{}", err.message());
assert!(err.message().contains("{post}"), "{}", err.message());
assert!(err.message().contains("Post"), "{}", err.message());
}
}