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