Skip to main content

backbone_core/
extractors.rs

1//! Request body extractors.
2//!
3//! [`JsonOrForm`] accepts a request body as EITHER `application/json` OR
4//! `application/x-www-form-urlencoded` (defaulting to JSON when the
5//! `Content-Type` is absent/unknown), so endpoints built with
6//! [`crate::http::BackboneCrudHandler`] — and any handler using it — work with
7//! both content types. Drop-in replacement for `Json(x): Json<T>` /
8//! `Form(x): Form<T>` in handler signatures and route closures.
9
10use axum::{
11    async_trait,
12    extract::{FromRequest, Request},
13    http::header::CONTENT_TYPE,
14    response::{IntoResponse, Response},
15    Form, Json, RequestExt,
16};
17
18/// Body extractor that accepts JSON or url-encoded form data.
19pub struct JsonOrForm<T>(pub T);
20
21#[async_trait]
22impl<S, T> FromRequest<S> for JsonOrForm<T>
23where
24    S: Send + Sync,
25    Json<T>: FromRequest<()>,
26    Form<T>: FromRequest<()>,
27    T: 'static,
28{
29    type Rejection = Response;
30
31    async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
32        let is_form = req
33            .headers()
34            .get(CONTENT_TYPE)
35            .and_then(|v| v.to_str().ok())
36            .map(|ct| ct.starts_with("application/x-www-form-urlencoded"))
37            .unwrap_or(false);
38
39        if is_form {
40            let Form(value) = req.extract().await.map_err(IntoResponse::into_response)?;
41            Ok(Self(value))
42        } else {
43            // JSON (and any other / missing content-type, leniently)
44            let Json(value) = req.extract().await.map_err(IntoResponse::into_response)?;
45            Ok(Self(value))
46        }
47    }
48}