1use crate::common::helpers::{Context, PushError, ValidateWithContext, validate_required_string};
4use crate::common::reference::RefOr;
5use crate::v3_0::header::Header;
6use crate::v3_0::link::Link;
7use crate::v3_0::media_type::MediaType;
8use crate::v3_0::spec::Spec;
9use crate::validation::Options;
10use serde::de::{Error, MapAccess, Visitor};
11use serde::ser::SerializeMap;
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13use std::collections::BTreeMap;
14use std::fmt;
15
16#[derive(Clone, Debug, PartialEq, Default)]
45pub struct Responses {
46 pub default: Option<RefOr<Response>>,
51
52 pub responses: Option<BTreeMap<String, RefOr<Response>>>,
65
66 pub extensions: Option<BTreeMap<String, serde_json::Value>>,
70}
71
72#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
73pub struct Response {
74 #[serde(skip_serializing_if = "String::is_empty")]
77 pub description: String,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
83 pub headers: Option<BTreeMap<String, RefOr<Header>>>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
90 pub content: Option<BTreeMap<String, MediaType>>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
96 pub links: Option<BTreeMap<String, RefOr<Link>>>,
97
98 #[serde(flatten)]
102 #[serde(with = "crate::common::extensions")]
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub extensions: Option<BTreeMap<String, serde_json::Value>>,
105}
106
107impl Serialize for Responses {
108 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
109 where
110 S: Serializer,
111 {
112 let mut map = serializer.serialize_map(None)?;
113
114 if let Some(ref default) = self.default {
115 map.serialize_entry("default", default)?;
116 }
117
118 if let Some(ref responses) = self.responses {
119 for (k, v) in responses {
120 map.serialize_entry(&k, &v)?;
121 }
122 }
123
124 if let Some(ref ext) = self.extensions {
125 for (k, v) in ext {
126 if k.starts_with("x-") {
127 map.serialize_entry(&k, &v)?;
128 }
129 }
130 }
131
132 map.end()
133 }
134}
135
136impl<'de> Deserialize<'de> for Responses {
137 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
138 where
139 D: Deserializer<'de>,
140 {
141 const FIELDS: &[&str] = &["default", "x-...", "1xx", "2xx", "3xx", "4xx", "5xx"];
142
143 struct ResponsesVisitor;
144
145 impl<'de> Visitor<'de> for ResponsesVisitor {
146 type Value = Responses;
147
148 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
149 formatter.write_str("struct Responses")
150 }
151
152 fn visit_map<V>(self, mut map: V) -> Result<Responses, V::Error>
153 where
154 V: MapAccess<'de>,
155 {
156 let mut res = Responses::default();
157 let mut responses: BTreeMap<String, RefOr<Response>> = BTreeMap::new();
158 let mut extensions: BTreeMap<String, serde_json::Value> = BTreeMap::new();
159 while let Some(key) = map.next_key::<String>()? {
160 if key == "default" {
161 if res.default.is_some() {
162 return Err(Error::duplicate_field("default"));
163 }
164 res.default = Some(map.next_value()?);
165 } else if key.starts_with("x-") {
166 if extensions.contains_key(key.as_str()) {
167 return Err(Error::custom(format_args!("duplicate field `{key}`")));
168 }
169 extensions.insert(key, map.next_value()?);
170 } else {
171 match key.parse::<u16>() {
172 Ok(100..=599) => {
173 if responses.contains_key(key.as_str()) {
174 return Err(Error::custom(format_args!(
175 "duplicate field `{key}`"
176 )));
177 }
178 responses.insert(key, map.next_value()?);
179 }
180 _ => return Err(Error::unknown_field(key.as_str(), FIELDS)),
181 }
182 }
183 }
184 if !responses.is_empty() {
185 res.responses = Some(responses);
186 }
187 if !extensions.is_empty() {
188 res.extensions = Some(extensions);
189 }
190 Ok(res)
191 }
192 }
193
194 deserializer.deserialize_struct("Responses", FIELDS, ResponsesVisitor)
195 }
196}
197
198impl ValidateWithContext<Spec> for Response {
199 fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
200 if !ctx.is_option(Options::IgnoreEmptyResponseDescription) {
201 validate_required_string(&self.description, ctx, format!("{path}.description"));
202 }
203 if let Some(headers) = &self.headers {
204 for (name, header) in headers {
205 header.validate_with_context(ctx, format!("{path}.headers[{name}]"));
206 }
207 }
208 if let Some(media_types) = &self.content {
209 for (name, media_type) in media_types {
210 media_type.validate_with_context(ctx, format!("{path}.mediaTypes[{name}]"));
211 }
212 }
213 if let Some(links) = &self.links {
214 for (name, link) in links {
215 link.validate_with_context(ctx, format!("{path}.links[{name}]"));
216 }
217 }
218 }
219}
220
221impl ValidateWithContext<Spec> for Responses {
222 fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
223 if let Some(response) = &self.default {
224 response.validate_with_context(ctx, format!("{path}.default"));
225 }
226 if let Some(responses) = &self.responses {
227 for (name, response) in responses {
228 match name.parse::<u16>() {
229 Ok(100..=599) => {}
230 _ => {
231 ctx.error(
232 path.clone(),
233 format_args!(
234 "name must be an integer within [100..599] range, found `{name}`"
235 ),
236 );
237 }
238 }
239 response.validate_with_context(ctx, format!("{path}.{name}"));
240 }
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::v3_0::parameter::InHeaderStyle;
249 use crate::v3_0::schema::{ObjectSchema, Schema, SingleSchema};
250
251 #[test]
252 fn test_response_deserialize() {
253 assert_eq!(
254 serde_json::from_value::<Response>(serde_json::json!({
255 "description": "A simple response",
256 "headers": {
257 "Authorization": {
258 "description": "A short description of the header.",
259 "style": "simple",
260 "required": true,
261 },
262 },
263 "content": {
264 "application/json": {
265 "schema": {
266 "type": "object",
267 "title": "foo"
268 }
269 }
270 },
271 "links": {
272 "next": {
273 "operationRef": "getNextPage",
274 "description": "Get the next page of results"
275 }
276 },
277 "x-extra": "extension",
278 }))
279 .unwrap(),
280 Response {
281 description: "A simple response".to_owned(),
282 headers: Some({
283 let mut map = BTreeMap::new();
284 map.insert(
285 "Authorization".to_owned(),
286 RefOr::new_item(Header {
287 description: Some("A short description of the header.".to_owned()),
288 required: Some(true),
289 style: Some(InHeaderStyle::Simple),
290 ..Default::default()
291 }),
292 );
293 map
294 }),
295 content: Some({
296 let mut map = BTreeMap::new();
297 map.insert(
298 "application/json".to_owned(),
299 MediaType {
300 schema: Some(RefOr::new_item(Schema::Single(Box::new(
301 SingleSchema::Object(ObjectSchema {
302 title: Some("foo".to_owned()),
303 ..Default::default()
304 }),
305 )))),
306 ..Default::default()
307 },
308 );
309 map
310 }),
311 links: Some({
312 let mut map = BTreeMap::new();
313 map.insert(
314 "next".to_owned(),
315 RefOr::new_item(Link {
316 operation_ref: Some("getNextPage".to_owned()),
317 description: Some("Get the next page of results".to_owned()),
318 ..Default::default()
319 }),
320 );
321 map
322 }),
323 extensions: Some({
324 let mut map = BTreeMap::new();
325 map.insert("x-extra".to_owned(), serde_json::json!("extension"));
326 map
327 }),
328 },
329 "response deserialization",
330 );
331 }
332
333 #[test]
334 fn test_response_serialization() {
335 assert_eq!(
336 serde_json::to_value(Response {
337 description: "A simple response".to_owned(),
338 headers: Some({
339 let mut map = BTreeMap::new();
340 map.insert(
341 "Authorization".to_owned(),
342 RefOr::new_item(Header {
343 description: Some("A short description of the header.".to_owned()),
344 required: Some(true),
345 style: Some(InHeaderStyle::Simple),
346 ..Default::default()
347 }),
348 );
349 map
350 }),
351 content: Some({
352 let mut map = BTreeMap::new();
353 map.insert(
354 "application/json".to_owned(),
355 MediaType {
356 schema: Some(RefOr::new_item(Schema::Single(Box::new(
357 SingleSchema::Object(ObjectSchema {
358 title: Some("foo".to_owned()),
359 ..Default::default()
360 }),
361 )))),
362 ..Default::default()
363 },
364 );
365 map
366 }),
367 links: Some({
368 let mut map = BTreeMap::new();
369 map.insert(
370 "next".to_owned(),
371 RefOr::new_item(Link {
372 operation_ref: Some("getNextPage".to_owned()),
373 description: Some("Get the next page of results".to_owned()),
374 ..Default::default()
375 }),
376 );
377 map
378 }),
379 extensions: Some({
380 let mut map = BTreeMap::new();
381 map.insert("x-extra".to_owned(), serde_json::json!("extension"));
382 map
383 }),
384 })
385 .unwrap(),
386 serde_json::json!({
387 "description": "A simple response",
388 "headers": {
389 "Authorization": {
390 "description": "A short description of the header.",
391 "style": "simple",
392 "required": true,
393 },
394 },
395 "content": {
396 "application/json": {
397 "schema": {
398 "type": "object",
399 "title": "foo"
400 }
401 }
402 },
403 "links": {
404 "next": {
405 "operationRef": "getNextPage",
406 "description": "Get the next page of results"
407 }
408 },
409 "x-extra": "extension",
410 }),
411 "response serialization",
412 );
413 }
414
415 #[test]
416 fn test_responses_deserialize() {
417 assert_eq!(
418 serde_json::from_value::<Responses>(serde_json::json!({
419 "default": {
420 "description": "A simple response",
421 "headers": {
422 "Authorization": {
423 "description": "A short description of the header.",
424 "style": "simple",
425 "required": true,
426 },
427 },
428 "content": {
429 "application/json": {
430 "schema": {
431 "type": "object",
432 "title": "foo"
433 }
434 }
435 },
436 "links": {
437 "next": {
438 "operationRef": "getNextPage",
439 "description": "Get the next page of results"
440 }
441 },
442 "x-extra": "extension",
443 },
444 "200": {
445 "description": "200 OK"
446 },
447 "x-extra": "extension",
448 }))
449 .unwrap(),
450 Responses {
451 default: Some(RefOr::new_item(Response {
452 description: "A simple response".to_owned(),
453 headers: Some({
454 let mut map = BTreeMap::new();
455 map.insert(
456 "Authorization".to_owned(),
457 RefOr::new_item(Header {
458 description: Some("A short description of the header.".to_owned()),
459 required: Some(true),
460 style: Some(InHeaderStyle::Simple),
461 ..Default::default()
462 }),
463 );
464 map
465 }),
466 content: Some({
467 let mut map = BTreeMap::new();
468 map.insert(
469 "application/json".to_owned(),
470 MediaType {
471 schema: Some(RefOr::new_item(Schema::Single(Box::new(
472 SingleSchema::Object(ObjectSchema {
473 title: Some("foo".to_owned()),
474 ..Default::default()
475 }),
476 )))),
477 ..Default::default()
478 },
479 );
480 map
481 }),
482 links: Some({
483 let mut map = BTreeMap::new();
484 map.insert(
485 "next".to_owned(),
486 RefOr::new_item(Link {
487 operation_ref: Some("getNextPage".to_owned()),
488 description: Some("Get the next page of results".to_owned()),
489 ..Default::default()
490 }),
491 );
492 map
493 }),
494 extensions: Some({
495 let mut map = BTreeMap::new();
496 map.insert("x-extra".to_owned(), serde_json::json!("extension"));
497 map
498 }),
499 })),
500 responses: Some({
501 let mut map = BTreeMap::new();
502 map.insert(
503 "200".to_owned(),
504 RefOr::new_item(Response {
505 description: "200 OK".to_owned(),
506 ..Default::default()
507 }),
508 );
509 map
510 }),
511 extensions: Some({
512 let mut map = BTreeMap::new();
513 map.insert("x-extra".to_owned(), serde_json::json!("extension"));
514 map
515 }),
516 },
517 "responses deserialization",
518 );
519 }
520
521 #[test]
522 fn test_responses_serialization() {
523 assert_eq!(
524 serde_json::to_value(Responses {
525 default: Some(RefOr::new_item(Response {
526 description: "A simple response".to_owned(),
527 headers: Some({
528 let mut map = BTreeMap::new();
529 map.insert(
530 "Authorization".to_owned(),
531 RefOr::new_item(Header {
532 description: Some("A short description of the header.".to_owned()),
533 required: Some(true),
534 style: Some(InHeaderStyle::Simple),
535 ..Default::default()
536 }),
537 );
538 map
539 }),
540 content: Some({
541 let mut map = BTreeMap::new();
542 map.insert(
543 "application/json".to_owned(),
544 MediaType {
545 schema: Some(RefOr::new_item(Schema::Single(Box::new(
546 SingleSchema::Object(ObjectSchema {
547 title: Some("foo".to_owned()),
548 ..Default::default()
549 }),
550 )))),
551 ..Default::default()
552 },
553 );
554 map
555 }),
556 links: Some({
557 let mut map = BTreeMap::new();
558 map.insert(
559 "next".to_owned(),
560 RefOr::new_item(Link {
561 operation_ref: Some("getNextPage".to_owned()),
562 description: Some("Get the next page of results".to_owned()),
563 ..Default::default()
564 }),
565 );
566 map
567 }),
568 extensions: Some({
569 let mut map = BTreeMap::new();
570 map.insert("x-extra".to_owned(), serde_json::json!("extension"));
571 map
572 }),
573 })),
574 responses: Some({
575 let mut map = BTreeMap::new();
576 map.insert(
577 "200".to_owned(),
578 RefOr::new_item(Response {
579 description: "200 OK".to_owned(),
580 ..Default::default()
581 }),
582 );
583 map
584 }),
585 extensions: Some({
586 let mut map = BTreeMap::new();
587 map.insert("x-extra".to_owned(), serde_json::json!("extension"));
588 map
589 }),
590 })
591 .unwrap(),
592 serde_json::json!({
593 "default": {
594 "description": "A simple response",
595 "headers": {
596 "Authorization": {
597 "description": "A short description of the header.",
598 "style": "simple",
599 "required": true,
600 },
601 },
602 "content": {
603 "application/json": {
604 "schema": {
605 "type": "object",
606 "title": "foo"
607 }
608 }
609 },
610 "links": {
611 "next": {
612 "operationRef": "getNextPage",
613 "description": "Get the next page of results"
614 }
615 },
616 "x-extra": "extension",
617 },
618 "200": {
619 "description": "200 OK"
620 },
621 "x-extra": "extension",
622 }),
623 "response serialization",
624 );
625 }
626
627 #[test]
628 fn test_response_validate() {
629 let spec = Spec::default();
630
631 let mut ctx = Context::new(&spec, Options::new());
632 Response {
633 description: "A simple response".to_owned(),
634 headers: Some({
635 let mut map = BTreeMap::new();
636 map.insert(
637 "Authorization".to_owned(),
638 RefOr::new_item(Header {
639 description: Some("A short description of the header.".to_owned()),
640 required: Some(true),
641 style: Some(InHeaderStyle::Simple),
642 ..Default::default()
643 }),
644 );
645 map
646 }),
647 content: Some({
648 let mut map = BTreeMap::new();
649 map.insert(
650 "application/json".to_owned(),
651 MediaType {
652 schema: Some(RefOr::new_item(Schema::Single(Box::new(
653 SingleSchema::Object(ObjectSchema {
654 title: Some("foo".to_owned()),
655 ..Default::default()
656 }),
657 )))),
658 ..Default::default()
659 },
660 );
661 map
662 }),
663 links: Some({
664 let mut map = BTreeMap::new();
665 map.insert(
666 "next".to_owned(),
667 RefOr::new_item(Link {
668 operation_ref: Some("getNextPage".to_owned()),
669 description: Some("Get the next page of results".to_owned()),
670 ..Default::default()
671 }),
672 );
673 map
674 }),
675 extensions: Some({
676 let mut map = BTreeMap::new();
677 map.insert("x-extra".to_owned(), serde_json::json!("extension"));
678 map
679 }),
680 }
681 .validate_with_context(&mut ctx, "response".to_owned());
682 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
683
684 let mut ctx = Context::new(&spec, Options::new());
685 Response {
686 description: "A simple response".to_owned(),
687 ..Default::default()
688 }
689 .validate_with_context(&mut ctx, "response".to_owned());
690 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
691
692 let mut ctx = Context::new(&spec, Options::new());
693 Response::default().validate_with_context(&mut ctx, "response".to_owned());
694 assert!(
695 ctx.errors
696 .contains(&"response.description: must not be empty".to_string()),
697 "expected error: {:?}",
698 ctx.errors
699 );
700
701 let mut ctx = Context::new(
702 &spec,
703 Options::only(&Options::IgnoreEmptyResponseDescription),
704 );
705 Response::default().validate_with_context(&mut ctx, "response".to_owned());
706 assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
707 }
708}