actix_web_jsonschema/
lib.rs

1//! [![Latest Version](https://img.shields.io/crates/v/actix-web-jsonschema.svg?color=green&style=flat-square)](https://crates.io/crates/actix-web-jsonschema)
2//! [![Documentation](https://docs.rs/actix-web-jsonschema/badge.svg)](https://docs.rs/actix-web-jsonschema)
3//! [![GitHub license](https://badgen.net/github/license/Naereen/Strapdown.js?style=flat-square)](https://github.com/Naereen/StrapDown.js/blob/master/LICENSE)
4//!
5//! This crate is a Rust library for providing validation mechanism
6//! to [actix-web](https://github.com/actix/actix-web) with [jsonschema](https://github.com/Stranger6667/jsonschema-rs) crate.
7//!
8//! More information about this crate can be found in the [crate documentation](https://docs.rs/actix-web-jsonschema).
9//!
10//! ## Installation
11//!
12//! This crate works with Cargo and can be found on [crates.io](https://crates.io/crates/actix-web-jsonschema) with a Cargo.toml like:
13//!
14//! ```toml
15//! [dependencies]
16//! actix-web = { version = "4", features = ["macros"] }
17//! actix-web-jsonschema = { version = "1", features = ["validator"] }
18//! serde = { version = "1", features = ["derive"] }
19//! schemars = { version = "0.8" }
20//! validator = { version = "0.16", features = ["derive"] }
21//! ```
22//!
23//! ## Feature Flags
24//!
25//! - `validator` - provides [validator](https://github.com/Keats/validator) validation.
26//! - `qs_query` - provides [QsQuery](https://docs.rs/actix-web-jsonschema/latest/actix_web_jsonschema/struct.QsQuery.html) extractor.
27//!
28//! ## Supported extractors
29//!
30//! | actix_web                                                                                      | actix_web_jsonschema                                                                                                  |
31//! | :--------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------- |
32//! | [actix_web::web::Path](https://docs.rs/actix-web/latest/actix_web/web/struct.Path.html)        | [actix_web_jsonschema::Path](https://docs.rs/actix-web-jsonschema/latest/actix_web_jsonschema/struct.Path.html)       |
33//! | [actix_web::web::Query](https://docs.rs/actix-web/latest/actix_web/web/struct.Query.html)      | [actix_web_jsonschema::Query](https://docs.rs/actix-web-jsonschema/latest/actix_web_jsonschema/struct.Query.html)     |
34//! | [actix_web::web::Form](https://docs.rs/actix-web/latest/actix_web/web/struct.Form.html)        | [actix_web_jsonschema::Form](https://docs.rs/actix-web-jsonschema/latest/actix_web_jsonschema/struct.Form.html)       |
35//! | [actix_web::web::Json](https://docs.rs/actix-web/latest/actix_web/web/struct.Json.html)        | [actix_web_jsonschema::Json](https://docs.rs/actix-web-jsonschema/latest/actix_web_jsonschema/struct.Json.html)       |
36//! | [serde_qs::actix::QsQuery](https://docs.rs/serde_qs/latest/serde_qs/actix/struct.QsQuery.html) | [actix_web_jsonschema::QsQuery](https://docs.rs/actix-web-jsonschema/latest/actix_web_jsonschema/struct.QsQuery.html) |
37//!
38//! ## Example
39//!
40//! ```rust
41//! use actix_web::{web, App};
42//! use serde::Deserialize;
43//! use schemars::JsonSchema;
44//! use validator::Validate;
45//! use actix_web_jsonschema::Query;
46//!
47//! #[derive(Deserialize, JsonSchema, Validate)]
48//! struct Request {
49//!     #[validate(length(min = 1, max = 20))]
50//!     name: String,
51//! }
52//!
53//! async fn index(Query(Request{ name }): Query<Request>) -> String {
54//!     format!("Hello, {name}!")
55//! }
56//!
57//! fn main() {
58//!     let app = App::new().service(
59//!         web::resource("/hello").route(web::get().to(index))); // <- use `Query` extractor
60//! }
61//! ```
62//!
63
64mod error;
65mod macros;
66mod schema;
67
68use futures::FutureExt;
69
70pub use error::Error;
71use macros::jsonschema_extractor;
72
73jsonschema_extractor! {
74    #[doc = "Extract typed information from the request’s path."]
75    #[derive(Debug, AsRef, Deref, DerefMut, From, FromRequest)]
76    pub struct Path<T>(pub T);
77}
78
79jsonschema_extractor! {
80    #[doc = "Extract and validate typed information from the request’s query."]
81    #[derive(Debug, AsRef, Deref, DerefMut, From, FromRequest)]
82    pub struct Query<T>(pub T);
83}
84
85jsonschema_extractor! {
86    #[doc = "Form can be used for extracting typed information and validation from request’s form data."]
87    #[derive(Debug, AsRef, Deref, DerefMut, From, FromRequest)]
88    pub struct Form<T>(pub T);
89}
90
91jsonschema_extractor! {
92    #[doc = "Json can be used for exstracting typed information and validation from request’s payload."]
93    #[derive(Debug, AsRef, Deref, DerefMut, From, FromRequest, Responder)]
94    pub struct Json<T>(pub T);
95}
96
97#[cfg(feature = "qs_query")]
98mod qs_query {
99    use super::*;
100
101    mod actix_web {
102        pub use actix_web::*;
103
104        pub mod web {
105            pub use serde_qs::actix::QsQuery;
106        }
107    }
108
109    jsonschema_extractor! {
110        #[doc = "Extract and validate typed information from the request’s query ([serde_qs](https://crates.io/crates/serde_qs) based)."]
111        #[derive(Debug, AsRef, Deref, DerefMut, From, FromRequest)]
112        pub struct QsQuery<T>(pub T);
113    }
114}
115#[cfg(feature = "qs_query")]
116pub use qs_query::QsQuery;
117
118#[cfg(test)]
119mod test {
120    use actix_web::http::StatusCode;
121    use actix_web::{
122        body::to_bytes, dev::ServiceResponse, http::header::ContentType, test, web, App,
123    };
124    use schemars::JsonSchema;
125    use serde::{Deserialize, Serialize};
126    use serde_json::json;
127
128    async fn json_body(
129        response: ServiceResponse,
130    ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
131        let body = to_bytes(response.into_body()).await?;
132        let value = serde_json::from_str::<serde_json::Value>(std::str::from_utf8(&body)?)?;
133
134        Ok(value)
135    }
136
137    #[cfg(not(feature = "validator"))]
138    mod default_tests {
139        use super::*;
140
141        #[derive(Debug, Serialize, Deserialize, JsonSchema)]
142        struct Request {
143            name: String,
144        }
145
146        #[derive(Debug, Serialize, JsonSchema)]
147        struct Response {
148            name: String,
149        }
150
151        async fn index(
152            crate::Json(Request { name }): crate::Json<Request>,
153        ) -> crate::Json<Response> {
154            crate::Json(Response { name })
155        }
156
157        #[actix_web::test]
158        async fn test_request_ok() {
159            let app = test::init_service(App::new().route("/", web::get().to(index))).await;
160            let request = test::TestRequest::default()
161                .insert_header(ContentType::json())
162                .set_json(Request {
163                    name: "taro".to_string(),
164                })
165                .to_request();
166            let response = test::call_service(&app, request).await;
167
168            assert!(response.status().is_success());
169        }
170
171        #[actix_web::test]
172        async fn test_required_key_err() -> Result<(), Box<dyn std::error::Error>> {
173            let app = test::init_service(App::new().route("/", web::get().to(index))).await;
174            let request = test::TestRequest::default()
175                .insert_header(ContentType::json())
176                .set_json(json!({}))
177                .to_request();
178            let response = test::call_service(&app, request).await;
179
180            assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
181            assert_eq!(
182                json_body(response).await?,
183                json!([
184                    {
185                        "error": "\"name\" is a required property",
186                        "instanceLocation": "",
187                        "keywordLocation": "/required"
188                    }
189                ])
190            );
191
192            Ok(())
193        }
194
195        #[actix_web::test]
196        async fn test_wrong_type_err() -> Result<(), Box<dyn std::error::Error>> {
197            let app = test::init_service(App::new().route("/", web::get().to(index))).await;
198            let request = test::TestRequest::default()
199                .insert_header(ContentType::json())
200                .set_json(json!({"name": 0}))
201                .to_request();
202            let response = test::call_service(&app, request).await;
203
204            assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
205            assert_eq!(
206                json_body(response).await?,
207                json!([
208                    {
209                        "error": "0 is not of type \"string\"",
210                        "instanceLocation": "/name",
211                        "keywordLocation": "/properties/name/type"
212                    }
213                ])
214            );
215
216            Ok(())
217        }
218    }
219
220    #[cfg(feature = "validator")]
221    mod validator_tests {
222        use super::*;
223        use validator::Validate;
224
225        #[derive(Debug, Serialize, Deserialize, JsonSchema, Validate)]
226        struct Request {
227            #[validate(length(min = 1, max = 5))]
228            name: String,
229        }
230
231        #[derive(Debug, Serialize, JsonSchema)]
232        struct Response {
233            name: String,
234        }
235
236        async fn index(
237            crate::Json(Request { name }): crate::Json<Request>,
238        ) -> crate::Json<Response> {
239            crate::Json(Response { name })
240        }
241
242        #[actix_web::test]
243        async fn test_request_ok() {
244            let app = test::init_service(App::new().route("/", web::get().to(index))).await;
245            let request = test::TestRequest::default()
246                .insert_header(ContentType::json())
247                .set_json(Request {
248                    name: "taro".to_string(),
249                })
250                .to_request();
251
252            let response = test::call_service(&app, request).await;
253
254            assert!(response.status().is_success());
255        }
256
257        #[actix_web::test]
258        async fn test_validation_error() -> Result<(), Box<dyn std::error::Error>> {
259            let app = test::init_service(App::new().route("/", web::get().to(index))).await;
260            let request = test::TestRequest::default()
261                .insert_header(ContentType::json())
262                .set_json(Request {
263                    name: "kojiro".to_string(),
264                })
265                .to_request();
266
267            let response = test::call_service(&app, request).await;
268
269            assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
270            assert_eq!(
271                json_body(response).await?,
272                json!([
273                    {
274                        "error": "\"kojiro\" is longer than 5 characters",
275                        "instanceLocation": "/name",
276                        "keywordLocation": "/properties/name/maxLength"
277                    }
278                ])
279            );
280
281            Ok(())
282        }
283    }
284}