Skip to main content

utoipa/openapi/
response.rs

1//! Implements [OpenApi Responses][responses].
2//!
3//! [responses]: https://spec.openapis.org/oas/latest.html#responses-object
4use std::collections::BTreeMap;
5
6use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8
9use crate::openapi::{Ref, RefOr};
10use crate::IntoResponses;
11
12use super::extensions::Extensions;
13use super::link::Link;
14use super::{builder, header::Header, set_value, Content};
15
16builder! {
17    ResponsesBuilder;
18
19    /// Implements [OpenAPI Responses Object][responses].
20    ///
21    /// Responses is a map holding api operation responses identified by their status code.
22    ///
23    /// [responses]: https://spec.openapis.org/oas/latest.html#responses-object
24    #[non_exhaustive]
25    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
26    #[cfg_attr(feature = "debug", derive(Debug))]
27    #[serde(rename_all = "camelCase")]
28    pub struct Responses {
29        /// Map containing status code as a key with represented response as a value.
30        #[serde(flatten)]
31        pub responses: BTreeMap<String, RefOr<Response>>,
32
33        /// Optional extensions "x-something".
34        #[serde(skip_serializing_if = "Option::is_none", flatten)]
35        pub extensions: Option<Extensions>,
36    }
37}
38
39impl Responses {
40    /// Construct a new [`Responses`].
41    pub fn new() -> Self {
42        Default::default()
43    }
44}
45
46impl ResponsesBuilder {
47    /// Add a [`Response`].
48    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
49        mut self,
50        code: S,
51        response: R,
52    ) -> Self {
53        self.responses.insert(code.into(), response.into());
54
55        self
56    }
57
58    /// Add responses from an iterator over a pair of `(status_code, response): (String, Response)`.
59    pub fn responses_from_iter<
60        I: IntoIterator<Item = (C, R)>,
61        C: Into<String>,
62        R: Into<RefOr<Response>>,
63    >(
64        mut self,
65        iter: I,
66    ) -> Self {
67        self.responses.extend(
68            iter.into_iter()
69                .map(|(code, response)| (code.into(), response.into())),
70        );
71        self
72    }
73
74    /// Add responses from a type that implements [`IntoResponses`].
75    pub fn responses_from_into_responses<I: IntoResponses>(mut self) -> Self {
76        self.responses.extend(I::responses());
77        self
78    }
79
80    /// Add openapi extensions (x-something) of the API.
81    pub fn extensions(mut self, extensions: Option<Extensions>) -> Self {
82        set_value!(self extensions extensions)
83    }
84}
85
86impl From<Responses> for BTreeMap<String, RefOr<Response>> {
87    fn from(responses: Responses) -> Self {
88        responses.responses
89    }
90}
91
92impl<C, R> FromIterator<(C, R)> for Responses
93where
94    C: Into<String>,
95    R: Into<RefOr<Response>>,
96{
97    fn from_iter<T: IntoIterator<Item = (C, R)>>(iter: T) -> Self {
98        Self {
99            responses: BTreeMap::from_iter(
100                iter.into_iter()
101                    .map(|(code, response)| (code.into(), response.into())),
102            ),
103            ..Default::default()
104        }
105    }
106}
107
108builder! {
109    ResponseBuilder;
110
111    /// Implements [OpenAPI Response Object][response].
112    ///
113    /// Response is api operation response.
114    ///
115    /// [response]: https://spec.openapis.org/oas/latest.html#response-object
116    #[non_exhaustive]
117    #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
118    #[cfg_attr(feature = "debug", derive(Debug))]
119    #[serde(rename_all = "camelCase")]
120    pub struct Response {
121        /// Short summary of the response.
122        #[serde(skip_serializing_if = "Option::is_none")]
123        pub summary: Option<String>,
124
125        /// Description of the response. Response support markdown syntax.
126        #[serde(skip_serializing_if = "String::is_empty", default)]
127        pub description: String,
128
129        /// Map of headers identified by their name. `Content-Type` header will be ignored.
130        #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
131        pub headers: BTreeMap<String, RefOr<Header>>,
132
133        /// Map of response [`Content`] objects identified by response body content type e.g `application/json`.
134        ///
135        /// [`Content`]s are stored within [`IndexMap`] to retain their insertion order. Swagger UI
136        /// will create and show default example according to the first entry in `content` map.
137        #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
138        pub content: IndexMap<String, RefOr<Content>>,
139
140        /// Optional extensions "x-something".
141        #[serde(skip_serializing_if = "Option::is_none", flatten)]
142        pub extensions: Option<Extensions>,
143
144        /// A map of operations links that can be followed from the response. The key of the
145        /// map is a short name for the link.
146        #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
147        pub links: BTreeMap<String, RefOr<Link>>,
148    }
149}
150
151impl Response {
152    /// Construct a new [`Response`].
153    ///
154    /// Function takes description as argument.
155    pub fn new<S: Into<String>>(description: S) -> Self {
156        Self {
157            description: description.into(),
158            ..Default::default()
159        }
160    }
161}
162
163impl ResponseBuilder {
164    /// Add description. Description supports markdown syntax.
165    pub fn description<I: Into<String>>(mut self, description: I) -> Self {
166        set_value!(self description description.into())
167    }
168
169    /// Add short summary of the response.
170    pub fn summary<I: Into<String>>(mut self, summary: Option<I>) -> Self {
171        set_value!(self summary summary.map(Into::into))
172    }
173
174    /// Add [`Content`] of the [`Response`] with content type e.g `application/json`.
175    pub fn content<S: Into<String>, C: Into<Content>>(
176        mut self,
177        content_type: S,
178        content: C,
179    ) -> Self {
180        self.content
181            .insert(content_type.into(), content.into().into());
182
183        self
184    }
185
186    /// Add reusable [`Content`] reference by content type e.g `application/json`.
187    pub fn content_ref<S: Into<String>>(mut self, content_type: S, content_ref: Ref) -> Self {
188        self.content
189            .insert(content_type.into(), RefOr::Ref(content_ref));
190
191        self
192    }
193
194    /// Add response [`Header`].
195    pub fn header<S: Into<String>>(mut self, name: S, header: Header) -> Self {
196        self.headers.insert(name.into(), header.into());
197
198        self
199    }
200
201    /// Add response [`Header`] reference.
202    pub fn header_ref<S: Into<String>>(mut self, name: S, header: Ref) -> Self {
203        self.headers.insert(name.into(), RefOr::Ref(header));
204
205        self
206    }
207
208    /// Add openapi extensions (x-something) to the [`Header`].
209    pub fn extensions(mut self, extensions: Option<Extensions>) -> Self {
210        set_value!(self extensions extensions)
211    }
212
213    /// Add link that can be followed from the response.
214    pub fn link<S: Into<String>, L: Into<RefOr<Link>>>(mut self, name: S, link: L) -> Self {
215        self.links.insert(name.into(), link.into());
216
217        self
218    }
219}
220
221impl From<ResponseBuilder> for RefOr<Response> {
222    fn from(builder: ResponseBuilder) -> Self {
223        Self::T(builder.build())
224    }
225}
226
227impl From<Ref> for RefOr<Response> {
228    fn from(r: Ref) -> Self {
229        Self::Ref(r)
230    }
231}
232
233/// Trait with convenience functions for documenting response bodies.
234///
235/// With a single method call we can add [`Content`] to our [`ResponseBuilder`] and [`Response`]
236/// that references a [schema][schema] using content-type `"application/json"`.
237///
238/// _**Add json response from schema ref.**_
239/// ```rust
240/// use utoipa::openapi::response::{ResponseBuilder, ResponseExt};
241///
242/// let request = ResponseBuilder::new()
243///     .description("A sample response")
244///     .json_schema_ref("MyResponsePayload").build();
245/// ```
246///
247/// If serialized to JSON, the above will result in a response schema like this.
248/// ```json
249/// {
250///   "description": "A sample response",
251///   "content": {
252///     "application/json": {
253///       "schema": {
254///         "$ref": "#/components/schemas/MyResponsePayload"
255///       }
256///     }
257///   }
258/// }
259/// ```
260///
261/// [response]: crate::ToResponse
262/// [schema]: crate::ToSchema
263///
264#[cfg(feature = "openapi_extensions")]
265#[cfg_attr(doc_cfg, doc(cfg(feature = "openapi_extensions")))]
266pub trait ResponseExt {
267    /// Add [`Content`] to [`Response`] referring to a _`schema`_
268    /// with Content-Type `application/json`.
269    fn json_schema_ref(self, ref_name: &str) -> Self;
270}
271
272#[cfg(feature = "openapi_extensions")]
273impl ResponseExt for Response {
274    fn json_schema_ref(mut self, ref_name: &str) -> Response {
275        self.content.insert(
276            "application/json".to_string(),
277            Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name))).into(),
278        );
279        self
280    }
281}
282
283#[cfg(feature = "openapi_extensions")]
284impl ResponseExt for ResponseBuilder {
285    fn json_schema_ref(self, ref_name: &str) -> ResponseBuilder {
286        self.content(
287            "application/json",
288            Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name))),
289        )
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::{Content, ResponseBuilder, Responses};
296    use insta::assert_json_snapshot;
297
298    #[test]
299    fn responses_new() {
300        let responses = Responses::new();
301
302        assert!(responses.responses.is_empty());
303    }
304
305    #[test]
306    fn response_builder() {
307        let request_body = ResponseBuilder::new()
308            .description("A sample response")
309            .content(
310                "application/json",
311                Content::new(Some(crate::openapi::Ref::from_schema_name(
312                    "MySchemaPayload",
313                ))),
314            )
315            .build();
316        assert_json_snapshot!(request_body);
317    }
318}
319
320#[cfg(all(test, feature = "openapi_extensions"))]
321mod openapi_extensions_tests {
322    use crate::openapi::ResponseBuilder;
323    use insta::assert_json_snapshot;
324
325    use super::ResponseExt;
326
327    #[test]
328    fn response_ext() {
329        let request_body = ResponseBuilder::new()
330            .description("A sample response")
331            .build()
332            .json_schema_ref("MySchemaPayload");
333
334        assert_json_snapshot!(request_body);
335    }
336
337    #[test]
338    fn response_builder_ext() {
339        let request_body = ResponseBuilder::new()
340            .description("A sample response")
341            .json_schema_ref("MySchemaPayload")
342            .build();
343        assert_json_snapshot!(request_body);
344    }
345}