Skip to main content

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    /// Optional extensions "x-something"
117    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
118    pub extensions: PropMap<String, serde_json::Value>,
119}
120
121impl PathItem {
122    /// Construct a new [`PathItem`] with provided [`Operation`] mapped to given [`PathItemType`].
123    pub fn new<O: Into<Operation>>(path_item_type: PathItemType, operation: O) -> Self {
124        let operations = PropMap::from_iter(iter::once((path_item_type, operation.into())));
125
126        Self {
127            operations: Operations(operations),
128            ..Default::default()
129        }
130    }
131    /// Moves all elements from `other` into `self`, leaving `other` empty.
132    ///
133    /// If a key from `other` is already present in `self`, the respective
134    /// value from `self` will be overwritten with the respective value from `other`.
135    pub fn append(&mut self, other: &mut Self) {
136        self.operations.append(&mut other.operations);
137        self.servers.append(&mut other.servers);
138        self.parameters.append(&mut other.parameters);
139        if other.description.is_some() {
140            self.description = other.description.take();
141        }
142        if other.summary.is_some() {
143            self.summary = other.summary.take();
144        }
145        other
146            .extensions
147            .retain(|name, _| !self.extensions.contains_key(name));
148        self.extensions.append(&mut other.extensions);
149    }
150
151    /// Append a new [`Operation`] by [`PathItemType`] to this [`PathItem`]. Operations can
152    /// hold only one operation per [`PathItemType`].
153    #[must_use]
154    pub fn add_operation<O: Into<Operation>>(
155        mut self,
156        path_item_type: PathItemType,
157        operation: O,
158    ) -> Self {
159        self.operations.insert(path_item_type, operation.into());
160        self
161    }
162
163    /// Add or change summary intended to apply all operations in this [`PathItem`].
164    #[must_use]
165    pub fn summary<S: Into<String>>(mut self, summary: S) -> Self {
166        self.summary = Some(summary.into());
167        self
168    }
169
170    /// Add or change optional description intended to apply all operations in this [`PathItem`].
171    /// Description supports markdown syntax.
172    #[must_use]
173    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
174        self.description = Some(description.into());
175        self
176    }
177
178    /// Add list of alternative [`Server`]s to serve all [`Operation`]s in this [`PathItem`]
179    /// overriding the global server array.
180    #[must_use]
181    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: I) -> Self {
182        self.servers = Servers(servers.into_iter().collect());
183        self
184    }
185
186    /// Append list of [`Parameter`]s common to all [`Operation`]s to this [`PathItem`].
187    #[must_use]
188    pub fn parameters<I: IntoIterator<Item = Parameter>>(mut self, parameters: I) -> Self {
189        self.parameters = Parameters(parameters.into_iter().collect());
190        self
191    }
192
193    /// Add openapi extensions (`x-something`) for [`PathItem`].
194    #[must_use]
195    pub fn extensions(mut self, extensions: PropMap<String, serde_json::Value>) -> Self {
196        self.extensions = extensions;
197        self
198    }
199}
200
201/// Path item operation type.
202#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord, Clone, Copy, Debug)]
203#[serde(rename_all = "lowercase")]
204pub enum PathItemType {
205    /// Type mapping for HTTP _GET_ request.
206    Get,
207    /// Type mapping for HTTP _POST_ request.
208    Post,
209    /// Type mapping for HTTP _PUT_ request.
210    Put,
211    /// Type mapping for HTTP _DELETE_ request.
212    Delete,
213    /// Type mapping for HTTP _OPTIONS_ request.
214    Options,
215    /// Type mapping for HTTP _HEAD_ request.
216    Head,
217    /// Type mapping for HTTP _PATCH_ request.
218    Patch,
219    /// Type mapping for HTTP _TRACE_ request.
220    Trace,
221    /// Type mapping for HTTP _CONNECT_ request.
222    Connect,
223}
224
225#[cfg(test)]
226mod tests {
227    use assert_json_diff::assert_json_eq;
228    use serde_json::json;
229
230    use super::*;
231    use crate::oapi::response::Response;
232
233    #[test]
234    fn test_build_path_item() {
235        let path_item = PathItem::new(PathItemType::Get, Operation::new())
236            .summary("summary")
237            .description("description")
238            .servers(Servers::new())
239            .parameters(Parameters::new());
240
241        assert_json_eq!(
242            path_item,
243            json!({
244                "description": "description",
245                "summary": "summary",
246                "get": {
247                    "responses": {}
248                }
249            })
250        )
251    }
252
253    #[test]
254    fn test_path_item_append() {
255        let mut path_item = PathItem::new(
256            PathItemType::Get,
257            Operation::new().add_response("200", Response::new("Get success")),
258        );
259        let mut other_path_item = PathItem::new(
260            PathItemType::Post,
261            Operation::new().add_response("200", Response::new("Post success")),
262        )
263        .description("description")
264        .summary("summary");
265        path_item.append(&mut other_path_item);
266
267        assert_json_eq!(
268            path_item,
269            json!({
270                "description": "description",
271                "summary": "summary",
272                "get": {
273                    "responses": {
274                        "200": {
275                            "description": "Get success"
276                        }
277                    }
278                },
279                "post": {
280                    "responses": {
281                        "200": {
282                            "description": "Post success"
283                        }
284                    }
285                }
286            })
287        )
288    }
289
290    #[test]
291    fn test_path_item_add_operation() {
292        let path_item = PathItem::new(
293            PathItemType::Get,
294            Operation::new().add_response("200", Response::new("Get success")),
295        )
296        .add_operation(
297            PathItemType::Post,
298            Operation::new().add_response("200", Response::new("Post success")),
299        );
300
301        assert_json_eq!(
302            path_item,
303            json!({
304                "get": {
305                    "responses": {
306                        "200": {
307                            "description": "Get success"
308                        }
309                    }
310                },
311                "post": {
312                    "responses": {
313                        "200": {
314                            "description": "Post success"
315                        }
316                    }
317                }
318            })
319        )
320    }
321
322    #[test]
323    fn test_paths_extend() {
324        let mut paths = Paths::new().path(
325            "/api/do_something",
326            PathItem::new(
327                PathItemType::Get,
328                Operation::new().add_response("200", Response::new("Get success")),
329            ),
330        );
331        paths.extend([(
332            "/api/do_something",
333            PathItem::new(
334                PathItemType::Post,
335                Operation::new().add_response("200", Response::new("Post success")),
336            )
337            .summary("summary")
338            .description("description"),
339        )]);
340
341        assert_json_eq!(
342            paths,
343            json!({
344                "/api/do_something": {
345                    "description": "description",
346                    "summary": "summary",
347                    "get": {
348                        "responses": {
349                            "200": {
350                                "description": "Get success"
351                            }
352                        }
353                    },
354                    "post": {
355                        "responses": {
356                            "200": {
357                                "description": "Post success"
358                            }
359                        }
360                    }
361                }
362            })
363        );
364    }
365
366    #[test]
367    fn test_paths_deref() {
368        let paths = Paths::new();
369        assert_eq!(0, paths.len());
370    }
371}