Skip to main content

tork_core/extract/
valid.rs

1//! Validating request-body extractor.
2
3use garde::Validate;
4use serde::de::DeserializeOwned;
5
6use crate::error::{Error, Result};
7use crate::extract::body::{
8    configured_body_limit, ensure_json_depth_within_limit, read_body_capped_with,
9};
10use crate::extract::{FromRequest, RequestContext};
11
12/// Deserializes the JSON request body into `T` and validates it.
13///
14/// `T` is usually an `#[api_model]` type. Deserialization failures produce a
15/// `422 Unprocessable Entity`, and validation failures produce a `422` whose
16/// body lists the offending fields (see [`Error::from_garde_report`]).
17///
18/// Access the inner value via the `.0` field or [`Valid::into_inner`].
19#[derive(Debug, Clone)]
20pub struct Valid<T>(pub T);
21
22impl<T> Valid<T> {
23    /// Unwraps the validated value.
24    pub fn into_inner(self) -> T {
25        self.0
26    }
27}
28
29impl<T> FromRequest for Valid<T>
30where
31    T: DeserializeOwned + Validate<Context = ()> + Send,
32{
33    fn from_request(
34        ctx: &RequestContext,
35    ) -> impl std::future::Future<Output = Result<Self>> + Send {
36        let taken = ctx.take_body();
37        let limit = configured_body_limit(ctx);
38        async move {
39            let bytes = read_body_capped_with(taken?, limit).await?;
40            ensure_json_depth_within_limit(&bytes)?;
41            let value: T = serde_json::from_slice(&bytes)
42                .map_err(|_| Error::unprocessable("request body is not valid JSON"))?;
43            value.validate().map_err(Error::from_garde_report)?;
44            Ok(Valid(value))
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::body::box_body;
53    use crate::error::ErrorKind;
54    use crate::extract::PathParams;
55    use crate::state::StateMap;
56
57    use bytes::Bytes;
58    use http_body_util::Full;
59    use serde::Deserialize;
60    use std::sync::Arc;
61
62    #[derive(Debug, Deserialize, garde::Validate)]
63    struct Sample {
64        #[garde(range(min = 1))]
65        count: i64,
66    }
67
68    fn context_with_body(json: &str) -> RequestContext {
69        let head = http::Request::new(()).into_parts().0;
70        let body = box_body(Full::new(Bytes::copy_from_slice(json.as_bytes())));
71        RequestContext::new(head, PathParams::new(), Arc::new(StateMap::new()), body)
72    }
73
74    #[tokio::test]
75    async fn valid_body_is_accepted() {
76        let ctx = context_with_body(r#"{"count": 5}"#);
77        let valid = <Valid<Sample> as FromRequest>::from_request(&ctx)
78            .await
79            .expect("should validate");
80        assert_eq!(valid.into_inner().count, 5);
81    }
82
83    #[tokio::test]
84    async fn invalid_body_is_unprocessable_with_details() {
85        let ctx = context_with_body(r#"{"count": 0}"#);
86        let error = <Valid<Sample> as FromRequest>::from_request(&ctx)
87            .await
88            .unwrap_err();
89        assert_eq!(error.kind(), ErrorKind::Unprocessable);
90        assert!(
91            !error.details().is_empty(),
92            "should report the failing field"
93        );
94    }
95
96    #[tokio::test]
97    async fn deeply_nested_body_is_rejected_before_validation() {
98        let json = format!(
99            "{}0{}",
100            "{\"count\":".to_owned() + &"[".repeat(crate::extract::body::MAX_JSON_NESTING + 1),
101            "]".repeat(crate::extract::body::MAX_JSON_NESTING + 1) + "}"
102        );
103        let ctx = context_with_body(&json);
104        let error = <Valid<Sample> as FromRequest>::from_request(&ctx)
105            .await
106            .unwrap_err();
107        assert_eq!(error.kind(), ErrorKind::BadRequest);
108        assert_eq!(error.message(), "request body is too deeply nested");
109    }
110}