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    /// Description of the response. Response support markdown syntax.
149    pub description: String,
150
151    /// Map of headers identified by their name. `Content-Type` header will be ignored.
152    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
153    pub headers: PropMap<String, Header>,
154
155    /// Map of response [`Content`] objects identified by response body content type e.g
156    /// `application/json`.
157    ///
158    /// [`Content`]s are stored within [`IndexMap`] to retain their insertion order. Swagger UI
159    /// will create and show default example according to the first entry in `content` map.
160    #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
161    #[serde(rename = "content")]
162    pub contents: IndexMap<String, Content>,
163
164    /// Optional extensions "x-something"
165    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
166    pub extensions: PropMap<String, serde_json::Value>,
167
168    /// A map of operations links that can be followed from the response. The key of the
169    /// map is a short name for the link.
170    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
171    pub links: PropMap<String, RefOr<Link>>,
172}
173
174impl Response {
175    /// Construct a new [`Response`].
176    ///
177    /// Function takes description as argument.
178    #[must_use]
179    pub fn new<S: Into<String>>(description: S) -> Self {
180        Self {
181            description: description.into(),
182            ..Default::default()
183        }
184    }
185
186    /// Add description. Description supports markdown syntax.
187    #[must_use]
188    pub fn description<I: Into<String>>(mut self, description: I) -> Self {
189        self.description = description.into();
190        self
191    }
192
193    /// Add [`Content`] of the [`Response`] with content type e.g `application/json` and returns
194    /// `Self`.
195    #[must_use]
196    pub fn add_content<S: Into<String>, C: Into<Content>>(mut self, key: S, content: C) -> Self {
197        self.contents.insert(key.into(), content.into());
198        self
199    }
200    /// Add response [`Header`] and returns `Self`.
201    #[must_use]
202    pub fn add_header<S: Into<String>>(mut self, name: S, header: Header) -> Self {
203        self.headers.insert(name.into(), header);
204        self
205    }
206
207    /// Add openapi extension (`x-something`) for [`Response`].
208    #[must_use]
209    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
210        self.extensions.insert(key.into(), value);
211        self
212    }
213
214    /// Add link that can be followed from the response.
215    #[must_use]
216    pub fn add_link<S: Into<String>, L: Into<RefOr<Link>>>(mut self, name: S, link: L) -> Self {
217        self.links.insert(name.into(), link.into());
218
219        self
220    }
221}
222
223impl From<Ref> for RefOr<Response> {
224    fn from(r: Ref) -> Self {
225        Self::Ref(r)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use assert_json_diff::assert_json_eq;
232    use serde_json::json;
233
234    use super::{Content, Header, PropMap, Ref, RefOr, Response, Responses};
235
236    #[test]
237    fn responses_new() {
238        let responses = Responses::new();
239        assert!(responses.is_empty());
240    }
241
242    #[test]
243    fn response_builder() -> Result<(), serde_json::Error> {
244        let request_body = Response::new("A sample response")
245            .description("A sample response description")
246            .add_content(
247                "application/json",
248                Content::new(Ref::from_schema_name("MySchemaPayload")),
249            )
250            .add_header(
251                "content-type",
252                Header::default().description("application/json"),
253            );
254
255        assert_json_eq!(
256            request_body,
257            json!({
258              "description": "A sample response description",
259              "content": {
260                "application/json": {
261                  "schema": {
262                    "$ref": "#/components/schemas/MySchemaPayload"
263                  }
264                }
265              },
266              "headers": {
267                "content-type": {
268                  "description": "application/json",
269                  "schema": {
270                    "type": "string"
271                  }
272                }
273              }
274            })
275        );
276        Ok(())
277    }
278
279    #[test]
280    fn test_responses_from_btree_map() {
281        let input = PropMap::from([
282            ("response1".to_owned(), Response::new("response1")),
283            ("response2".to_owned(), Response::new("response2")),
284        ]);
285
286        let expected = Responses(PropMap::from([
287            (
288                "response1".to_owned(),
289                RefOr::Type(Response::new("response1")),
290            ),
291            (
292                "response2".to_owned(),
293                RefOr::Type(Response::new("response2")),
294            ),
295        ]));
296
297        let actual = Responses::from(input);
298
299        assert_eq!(expected, actual);
300    }
301
302    #[test]
303    fn test_responses_from_kv_sequence() {
304        let input = [
305            ("response1".to_owned(), Response::new("response1")),
306            ("response2".to_owned(), Response::new("response2")),
307        ];
308
309        let expected = Responses(PropMap::from([
310            (
311                "response1".to_owned(),
312                RefOr::Type(Response::new("response1")),
313            ),
314            (
315                "response2".to_owned(),
316                RefOr::Type(Response::new("response2")),
317            ),
318        ]));
319
320        let actual = Responses::from(input);
321
322        assert_eq!(expected, actual);
323    }
324
325    #[test]
326    fn test_responses_from_iter() {
327        let input = [
328            ("response1".to_owned(), Response::new("response1")),
329            ("response2".to_owned(), Response::new("response2")),
330        ];
331
332        let expected = Responses(PropMap::from([
333            (
334                "response1".to_owned(),
335                RefOr::Type(Response::new("response1")),
336            ),
337            (
338                "response2".to_owned(),
339                RefOr::Type(Response::new("response2")),
340            ),
341        ]));
342
343        let actual = Responses::from_iter(input);
344
345        assert_eq!(expected, actual);
346    }
347
348    #[test]
349    fn test_responses_into_iter() {
350        let responses = Responses::new();
351        let responses = responses.response("response1", Response::new("response1"));
352        assert_eq!(1, responses.into_iter().collect::<Vec<_>>().len());
353    }
354
355    #[test]
356    fn test_btree_map_from_responses() {
357        let expected = PropMap::from([
358            (
359                "response1".to_owned(),
360                RefOr::Type(Response::new("response1")),
361            ),
362            (
363                "response2".to_owned(),
364                RefOr::Type(Response::new("response2")),
365            ),
366        ]);
367
368        let actual = PropMap::from(
369            Responses::new()
370                .response("response1", Response::new("response1"))
371                .response("response2", Response::new("response2")),
372        );
373        assert_eq!(expected, actual);
374    }
375}