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 crate::{PropMap, Ref, RefOr};
10
11use super::link::Link;
12use super::{Content, header::Header};
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 [`Responses::default`].
77    #[must_use]
78    pub fn new() -> Self {
79        Default::default()
80    }
81    /// Inserts a key-value pair into the instance and returns `self`.
82    #[must_use]
83    pub fn response<S: Into<String>, R: Into<RefOr<Response>>>(
84        mut self,
85        key: S,
86        response: R,
87    ) -> Self {
88        self.insert(key, response);
89        self
90    }
91
92    /// Inserts a key-value pair into the instance.
93    pub fn insert<S: Into<String>, R: Into<RefOr<Response>>>(&mut self, key: S, response: R) {
94        self.0.insert(key.into(), response.into());
95    }
96
97    /// Moves all elements from `other` into `self`, leaving `other` empty.
98    ///
99    /// If a key from `other` is already present in `self`, the respective
100    /// value from `self` will be overwritten with the respective value from `other`.
101    pub fn append(&mut self, other: &mut Self) {
102        self.0.append(&mut other.0);
103    }
104
105    /// Add responses from an iterator over a pair of `(status_code, response): (String, Response)`.
106    pub fn extend<I, C, R>(&mut self, iter: I)
107    where
108        I: IntoIterator<Item = (C, R)>,
109        C: Into<String>,
110        R: Into<RefOr<Response>>,
111    {
112        self.0.extend(
113            iter.into_iter()
114                .map(|(key, response)| (key.into(), response.into())),
115        );
116    }
117}
118
119impl From<Responses> for PropMap<String, RefOr<Response>> {
120    fn from(responses: Responses) -> Self {
121        responses.0
122    }
123}
124
125impl<C, R> FromIterator<(C, R)> for Responses
126where
127    C: Into<String>,
128    R: Into<RefOr<Response>>,
129{
130    fn from_iter<T: IntoIterator<Item = (C, R)>>(iter: T) -> Self {
131        Self(PropMap::from_iter(
132            iter.into_iter()
133                .map(|(key, response)| (key.into(), response.into())),
134        ))
135    }
136}
137
138/// Implements [OpenAPI Response Object][response].
139///
140/// Response is api operation response.
141///
142/// [response]: https://spec.openapis.org/oas/latest.html#response-object
143#[non_exhaustive]
144#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
145#[serde(rename_all = "camelCase")]
146pub struct Response {
147    /// Description of the response. Response support markdown syntax.
148    pub description: String,
149
150    /// Map of headers identified by their name. `Content-Type` header will be ignored.
151    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
152    pub headers: PropMap<String, Header>,
153
154    /// Map of response [`Content`] objects identified by response body content type e.g `application/json`.
155    ///
156    /// [`Content`]s are stored within [`IndexMap`] to retain their insertion order. Swagger UI
157    /// will create and show default example according to the first entry in `content` map.
158    #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
159    #[serde(rename = "content")]
160    pub contents: IndexMap<String, Content>,
161
162    /// Optional extensions "x-something"
163    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
164    pub extensions: PropMap<String, serde_json::Value>,
165
166    /// A map of operations links that can be followed from the response. The key of the
167    /// map is a short name for the link.
168    #[serde(skip_serializing_if = "PropMap::is_empty", default)]
169    pub links: PropMap<String, RefOr<Link>>,
170}
171
172impl Response {
173    /// Construct a new [`Response`].
174    ///
175    /// Function takes description as argument.
176    #[must_use]
177    pub fn new<S: Into<String>>(description: S) -> Self {
178        Self {
179            description: description.into(),
180            ..Default::default()
181        }
182    }
183
184    /// Add description. Description supports markdown syntax.
185    #[must_use]
186    pub fn description<I: Into<String>>(mut self, description: I) -> Self {
187        self.description = description.into();
188        self
189    }
190
191    /// Add [`Content`] of the [`Response`] with content type e.g `application/json` and returns `Self`.
192    #[must_use]
193    pub fn add_content<S: Into<String>, C: Into<Content>>(mut self, key: S, content: C) -> Self {
194        self.contents.insert(key.into(), content.into());
195        self
196    }
197    /// Add response [`Header`] and returns `Self`.
198    #[must_use]
199    pub fn add_header<S: Into<String>>(mut self, name: S, header: Header) -> Self {
200        self.headers.insert(name.into(), header);
201        self
202    }
203
204    /// Add openapi extension (`x-something`) for [`Response`].
205    #[must_use]
206    pub fn add_extension<K: Into<String>>(mut self, key: K, value: serde_json::Value) -> Self {
207        self.extensions.insert(key.into(), value);
208        self
209    }
210
211    /// Add link that can be followed from the response.
212    #[must_use]
213    pub fn add_link<S: Into<String>, L: Into<RefOr<Link>>>(mut self, name: S, link: L) -> Self {
214        self.links.insert(name.into(), link.into());
215
216        self
217    }
218}
219
220impl From<Ref> for RefOr<Response> {
221    fn from(r: Ref) -> Self {
222        Self::Ref(r)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::{Content, Header, PropMap, Ref, RefOr, Response, Responses};
229    use assert_json_diff::assert_json_eq;
230    use serde_json::json;
231
232    #[test]
233    fn responses_new() {
234        let responses = Responses::new();
235        assert!(responses.is_empty());
236    }
237
238    #[test]
239    fn response_builder() -> Result<(), serde_json::Error> {
240        let request_body = Response::new("A sample response")
241            .description("A sample response description")
242            .add_content(
243                "application/json",
244                Content::new(Ref::from_schema_name("MySchemaPayload")),
245            )
246            .add_header(
247                "content-type",
248                Header::default().description("application/json"),
249            );
250
251        assert_json_eq!(
252            request_body,
253            json!({
254              "description": "A sample response description",
255              "content": {
256                "application/json": {
257                  "schema": {
258                    "$ref": "#/components/schemas/MySchemaPayload"
259                  }
260                }
261              },
262              "headers": {
263                "content-type": {
264                  "description": "application/json",
265                  "schema": {
266                    "type": "string"
267                  }
268                }
269              }
270            })
271        );
272        Ok(())
273    }
274
275    #[test]
276    fn test_responses_from_btree_map() {
277        let input = PropMap::from([
278            ("response1".to_owned(), Response::new("response1")),
279            ("response2".to_owned(), Response::new("response2")),
280        ]);
281
282        let expected = Responses(PropMap::from([
283            (
284                "response1".to_owned(),
285                RefOr::Type(Response::new("response1")),
286            ),
287            (
288                "response2".to_owned(),
289                RefOr::Type(Response::new("response2")),
290            ),
291        ]));
292
293        let actual = Responses::from(input);
294
295        assert_eq!(expected, actual);
296    }
297
298    #[test]
299    fn test_responses_from_kv_sequence() {
300        let input = [
301            ("response1".to_owned(), Response::new("response1")),
302            ("response2".to_owned(), Response::new("response2")),
303        ];
304
305        let expected = Responses(PropMap::from([
306            (
307                "response1".to_owned(),
308                RefOr::Type(Response::new("response1")),
309            ),
310            (
311                "response2".to_owned(),
312                RefOr::Type(Response::new("response2")),
313            ),
314        ]));
315
316        let actual = Responses::from(input);
317
318        assert_eq!(expected, actual);
319    }
320
321    #[test]
322    fn test_responses_from_iter() {
323        let input = [
324            ("response1".to_owned(), Response::new("response1")),
325            ("response2".to_owned(), Response::new("response2")),
326        ];
327
328        let expected = Responses(PropMap::from([
329            (
330                "response1".to_owned(),
331                RefOr::Type(Response::new("response1")),
332            ),
333            (
334                "response2".to_owned(),
335                RefOr::Type(Response::new("response2")),
336            ),
337        ]));
338
339        let actual = Responses::from_iter(input);
340
341        assert_eq!(expected, actual);
342    }
343
344    #[test]
345    fn test_responses_into_iter() {
346        let responses = Responses::new();
347        let responses = responses.response("response1", Response::new("response1"));
348        assert_eq!(1, responses.into_iter().collect::<Vec<_>>().len());
349    }
350
351    #[test]
352    fn test_btree_map_from_responses() {
353        let expected = PropMap::from([
354            (
355                "response1".to_owned(),
356                RefOr::Type(Response::new("response1")),
357            ),
358            (
359                "response2".to_owned(),
360                RefOr::Type(Response::new("response2")),
361            ),
362        ]);
363
364        let actual = PropMap::from(
365            Responses::new()
366                .response("response1", Response::new("response1"))
367                .response("response2", Response::new("response2")),
368        );
369        assert_eq!(expected, actual);
370    }
371}