roas/v3_0/
response.rs

1//! Response Object
2
3use 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/// A container for the expected responses of an operation.
17/// The container maps a HTTP response code to the expected response.
18///
19/// The documentation is not necessarily expected to cover all possible HTTP response codes
20/// because they may not be known in advance.
21/// However, documentation is expected to cover a successful operation response and any known errors.
22///
23/// The `default` MAY be used as a default response object for all HTTP codes that are
24/// not covered individually by the specification.
25///
26/// The `Responses Object` MUST contain at least one response code,
27/// and it SHOULD be the response for a successful operation call.
28///
29/// Specification example:
30/// ```yaml
31/// '200':
32///   description: a pet to be returned
33///   content:
34///     application/json:
35///       schema:
36///         $ref: '#/components/schemas/Pet'
37/// default:
38///   description: Unexpected error
39///   content:
40///     application/json:
41///       schema:
42///         $ref: '#/components/schemas/ErrorModel'
43/// ```
44#[derive(Clone, Debug, PartialEq, Default)]
45pub struct Responses {
46    /// The documentation of responses other than the ones declared for specific HTTP response codes.
47    /// Use this field to cover undeclared responses.
48    /// A Reference Object can link to a response that the OpenAPI Object’s components/responses
49    /// section defines.
50    pub default: Option<RefOr<Response>>,
51
52    /// Any HTTP status code can be used as the property name,
53    /// but only one property per code,
54    /// to describe the expected response for that HTTP status code.
55    /// A Reference Object can link to a response that is defined in the OpenAPI Object’s
56    /// components/responses section.
57    /// This field MUST be enclosed in quotation marks (for example, “200”) for compatibility
58    /// between JSON and YAML.
59    /// To define a range of response codes, this field MAY contain the uppercase wildcard character `X`.
60    /// For example, `2XX` represents all response codes between `[200-299]`.
61    /// Only the following range definitions are allowed: `1XX`, `2XX`, `3XX`, `4XX`, and `5XX`.
62    /// If a response is defined using an explicit code,
63    /// the explicit code definition takes precedence over the range definition for that code.
64    pub responses: Option<BTreeMap<String, RefOr<Response>>>,
65
66    /// Allows extensions to the Swagger Schema.
67    /// The field name MUST begin with `x-`, for example, `x-internal-id`.
68    /// The value can be null, a primitive, an array or an object.
69    pub extensions: Option<BTreeMap<String, serde_json::Value>>,
70}
71
72#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
73pub struct Response {
74    /// **Required** A short description of the response.
75    /// [CommonMark](https://spec.commonmark.org) syntax MAY be used for rich text representation.
76    #[serde(skip_serializing_if = "String::is_empty")]
77    pub description: String,
78
79    /// Maps a header name to its definition.
80    /// [RFC7230](https://www.rfc-editor.org/rfc/rfc7230) states header names are case insensitive.
81    /// If a response header is defined with the name `"Content-Type"`, it SHALL be ignored.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub headers: Option<BTreeMap<String, RefOr<Header>>>,
84
85    /// A map containing descriptions of potential response payloads.
86    /// The key is a media type or media type range and the value describes it.
87    /// For responses that match multiple keys, only the most specific key is applicable.
88    /// e.g. `text/plain` overrides `text/*`
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub content: Option<BTreeMap<String, MediaType>>,
91
92    /// Maps a header name to its definition.
93    /// [RFC7230](https://www.rfc-editor.org/rfc/rfc7230) states header names are case insensitive.
94    /// If a response header is defined with the name `"Content-Type"`, it SHALL be ignored.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub links: Option<BTreeMap<String, RefOr<Link>>>,
97
98    /// A map of operations links that can be followed from the response.
99    /// The key of the map is a short name for the link,
100    /// following the naming constraints of the names for Component Objects.
101    #[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}