salvo_oapi/openapi/
path.rs

1//! Implements [OpenAPI Path Object][paths] types.
2//!
3//! [paths]: https://spec.openapis.org/oas/latest.html#paths-object
4use std::iter;
5use std::ops::{Deref, DerefMut};
6
7use serde::{Deserialize, Serialize};
8
9use super::{Operation, Operations, Parameter, Parameters, PathMap, PropMap, Server, Servers};
10
11/// Implements [OpenAPI Path Object][paths] types.
12///
13/// [paths]: https://spec.openapis.org/oas/latest.html#paths-object
14#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
15pub struct Paths(PathMap<String, PathItem>);
16impl Deref for Paths {
17    type Target = PathMap<String, PathItem>;
18
19    fn deref(&self) -> &Self::Target {
20        &self.0
21    }
22}
23impl DerefMut for Paths {
24    fn deref_mut(&mut self) -> &mut Self::Target {
25        &mut self.0
26    }
27}
28impl Paths {
29    /// Construct a new empty [`Paths`]. This is effectively same as calling [`Paths::default`].
30    #[must_use]
31    pub fn new() -> Self {
32        Default::default()
33    }
34    /// Inserts a key-value pair into the instance and returns `self`.
35    #[must_use]
36    pub fn path<K: Into<String>, V: Into<PathItem>>(mut self, key: K, value: V) -> Self {
37        self.insert(key, value);
38        self
39    }
40    /// Inserts a key-value pair into the instance.
41    pub fn insert<K: Into<String>, V: Into<PathItem>>(&mut self, key: K, value: V) {
42        let key = key.into();
43        let mut value = value.into();
44        self.0
45            .entry(key)
46            .and_modify(|item| {
47                if value.summary.is_some() {
48                    item.summary = value.summary.take();
49                }
50                if value.description.is_some() {
51                    item.description = value.description.take();
52                }
53                item.servers.append(&mut value.servers);
54                item.parameters.append(&mut value.parameters);
55                item.operations.append(&mut value.operations);
56            })
57            .or_insert(value);
58    }
59    /// Moves all elements from `other` into `self`, leaving `other` empty.
60    ///
61    /// If a key from `other` is already present in `self`, the respective
62    /// value from `self` will be overwritten with the respective value from `other`.
63    pub fn append(&mut self, other: &mut Self) {
64        let items = std::mem::take(&mut other.0);
65        for item in items {
66            self.insert(item.0, item.1);
67        }
68    }
69    /// Extends a collection with the contents of an iterator.
70    pub fn extend<I, K, V>(&mut self, iter: I)
71    where
72        I: IntoIterator<Item = (K, V)>,
73        K: Into<String>,
74        V: Into<PathItem>,
75    {
76        for (k, v) in iter.into_iter() {
77            self.insert(k, v);
78        }
79    }
80}
81
82/// Implements [OpenAPI Path Item Object][path_item] what describes [`Operation`]s available on
83/// a single path.
84///
85/// [path_item]: https://spec.openapis.org/oas/latest.html#path-item-object
86#[non_exhaustive]
87#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
88#[serde(rename_all = "camelCase")]
89pub struct PathItem {
90    /// Optional summary intended to apply all operations in this [`PathItem`].
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub summary: Option<String>,
93
94    /// Optional description intended to apply all operations in this [`PathItem`].
95    /// Description supports markdown syntax.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub description: Option<String>,
98
99    /// Alternative [`Server`] array to serve all [`Operation`]s in this [`PathItem`] overriding
100    /// the global server array.
101    #[serde(skip_serializing_if = "Servers::is_empty")]
102    pub servers: Servers,
103
104    /// List of [`Parameter`]s common to all [`Operation`]s in this [`PathItem`]. Parameters cannot
105    /// contain duplicate parameters. They can be overridden in [`Operation`] level but cannot be
106    /// removed there.
107    #[serde(skip_serializing_if = "Parameters::is_empty")]
108    #[serde(flatten)]
109    pub parameters: Parameters,
110
111    /// Map of operations in this [`PathItem`]. Operations can hold only one operation
112    /// per [`PathItemType`].
113    #[serde(flatten)]
114    pub operations: Operations,
115}
116
117impl PathItem {
118    /// Construct a new [`PathItem`] with provided [`Operation`] mapped to given [`PathItemType`].
119    pub fn new<O: Into<Operation>>(path_item_type: PathItemType, operation: O) -> Self {
120        let operations = PropMap::from_iter(iter::once((path_item_type, operation.into())));
121
122        Self {
123            operations: Operations(operations),
124            ..Default::default()
125        }
126    }
127    /// Moves all elements from `other` into `self`, leaving `other` empty.
128    ///
129    /// If a key from `other` is already present in `self`, the respective
130    /// value from `self` will be overwritten with the respective value from `other`.
131    pub fn append(&mut self, other: &mut Self) {
132        self.operations.append(&mut other.operations);
133        self.servers.append(&mut other.servers);
134        self.parameters.append(&mut other.parameters);
135        if other.description.is_some() {
136            self.description = other.description.take();
137        }
138        if other.summary.is_some() {
139            self.summary = other.summary.take();
140        }
141    }
142
143    /// Append a new [`Operation`] by [`PathItemType`] to this [`PathItem`]. Operations can
144    /// hold only one operation per [`PathItemType`].
145    #[must_use]
146    pub fn add_operation<O: Into<Operation>>(
147        mut self,
148        path_item_type: PathItemType,
149        operation: O,
150    ) -> Self {
151        self.operations.insert(path_item_type, operation.into());
152        self
153    }
154
155    /// Add or change summary intended to apply all operations in this [`PathItem`].
156    #[must_use]
157    pub fn summary<S: Into<String>>(mut self, summary: S) -> Self {
158        self.summary = Some(summary.into());
159        self
160    }
161
162    /// Add or change optional description intended to apply all operations in this [`PathItem`].
163    /// Description supports markdown syntax.
164    #[must_use]
165    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
166        self.description = Some(description.into());
167        self
168    }
169
170    /// Add list of alternative [`Server`]s to serve all [`Operation`]s in this [`PathItem`] overriding
171    /// the global server array.
172    #[must_use]
173    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: I) -> Self {
174        self.servers = Servers(servers.into_iter().collect());
175        self
176    }
177
178    /// Append list of [`Parameter`]s common to all [`Operation`]s to this [`PathItem`].
179    #[must_use]
180    pub fn parameters<I: IntoIterator<Item = Parameter>>(mut self, parameters: I) -> Self {
181        self.parameters = Parameters(parameters.into_iter().collect());
182        self
183    }
184}
185
186/// Path item operation type.
187#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy, Debug)]
188#[serde(rename_all = "lowercase")]
189pub enum PathItemType {
190    /// Type mapping for HTTP _GET_ request.
191    Get,
192    /// Type mapping for HTTP _POST_ request.
193    Post,
194    /// Type mapping for HTTP _PUT_ request.
195    Put,
196    /// Type mapping for HTTP _DELETE_ request.
197    Delete,
198    /// Type mapping for HTTP _OPTIONS_ request.
199    Options,
200    /// Type mapping for HTTP _HEAD_ request.
201    Head,
202    /// Type mapping for HTTP _PATCH_ request.
203    Patch,
204    /// Type mapping for HTTP _TRACE_ request.
205    Trace,
206    /// Type mapping for HTTP _CONNECT_ request.
207    Connect,
208}
209
210#[cfg(test)]
211mod tests {
212    use assert_json_diff::assert_json_eq;
213    use serde_json::json;
214
215    use super::*;
216    use crate::oapi::response::Response;
217
218    #[test]
219    fn test_build_path_item() {
220        let path_item = PathItem::new(PathItemType::Get, Operation::new())
221            .summary("summary")
222            .description("description")
223            .servers(Servers::new())
224            .parameters(Parameters::new());
225
226        assert_json_eq!(
227            path_item,
228            json!({
229                "description": "description",
230                "summary": "summary",
231                "get": {
232                    "responses": {}
233                }
234            })
235        )
236    }
237
238    #[test]
239    fn test_path_item_append() {
240        let mut path_item = PathItem::new(
241            PathItemType::Get,
242            Operation::new().add_response("200", Response::new("Get success")),
243        );
244        let mut other_path_item = PathItem::new(
245            PathItemType::Post,
246            Operation::new().add_response("200", Response::new("Post success")),
247        )
248        .description("description")
249        .summary("summary");
250        path_item.append(&mut other_path_item);
251
252        assert_json_eq!(
253            path_item,
254            json!({
255                "description": "description",
256                "summary": "summary",
257                "get": {
258                    "responses": {
259                        "200": {
260                            "description": "Get success"
261                        }
262                    }
263                },
264                "post": {
265                    "responses": {
266                        "200": {
267                            "description": "Post success"
268                        }
269                    }
270                }
271            })
272        )
273    }
274
275    #[test]
276    fn test_path_item_add_operation() {
277        let path_item = PathItem::new(
278            PathItemType::Get,
279            Operation::new().add_response("200", Response::new("Get success")),
280        )
281        .add_operation(
282            PathItemType::Post,
283            Operation::new().add_response("200", Response::new("Post success")),
284        );
285
286        assert_json_eq!(
287            path_item,
288            json!({
289                "get": {
290                    "responses": {
291                        "200": {
292                            "description": "Get success"
293                        }
294                    }
295                },
296                "post": {
297                    "responses": {
298                        "200": {
299                            "description": "Post success"
300                        }
301                    }
302                }
303            })
304        )
305    }
306
307    #[test]
308    fn test_paths_extend() {
309        let mut paths = Paths::new().path(
310            "/api/do_something",
311            PathItem::new(
312                PathItemType::Get,
313                Operation::new().add_response("200", Response::new("Get success")),
314            ),
315        );
316        paths.extend([(
317            "/api/do_something",
318            PathItem::new(
319                PathItemType::Post,
320                Operation::new().add_response("200", Response::new("Post success")),
321            )
322            .summary("summary")
323            .description("description"),
324        )]);
325
326        assert_json_eq!(
327            paths,
328            json!({
329                "/api/do_something": {
330                    "description": "description",
331                    "summary": "summary",
332                    "get": {
333                        "responses": {
334                            "200": {
335                                "description": "Get success"
336                            }
337                        }
338                    },
339                    "post": {
340                        "responses": {
341                            "200": {
342                                "description": "Post success"
343                            }
344                        }
345                    }
346                }
347            })
348        );
349    }
350
351    #[test]
352    fn test_paths_deref() {
353        let paths = Paths::new();
354        assert_eq!(0, paths.len());
355    }
356}