Skip to main content

renox_core/
path.rs

1//! `Path`, axum's path extractor with a 404 for values that don't parse.
2
3use std::ops::{Deref, DerefMut};
4
5use axum::extract::FromRequestParts;
6use axum::extract::path::ErrorKind;
7use axum::extract::rejection::PathRejection;
8use axum::http::request::Parts;
9use serde::de::DeserializeOwned;
10
11use crate::Error;
12
13/// The route's parameters (`/orders/{id}` → `Path(id): Path<i64>`), like
14/// axum's `Path`, except that a value that doesn't fit (`/orders/abc`, or an
15/// id too large) is a 404 page, as for a route that doesn't exist, rather
16/// than a plain-text 400.
17///
18/// ```
19/// # use renox::prelude::*;
20/// async fn show(Path(id): Path<i64>) -> String { format!("order {id}") }
21/// async fn line(Path((order, line)): Path<(i64, i64)>) -> String { format!("{order}/{line}") }
22/// ```
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub struct Path<T>(pub T);
25
26impl<T> Deref for Path<T> {
27    type Target = T;
28
29    fn deref(&self) -> &T {
30        &self.0
31    }
32}
33
34impl<T> DerefMut for Path<T> {
35    fn deref_mut(&mut self) -> &mut T {
36        &mut self.0
37    }
38}
39
40impl<T, S> FromRequestParts<S> for Path<T>
41where
42    T: DeserializeOwned + Send,
43    S: Send + Sync,
44{
45    type Rejection = Error;
46
47    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {
48        match axum::extract::Path::<T>::from_request_parts(parts, state).await {
49            Ok(axum::extract::Path(value)) => Ok(Path(value)),
50            // A visitor's value that doesn't parse (`/products/abc`): not found.
51            // A parameter count or type that can't work is the app's mistake.
52            Err(PathRejection::FailedToDeserializePathParams(err)) => match err.kind() {
53                ErrorKind::WrongNumberOfParameters { .. } | ErrorKind::UnsupportedType { .. } => {
54                    Err(anyhow::anyhow!("{err}").into())
55                }
56                _ => Err(Error::NotFound),
57            },
58            // The route has no such parameters: a mistake in the app.
59            Err(other) => Err(anyhow::anyhow!("{other}").into()),
60        }
61    }
62}
63
64/// The model a route parameter names, loaded from the database (Laravel's
65/// route model binding), or a 404 page when there's no such row.
66///
67/// Which parameter: the one named after the model's table (`{product}`
68/// for `Product`), else the route's only parameter. It is read as the
69/// model's key (`{product}`, `{id}`), or, when its name is one of the
70/// model's columns, matched against that column (`/posts/{slug}`). The
71/// query is the model's own, so default scopes (the current tenant) and
72/// soft deletes apply.
73///
74/// ```
75/// # use renox::prelude::*;
76/// # #[derive(Model, serde::Serialize, Default)] struct Product { id: i64, name: String }
77/// # #[derive(Model, serde::Serialize, Default)] struct Post { id: i64, slug: String }
78/// // GET /products/{product}
79/// async fn show(Found(product): Found<Product>) -> String { product.name }
80///
81/// // GET /blog/{slug}: the post whose `slug` column matches
82/// async fn post(Found(post): Found<Post>) -> String { post.slug }
83/// ```
84#[derive(Debug, Clone, Default, PartialEq)]
85pub struct Found<M>(pub M);
86
87impl<M> Deref for Found<M> {
88    type Target = M;
89
90    fn deref(&self) -> &M {
91        &self.0
92    }
93}
94
95impl<M> DerefMut for Found<M> {
96    fn deref_mut(&mut self) -> &mut M {
97        &mut self.0
98    }
99}
100
101impl<M, S> FromRequestParts<S> for Found<M>
102where
103    M: crate::db::Model,
104    S: Send + Sync,
105{
106    type Rejection = Error;
107
108    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Error> {
109        let params = axum::extract::RawPathParams::from_request_parts(parts, state)
110            .await
111            .map_err(|err| anyhow::anyhow!("{err}"))?;
112        let params: Vec<(String, String)> = params
113            .iter()
114            .map(|(name, value)| (name.to_owned(), value.to_owned()))
115            .collect();
116        let (name, value) = match params.iter().find(|(name, _)| name == M::TABLE) {
117            Some(param) => param.clone(),
118            None if params.len() == 1 => params[0].clone(),
119            None => {
120                return Err(anyhow::anyhow!(
121                    "Found<{}> needs a route parameter named `{}` (the route has {})",
122                    std::any::type_name::<M>(),
123                    M::TABLE,
124                    params
125                        .iter()
126                        .map(|(name, _)| format!("`{name}`"))
127                        .collect::<Vec<_>>()
128                        .join(", ")
129                )
130                .into());
131            }
132        };
133        let app = parts
134            .extensions
135            .get::<crate::AppState>()
136            .cloned()
137            .ok_or_else(|| anyhow::anyhow!("Found<T> needs Renox's request layers"))?;
138        let by_column = name != M::TABLE && name != "id" && M::COLUMNS.contains(&name.as_str());
139        let found = if by_column {
140            M::query().where_eq(&name, value).first(&app.db).await?
141        } else {
142            let key = value
143                .parse::<M::Key>()
144                .ok()
145                .filter(|key| !crate::db::ModelKey::is_unsaved(key));
146            match key {
147                Some(key) => M::find(&app.db, key).await?,
148                None => None,
149            }
150        };
151        found.map(Found).ok_or(Error::NotFound)
152    }
153}