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        /// Description of the response. Response support markdown syntax.
122        pub description: String,
123
124        /// Map of headers identified by their name. `Content-Type` header will be ignored.
125        #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
126        pub headers: BTreeMap<String, Header>,
127
128        /// Map of response [`Content`] objects identified by response body content type e.g `application/json`.
129        ///
130        /// [`Content`]s are stored within [`IndexMap`] to retain their insertion order. Swagger UI
131        /// will create and show default example according to the first entry in `content` map.
132        #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
133        pub content: IndexMap<String, Content>,
134
135        /// Optional extensions "x-something".
136        #[serde(skip_serializing_if = "Option::is_none", flatten)]
137        pub extensions: Option<Extensions>,
138
139        /// A map of operations links that can be followed from the response. The key of the
140        /// map is a short name for the link.
141        #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
142        pub links: BTreeMap<String, RefOr<Link>>,
143    }
144}
145
146impl Response {
147    /// Construct a new [`Response`].
148    ///
149    /// Function takes description as argument.
150    pub fn new<S: Into<String>>(description: S) -> Self {
151        Self {
152            description: description.into(),
153            ..Default::default()
154        }
155    }
156}
157
158impl ResponseBuilder {
159    /// Add description. Description supports markdown syntax.
160    pub fn description<I: Into<String>>(mut self, description: I) -> Self {
161        set_value!(self description description.into())
162    }
163
164    /// Add [`Content`] of the [`Response`] with content type e.g `application/json`.
165    pub fn content<S: Into<String>>(mut self, content_type: S, content: Content) -> Self {
166        self.content.insert(content_type.into(), content);
167
168        self
169    }
170
171    /// Add response [`Header`].
172    pub fn header<S: Into<String>>(mut self, name: S, header: Header) -> Self {
173        self.headers.insert(name.into(), header);
174
175        self
176    }
177
178    /// Add openapi extensions (x-something) to the [`Header`].
179    pub fn extensions(mut self, extensions: Option<Extensions>) -> Self {
180        set_value!(self extensions extensions)
181    }
182
183    /// Add link that can be followed from the response.
184    pub fn link<S: Into<String>, L: Into<RefOr<Link>>>(mut self, name: S, link: L) -> Self {
185        self.links.insert(name.into(), link.into());
186
187        self
188    }
189}
190
191impl From<ResponseBuilder> for RefOr<Response> {
192    fn from(builder: ResponseBuilder) -> Self {
193        Self::T(builder.build())
194    }
195}
196
197impl From<Ref> for RefOr<Response> {
198    fn from(r: Ref) -> Self {
199        Self::Ref(r)
200    }
201}
202
203/// Trait with convenience functions for documenting response bodies.
204///
205/// With a single method call we can add [`Content`] to our [`ResponseBuilder`] and [`Response`]
206/// that references a [schema][schema] using content-type `"application/json"`.
207///
208/// _**Add json response from schema ref.**_
209/// ```rust
210/// use utoipa::openapi::response::{ResponseBuilder, ResponseExt};
211///
212/// let request = ResponseBuilder::new()
213///     .description("A sample response")
214///     .json_schema_ref("MyResponsePayload").build();
215/// ```
216///
217/// If serialized to JSON, the above will result in a response schema like this.
218/// ```json
219/// {
220///   "description": "A sample response",
221///   "content": {
222///     "application/json": {
223///       "schema": {
224///         "$ref": "#/components/schemas/MyResponsePayload"
225///       }
226///     }
227///   }
228/// }
229/// ```
230///
231/// [response]: crate::ToResponse
232/// [schema]: crate::ToSchema
233///
234#[cfg(feature = "openapi_extensions")]
235#[cfg_attr(doc_cfg, doc(cfg(feature = "openapi_extensions")))]
236pub trait ResponseExt {
237    /// Add [`Content`] to [`Response`] referring to a _`schema`_
238    /// with Content-Type `application/json`.
239    fn json_schema_ref(self, ref_name: &str) -> Self;
240}
241
242#[cfg(feature = "openapi_extensions")]
243impl ResponseExt for Response {
244    fn json_schema_ref(mut self, ref_name: &str) -> Response {
245        self.content.insert(
246            "application/json".to_string(),
247            Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name))),
248        );
249        self
250    }
251}
252
253#[cfg(feature = "openapi_extensions")]
254impl ResponseExt for ResponseBuilder {
255    fn json_schema_ref(self, ref_name: &str) -> ResponseBuilder {
256        self.content(
257            "application/json",
258            Content::new(Some(crate::openapi::Ref::from_schema_name(ref_name))),
259        )
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::{Content, ResponseBuilder, Responses};
266    use insta::assert_json_snapshot;
267
268    #[test]
269    fn responses_new() {
270        let responses = Responses::new();
271
272        assert!(responses.responses.is_empty());
273    }
274
275    #[test]
276    fn response_builder() {
277        let request_body = ResponseBuilder::new()
278            .description("A sample response")
279            .content(
280                "application/json",
281                Content::new(Some(crate::openapi::Ref::from_schema_name(
282                    "MySchemaPayload",
283                ))),
284            )
285            .build();
286        assert_json_snapshot!(request_body);
287    }
288}
289
290#[cfg(all(test, feature = "openapi_extensions"))]
291mod openapi_extensions_tests {
292    use crate::openapi::ResponseBuilder;
293    use insta::assert_json_snapshot;
294
295    use super::ResponseExt;
296
297    #[test]
298    fn response_ext() {
299        let request_body = ResponseBuilder::new()
300            .description("A sample response")
301            .build()
302            .json_schema_ref("MySchemaPayload");
303
304        assert_json_snapshot!(request_body);
305    }
306
307    #[test]
308    fn response_builder_ext() {
309        let request_body = ResponseBuilder::new()
310            .description("A sample response")
311            .json_schema_ref("MySchemaPayload")
312            .build();
313        assert_json_snapshot!(request_body);
314    }
315}