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