actix_web_jsonschema/
lib.rs1mod 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}