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