Skip to main content

salvo_oapi/openapi/
response.rs

1//! Implements [OpenApi Responses][responses].
2//!
3//! [responses]: https://spec.openapis.org/oas/latest.html#responses-object
4use std::ops::{Deref, DerefMut};
5
6use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8
9use super::Content;
10use super::header::Header;
11use super::link::Link;
12use crate::{PropMap, Ref, RefOr};
13
14/// Implements [OpenAPI Responses Object][responses].
15///
16/// Responses is a map holding api operation responses identified by their status code.
17///
18/// [responses]: https://spec.openapis.org/oas/latest.html#responses-object
19#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
20#[serde(rename_all = "camelCase")]
21pub struct Responses(PropMap<String, RefOr<Response>>);
22
23impl<K, R> From<PropMap<K, R>> for Responses
24where
25    K: Into<String>,
26    R: Into<RefOr<Response>>,
27{
28    fn from(inner: PropMap<K, R>) -> Self {
29        Self(
30            inner
31                .into_iter()
32                .map(|(k, v)| (k.into(), v.into()))
33                .collect(),
34        )
35    }
36}
37impl<K, R, const N: usize> From<[(K, R); N]> for Responses
38where
39    K: Into<String>,
40    R: Into<RefOr<Response>>,
41{
42    fn from(inner: [(K, R); N]) -> Self {
43        Self(
44            <[(K, R)]>::into_vec(Box::new(inner))
45                .into_iter()
46                .map(|(k, v)| (k.into(), v.into()))
47                .collect(),
48        )
49    }
50}
51
52impl Deref for Responses {
53    type Target = PropMap<String, RefOr<Response>>;
54
55    fn deref(&self) -> &Self::Target {
56        &self.0
57    }
58}
59
60impl DerefMut for Responses {
61    fn deref_mut(&mut self) -> &mut Self::Target {
62        &mut self.0
63    }
64}
65
66impl IntoIterator for Responses {
67    type Item = (String, RefOr<Response>);
68    type IntoIter = <PropMap<String, RefOr<Response>> as IntoIterator>::IntoIter;
69
70    fn into_iter(self) -> Self::IntoIter {
71        self.0.into_iter()
72    }
73}
74
75impl Responses {
76    /// Construct a new empty [`Responses`]. This is effectively same as calling
77    /// [`Responses::default`].
78    #[must_use]
79    pub fn new() -> Self {
80        Default::default()
81    }
82    /// Inserts a key-value pair into the instance and returns `self`.
83    #[must_use]
84    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
85        mut self,
86        key: S,
87        response: R,
88    ) -> Self {
89        self.insert(key, response);
90        self
91    }
92
93    /// Inserts a key-value pair into the instance.
94    pub fn insert<S: Into<String>, R: Into<RefOr<Response>>>(&mut self, key: S, response: R) {
95        self.0.insert(key.into(), response.into());
96    }
97
98    /// Moves all elements from `other` into `self`, leaving `other` empty.
99    ///
100    /// If a key from `other` is already present in `self`, the respective
101    /// value from `self` will be overwritten with the respective value from `other`.
102    pub fn append(&mut self, other: &mut Self) {
103        self.0.append(&mut other.0);
104    }
105
106    /// Add responses from an iterator over a pair of `(status_code, response): (String, Response)`.
107    pub fn extend<I, C, R>(&mut self, iter: I)
108    where
109        I: IntoIterator<Item = (C, R)>,
110        C: Into<String>,
111        R: Into<RefOr<Response>>,
112    {
113        self.0.extend(
114            iter.into_iter()
115                .map(|(key, response)| (key.into(), response.into())),
116        );
117    }
118}
119
120impl From<Responses> for PropMap<String, RefOr<Response>> {
121    fn from(responses: Responses) -> Self {
122        responses.0
123    }
124}
125
126impl<C, R> FromIterator<(C, R)> for Responses
127where
128    C: Into<String>,
129    R: Into<RefOr<Response>>,
130{
131    fn from_iter<T: IntoIterator<Item = (C, R)>>(iter: T) -> Self {
132        Self(PropMap::from_iter(
133            iter.into_iter()
134                .map(|(key, response)| (key.into(), response.into())),
135        ))
136    }
137}
138
139/// Implements [OpenAPI Response Object][response].
140///
141/// Response is api operation response.
142///
143/// [response]: https://spec.openapis.org/oas/latest.html#response-object
144#[non_exhaustive]
145#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
146#[serde(rename_all = "camelCase")]
147pub struct Response {
148    /// Short summary of the meaning of the response. Added in OpenAPI 3.2.
149    ///
150    /// See <https://spec.openapis.org/oas/v3.2.0.html#response-object>.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub summary: Option<String>,
153
154    /// Description of the response. Response support markdown syntax.
155    ///
156    /// Required in OpenAPI 3.1 and optional as of OpenAPI 3.2, so documents that omit it
157    /// deserialize into an empty `String`. It is always serialized — an empty description is
158    /// valid under both versions, whereas omitting it would be invalid 3.1.
159    #[serde(default)]
160    pub description: String,
161
162    /// Map of headers identified by their name. `Content-Type` header will be ignored.
163    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
164    pub headers: PropMap<String, Header>,
165
166    /// Map of response [`Content`] objects identified by response body content type e.g
167    /// `application/json`.
168    ///
169    /// [`Content`]s are stored within [`IndexMap`] to retain their insertion order. Swagger UI
170    /// will create and show default example according to the first entry in `content` map.
171    #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
172    #[serde(rename = "content")]
173    pub contents: IndexMap<String, Content>,
174
175    /// Optional extensions "x-something"
176    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
177    pub extensions: PropMap<String, serde_json::Value>,
178
179    /// A map of operations links that can be followed from the response. The key of the
180    /// map is a short name for the link.
181    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
182    pub links: PropMap<String, RefOr<Link>>,
183}
184
185impl Response {
186    /// Construct a new [`Response`].
187    ///
188    /// Function takes description as argument.
189    #[must_use]
190    pub fn new<S: Into<String>>(description: S) -> Self {
191        Self {
192            description: description.into(),
193            ..Default::default()
194        }
195    }
196
197    /// Add description. Description supports markdown syntax.
198    #[must_use]
199    pub fn description<I: Into<String>>(mut self, description: I) -> Self {
200        self.description = description.into();
201        self
202    }
203
204    /// Add a short summary of the meaning of the response. Requires OpenAPI 3.2.
205    #[must_use]
206    pub fn summary<I: Into<String>>(mut self, summary: I) -> Self {
207        self.summary = Some(summary.into());
208        self
209    }
210
211    /// Add [`Content`] of the [`Response`] with content type e.g `application/json` and returns
212    /// `Self`.
213    #[must_use]
214    pub fn add_content<S: Into<String>, C: Into<Content>>(mut self, key: S, content: C) -> Self {
215        self.contents.insert(key.into(), content.into());
216        self
217    }
218    /// Add response [`Header`] and returns `Self`.
219    #[must_use]
220    pub fn add_header<S: Into<String>>(mut self, name: S, header: Header) -> Self {
221        self.headers.insert(name.into(), header);
222        self
223    }
224
225    /// Add openapi extension (`x-something`) for [`Response`].
226    #[must_use]
227    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
228        self.extensions.insert(key.into(), value);
229        self
230    }
231
232    /// Add link that can be followed from the response.
233    #[must_use]
234    pub fn add_link<S: Into<String>, L: Into<RefOr<Link>>>(mut self, name: S, link: L) -> Self {
235        self.links.insert(name.into(), link.into());
236
237        self
238    }
239}
240
241impl From<Ref> for RefOr<Response> {
242    fn from(r: Ref) -> Self {
243        Self::Ref(r)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use assert_json_diff::assert_json_eq;
250    use serde_json::json;
251
252    use super::{Content, Header, PropMap, Ref, RefOr, Response, Responses};
253
254    #[test]
255    fn responses_new() {
256        let responses = Responses::new();
257        assert!(responses.is_empty());
258    }
259
260    #[test]
261    fn response_builder() -> Result<(), serde_json::Error> {
262        let request_body = Response::new("A sample response")
263            .description("A sample response description")
264            .add_content(
265                "application/json",
266                Content::new(Ref::from_schema_name("MySchemaPayload")),
267            )
268            .add_header(
269                "content-type",
270                Header::default().description("application/json"),
271            );
272
273        assert_json_eq!(
274            request_body,
275            json!({
276              "description": "A sample response description",
277              "content": {
278                "application/json": {
279                  "schema": {
280                    "$ref": "#/components/schemas/MySchemaPayload"
281                  }
282                }
283              },
284              "headers": {
285                "content-type": {
286                  "description": "application/json",
287                  "schema": {
288                    "type": "string"
289                  }
290                }
291              }
292            })
293        );
294        Ok(())
295    }
296
297    #[test]
298    fn response_summary_serializes_and_description_is_optional_on_input() {
299        let response = Response::new("A sample response").summary("Sample");
300        assert_json_eq!(
301            response,
302            json!({ "summary": "Sample", "description": "A sample response" })
303        );
304
305        // OpenAPI 3.2 makes `description` optional; such a document must still parse.
306        let parsed: Response =
307            serde_json::from_value(json!({ "summary": "Sample" })).expect("deserialize");
308        assert_eq!(parsed.summary.as_deref(), Some("Sample"));
309        assert_eq!(parsed.description, "");
310    }
311
312    #[test]
313    fn test_responses_from_btree_map() {
314        let input = PropMap::from([
315            ("response1".to_owned(), Response::new("response1")),
316            ("response2".to_owned(), Response::new("response2")),
317        ]);
318
319        let expected = Responses(PropMap::from([
320            (
321                "response1".to_owned(),
322                RefOr::Type(Response::new("response1")),
323            ),
324            (
325                "response2".to_owned(),
326                RefOr::Type(Response::new("response2")),
327            ),
328        ]));
329
330        let actual = Responses::from(input);
331
332        assert_eq!(expected, actual);
333    }
334
335    #[test]
336    fn test_responses_from_kv_sequence() {
337        let input = [
338            ("response1".to_owned(), Response::new("response1")),
339            ("response2".to_owned(), Response::new("response2")),
340        ];
341
342        let expected = Responses(PropMap::from([
343            (
344                "response1".to_owned(),
345                RefOr::Type(Response::new("response1")),
346            ),
347            (
348                "response2".to_owned(),
349                RefOr::Type(Response::new("response2")),
350            ),
351        ]));
352
353        let actual = Responses::from(input);
354
355        assert_eq!(expected, actual);
356    }
357
358    #[test]
359    fn test_responses_from_iter() {
360        let input = [
361            ("response1".to_owned(), Response::new("response1")),
362            ("response2".to_owned(), Response::new("response2")),
363        ];
364
365        let expected = Responses(PropMap::from([
366            (
367                "response1".to_owned(),
368                RefOr::Type(Response::new("response1")),
369            ),
370            (
371                "response2".to_owned(),
372                RefOr::Type(Response::new("response2")),
373            ),
374        ]));
375
376        let actual = Responses::from_iter(input);
377
378        assert_eq!(expected, actual);
379    }
380
381    #[test]
382    fn test_responses_into_iter() {
383        let responses = Responses::new();
384        let responses = responses.response("response1", Response::new("response1"));
385        assert_eq!(1, responses.into_iter().collect::<Vec<_>>().len());
386    }
387
388    #[test]
389    fn test_btree_map_from_responses() {
390        let expected = PropMap::from([
391            (
392                "response1".to_owned(),
393                RefOr::Type(Response::new("response1")),
394            ),
395            (
396                "response2".to_owned(),
397                RefOr::Type(Response::new("response2")),
398            ),
399        ]);
400
401        let actual = PropMap::from(
402            Responses::new()
403                .response("response1", Response::new("response1"))
404                .response("response2", Response::new("response2")),
405        );
406        assert_eq!(expected, actual);
407    }
408}