axum_option/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use axum::{
4    extract::{rejection::PathRejection, FromRequest, FromRequestParts, Path, Query},
5    http::Request,
6};
7use serde::Deserialize;
8
9/// Validated Option allows your extractors to use either a valid
10/// `T`, or a missing `T`, but reject an invalid `T`, based on the
11/// definition of 'missing' for those items.
12///
13/// For this to work, the crate that defines the extractor must
14/// implement `FromRequestPartsOptional` for the extractor.
15pub struct ValidOption<T>(pub Option<T>);
16
17#[axum::async_trait]
18impl<S, T> FromRequestParts<S> for ValidOption<T>
19where
20    S: Send + Sync,
21    T: FromRequestPartsOptional<S> + Send + Sync,
22    T::Rejection: Send + Sync,
23{
24    type Rejection = <T as FromRequestParts<S>>::Rejection;
25
26    async fn from_request_parts(
27        parts: &mut axum::http::request::Parts,
28        state: &S,
29    ) -> Result<Self, Self::Rejection> {
30        T::option_reject(T::from_request_parts(parts, state).await)
31            .await
32            .map(ValidOption)
33    }
34}
35
36#[axum::async_trait]
37impl<S, B, T> FromRequest<S, B> for ValidOption<T>
38where
39    S: Send + Sync,
40    B: Sync + Send + 'static,
41    T: FromRequestOptional<S, B> + Send + Sync,
42    T::Rejection: Send + Sync,
43{
44    type Rejection = <T as FromRequest<S, B>>::Rejection;
45
46    async fn from_request(req: Request<B>, body: &S) -> Result<Self, Self::Rejection> {
47        T::option_reject(T::from_request(req, body).await)
48            .await
49            .map(ValidOption)
50    }
51}
52
53#[axum::async_trait]
54#[cfg_attr(
55    nightly_error_messages,
56    rustc_on_unimplemented(
57        note = "Function argument is not a valid optional extractor. \nSee `https://docs.rs/axum-option/latest/axum-option` for details",
58    )
59)]
60pub trait FromRequestPartsOptional<S>: FromRequestParts<S> {
61    async fn option_reject(
62        result: Result<Self, Self::Rejection>,
63    ) -> Result<Option<Self>, Self::Rejection>;
64}
65
66#[axum::async_trait]
67#[cfg_attr(
68    nightly_error_messages,
69    rustc_on_unimplemented(
70        note = "Function argument is not a valid optional extractor. \nSee `https://docs.rs/axum-option/latest/axum-option` for details",
71    )
72)]
73pub trait FromRequestOptional<S, B>: FromRequest<S, B> {
74    async fn option_reject(
75        result: Result<Self, Self::Rejection>,
76    ) -> Result<Option<Self>, Self::Rejection>;
77}
78
79/// note: requires a PR into axum first
80///
81#[axum::async_trait]
82impl<S: Send + Sync, T> FromRequestPartsOptional<S> for Query<T>
83where
84    T: std::fmt::Debug + for<'de> Deserialize<'de> + Send + Sync,
85{
86    async fn option_reject(
87        result: Result<Self, Self::Rejection>,
88    ) -> Result<Option<Self>, Self::Rejection> {
89        println!("{:?}", result);
90        match result {
91            Ok(query) => Ok(Some(query)),
92            // Err(QueryRejection::MissingQueryString(_)) => Ok(None),
93            Err(e) => Err(e),
94        }
95    }
96}
97
98/// note: requires a PR into axum first
99///
100#[axum::async_trait]
101impl<S: Send + Sync, T: Send + Sync + for<'de> Deserialize<'de>> FromRequestPartsOptional<S>
102    for Path<T>
103{
104    async fn option_reject(
105        result: Result<Self, Self::Rejection>,
106    ) -> Result<Option<Self>, Self::Rejection> {
107        match result {
108            Ok(p) => Ok(Some(p)),
109            Err(PathRejection::MissingPathParams(_)) => Ok(None),
110            Err(e) => Err(e),
111        }
112    }
113}
114
115#[cfg(feature = "headers")]
116mod headers {
117    use axum::extract::rejection::TypedHeaderRejectionReason;
118    use axum::headers::Header;
119    use axum::TypedHeader;
120
121    use super::FromRequestPartsOptional;
122
123    #[axum::async_trait]
124    impl<S: Send + Sync, T: Header + Send + Sync> FromRequestPartsOptional<S> for TypedHeader<T> {
125        async fn option_reject(
126            result: Result<Self, Self::Rejection>,
127        ) -> Result<Option<Self>, Self::Rejection> {
128            match result {
129                Ok(header) => Ok(Some(header)),
130                Err(e) if matches!(e.reason(), TypedHeaderRejectionReason::Missing) => Ok(None),
131                Err(e) => Err(e),
132            }
133        }
134    }
135}
136
137#[cfg(feature = "jwt-authorizer")]
138mod jwt_authorizer {
139    use jwt_authorizer::{error::AuthError, JwtClaims};
140    use serde::Deserialize;
141
142    use super::FromRequestPartsOptional;
143
144    #[axum::async_trait]
145    impl<S, T> FromRequestPartsOptional<S> for JwtClaims<T>
146    where
147        S: Send + Sync,
148        T: Clone + 'static + Sync + Send + for<'de> Deserialize<'de>,
149    {
150        async fn option_reject(
151            result: Result<Self, Self::Rejection>,
152        ) -> Result<Option<Self>, Self::Rejection> {
153            match result {
154                Ok(claims) => Ok(Some(claims)),
155                Err(AuthError::MissingToken()) => Ok(None),
156                Err(e) => Err(e),
157            }
158        }
159    }
160}