salvo_oapi/openapi/
operation.rs

1//! Implements [OpenAPI Operation Object][operation] types.
2//!
3//! [operation]: https://spec.openapis.org/oas/latest.html#operation-object
4use std::ops::{Deref, DerefMut};
5
6use serde::{Deserialize, Serialize};
7
8use super::{
9    Deprecated, ExternalDocs, RefOr, SecurityRequirement, Server,
10    request_body::RequestBody,
11    response::{Response, Responses},
12};
13use crate::{Parameter, Parameters, PathItemType, PropMap, Servers};
14
15/// Collection for save [`Operation`]s.
16#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
17pub struct Operations(pub PropMap<PathItemType, Operation>);
18impl Deref for Operations {
19    type Target = PropMap<PathItemType, Operation>;
20
21    fn deref(&self) -> &Self::Target {
22        &self.0
23    }
24}
25impl DerefMut for Operations {
26    fn deref_mut(&mut self) -> &mut Self::Target {
27        &mut self.0
28    }
29}
30impl IntoIterator for Operations {
31    type Item = (PathItemType, Operation);
32    type IntoIter = <PropMap<PathItemType, Operation> as IntoIterator>::IntoIter;
33
34    fn into_iter(self) -> Self::IntoIter {
35        self.0.into_iter()
36    }
37}
38impl Operations {
39    /// Construct a new empty [`Operations`]. This is effectively same as calling [`Operations::default`].
40    pub fn new() -> Self {
41        Default::default()
42    }
43
44    /// Returns `true` if instance contains no elements.
45    pub fn is_empty(&self) -> bool {
46        self.0.is_empty()
47    }
48    /// Add a new operation and returns `self`.
49    pub fn operation<K: Into<PathItemType>, O: Into<Operation>>(
50        mut self,
51        item_type: K,
52        operation: O,
53    ) -> Self {
54        self.insert(item_type, operation);
55        self
56    }
57
58    /// Inserts a key-value pair into the instance.
59    pub fn insert<K: Into<PathItemType>, O: Into<Operation>>(
60        &mut self,
61        item_type: K,
62        operation: O,
63    ) {
64        self.0.insert(item_type.into(), operation.into());
65    }
66
67    /// Moves all elements from `other` into `self`, leaving `other` empty.
68    ///
69    /// If a key from `other` is already present in `self`, the respective
70    /// value from `self` will be overwritten with the respective value from `other`.
71    pub fn append(&mut self, other: &mut Operations) {
72        self.0.append(&mut other.0);
73    }
74    /// Extends a collection with the contents of an iterator.
75    pub fn extend<I>(&mut self, iter: I)
76    where
77        I: IntoIterator<Item = (PathItemType, Operation)>,
78    {
79        for (item_type, operation) in iter {
80            self.insert(item_type, operation);
81        }
82    }
83}
84
85/// Implements [OpenAPI Operation Object][operation] object.
86///
87/// [operation]: https://spec.openapis.org/oas/latest.html#operation-object
88#[non_exhaustive]
89#[derive(Serialize, Deserialize, Default, Clone, PartialEq, Debug)]
90#[serde(rename_all = "camelCase")]
91pub struct Operation {
92    /// List of tags used for grouping operations.
93    ///
94    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the default
95    /// value used will be resolved from handler path provided in `#[openapi(paths(...))]` with
96    /// [`#[derive(OpenApi)]`][derive_openapi] macro. If path resolves to `None` value `crate` will
97    /// be used by default.
98    ///
99    /// [derive_path]: ../../attr.path.html
100    /// [derive_openapi]: ../../derive.OpenApi.html
101    #[serde(skip_serializing_if = "Vec::is_empty")]
102    pub tags: Vec<String>,
103
104    /// Short summary what [`Operation`] does.
105    ///
106    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the value
107    /// is taken from **first line** of doc comment.
108    ///
109    /// [derive_path]: ../../attr.path.html
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub summary: Option<String>,
112
113    /// Long explanation of [`Operation`] behaviour. Markdown syntax is supported.
114    ///
115    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the
116    /// doc comment is used as value for description.
117    ///
118    /// [derive_path]: ../../attr.path.html
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub description: Option<String>,
121
122    /// Unique identifier for the API [`Operation`]. Most typically this is mapped to handler function name.
123    ///
124    /// When used with derive [`#[salvo_oapi::endpoint(...)]`][derive_path] attribute macro the handler function
125    /// name will be used by default.
126    ///
127    /// [derive_path]: ../../attr.path.html
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub operation_id: Option<String>,
130
131    /// Additional external documentation for this operation.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub external_docs: Option<ExternalDocs>,
134
135    /// List of applicable parameters for this [`Operation`].
136    #[serde(skip_serializing_if = "Parameters::is_empty")]
137    pub parameters: Parameters,
138
139    /// Optional request body for this [`Operation`].
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub request_body: Option<RequestBody>,
142
143    /// List of possible responses returned by the [`Operation`].
144    pub responses: Responses,
145
146    /// Callback information.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub callbacks: Option<String>,
149
150    /// Define whether the operation is deprecated or not and thus should be avoided consuming.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub deprecated: Option<Deprecated>,
153
154    /// Declaration which security mechanisms can be used for for the operation. Only one
155    /// [`SecurityRequirement`] must be met.
156    ///
157    /// Security for the [`Operation`] can be set to optional by adding empty security with
158    /// [`SecurityRequirement::default`].
159    #[serde(skip_serializing_if = "Vec::is_empty")]
160    #[serde(rename = "security")]
161    pub securities: Vec<SecurityRequirement>,
162
163    /// Alternative [`Server`]s for this [`Operation`].
164    #[serde(skip_serializing_if = "Servers::is_empty")]
165    pub servers: Servers,
166
167    /// Optional extensions "x-something"
168    #[serde(skip_serializing_if = "PropMap::is_empty", flatten)]
169    pub extensions: PropMap<String, serde_json::Value>,
170}
171
172impl Operation {
173    /// Construct a new API [`Operation`].
174    pub fn new() -> Self {
175        Default::default()
176    }
177
178    /// Add or change tags of the [`Operation`].
179    pub fn tags<I, T>(mut self, tags: I) -> Self
180    where
181        I: IntoIterator<Item = T>,
182        T: Into<String>,
183    {
184        self.tags = tags.into_iter().map(|t| t.into()).collect();
185        self
186    }
187    /// Append tag to [`Operation`] tags and returns `Self`.
188    pub fn add_tag<S: Into<String>>(mut self, tag: S) -> Self {
189        self.tags.push(tag.into());
190        self
191    }
192
193    /// Add or change short summary of the [`Operation`].
194    pub fn summary<S: Into<String>>(mut self, summary: S) -> Self {
195        self.summary = Some(summary.into());
196        self
197    }
198
199    /// Add or change description of the [`Operation`].
200    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
201        self.description = Some(description.into());
202        self
203    }
204
205    /// Add or change operation id of the [`Operation`].
206    pub fn operation_id<S: Into<String>>(mut self, operation_id: S) -> Self {
207        self.operation_id = Some(operation_id.into());
208        self
209    }
210
211    /// Add or change parameters of the [`Operation`].
212    pub fn parameters<I: IntoIterator<Item = P>, P: Into<Parameter>>(
213        mut self,
214        parameters: I,
215    ) -> Self {
216        self.parameters
217            .extend(parameters.into_iter().map(|parameter| parameter.into()));
218        self
219    }
220    /// Append parameter to [`Operation`] parameters and returns `Self`.
221    pub fn add_parameter<P: Into<Parameter>>(mut self, parameter: P) -> Self {
222        self.parameters.insert(parameter);
223        self
224    }
225
226    /// Add or change request body of the [`Operation`].
227    pub fn request_body(mut self, request_body: RequestBody) -> Self {
228        self.request_body = Some(request_body);
229        self
230    }
231
232    /// Add or change responses of the [`Operation`].
233    pub fn responses<R: Into<Responses>>(mut self, responses: R) -> Self {
234        self.responses = responses.into();
235        self
236    }
237    /// Append status code and a [`Response`] to the [`Operation`] responses map and returns `Self`.
238    ///
239    /// * `code` must be valid HTTP status code.
240    /// * `response` is instances of [`Response`].
241    pub fn add_response<S: Into<String>, R: Into<RefOr<Response>>>(
242        mut self,
243        code: S,
244        response: R,
245    ) -> Self {
246        self.responses.insert(code, response);
247        self
248    }
249
250    /// Add or change deprecated status of the [`Operation`].
251    pub fn deprecated<D: Into<Deprecated>>(mut self, deprecated: D) -> Self {
252        self.deprecated = Some(deprecated.into());
253        self
254    }
255
256    /// Add or change list of [`SecurityRequirement`]s that are available for [`Operation`].
257    pub fn securities<I: IntoIterator<Item = SecurityRequirement>>(
258        mut self,
259        securities: I,
260    ) -> Self {
261        self.securities = securities.into_iter().collect();
262        self
263    }
264    /// Append [`SecurityRequirement`] to [`Operation`] security requirements and returns `Self`.
265    pub fn add_security(mut self, security: SecurityRequirement) -> Self {
266        self.securities.push(security);
267        self
268    }
269
270    /// Add or change list of [`Server`]s of the [`Operation`].
271    pub fn servers<I: IntoIterator<Item = Server>>(mut self, servers: I) -> Self {
272        self.servers = Servers(servers.into_iter().collect());
273        self
274    }
275    /// Append a new [`Server`] to the [`Operation`] servers and returns `Self`.
276    pub fn add_server(mut self, server: Server) -> Self {
277        self.servers.insert(server);
278        self
279    }
280
281    /// For easy chaining of operations.
282    pub fn then<F>(self, func: F) -> Self
283    where
284        F: FnOnce(Self) -> Self,
285    {
286        func(self)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use assert_json_diff::assert_json_eq;
293    use serde_json::json;
294
295    use super::{Operation, Operations};
296    use crate::{
297        Deprecated, Parameter, PathItemType, RequestBody, Responses, security::SecurityRequirement,
298        server::Server,
299    };
300
301    #[test]
302    fn operation_new() {
303        let operation = Operation::new();
304
305        assert!(operation.tags.is_empty());
306        assert!(operation.summary.is_none());
307        assert!(operation.description.is_none());
308        assert!(operation.operation_id.is_none());
309        assert!(operation.external_docs.is_none());
310        assert!(operation.parameters.is_empty());
311        assert!(operation.request_body.is_none());
312        assert!(operation.responses.is_empty());
313        assert!(operation.callbacks.is_none());
314        assert!(operation.deprecated.is_none());
315        assert!(operation.securities.is_empty());
316        assert!(operation.servers.is_empty());
317    }
318
319    #[test]
320    fn test_build_operation() {
321        let operation = Operation::new()
322            .tags(["tag1", "tag2"])
323            .add_tag("tag3")
324            .summary("summary")
325            .description("description")
326            .operation_id("operation_id")
327            .parameters([Parameter::new("param1")])
328            .add_parameter(Parameter::new("param2"))
329            .request_body(RequestBody::new())
330            .responses(Responses::new())
331            .deprecated(Deprecated::False)
332            .securities([SecurityRequirement::new("api_key", ["read:items"])])
333            .servers([Server::new("/api")]);
334
335        assert_json_eq!(
336            operation,
337            json!({
338                "responses": {},
339                "parameters": [
340                    {
341                        "name": "param1",
342                        "in": "path",
343                        "required": false
344                    },
345                    {
346                        "name": "param2",
347                        "in": "path",
348                        "required": false
349                    }
350                ],
351                "operationId": "operation_id",
352                "deprecated": false,
353                "security": [
354                    {
355                        "api_key": ["read:items"]
356                    }
357                ],
358                "servers": [{"url": "/api"}],
359                "summary": "summary",
360                "tags": ["tag1", "tag2", "tag3"],
361                "description": "description",
362                "requestBody": {
363                    "content": {}
364                }
365            })
366        );
367    }
368
369    #[test]
370    fn operation_security() {
371        let security_requirement1 =
372            SecurityRequirement::new("api_oauth2_flow", ["edit:items", "read:items"]);
373        let security_requirement2 = SecurityRequirement::new("api_oauth2_flow", ["remove:items"]);
374        let operation = Operation::new()
375            .add_security(security_requirement1)
376            .add_security(security_requirement2);
377
378        assert!(!operation.securities.is_empty());
379    }
380
381    #[test]
382    fn operation_server() {
383        let server1 = Server::new("/api");
384        let server2 = Server::new("/admin");
385        let operation = Operation::new().add_server(server1).add_server(server2);
386        assert!(!operation.servers.is_empty());
387    }
388
389    #[test]
390    fn test_operations() {
391        let operations = Operations::new();
392        assert!(operations.is_empty());
393
394        let mut operations = operations.operation(PathItemType::Get, Operation::new());
395        operations.insert(PathItemType::Post, Operation::new());
396        operations.extend([(PathItemType::Head, Operation::new())]);
397        assert_eq!(3, operations.len());
398    }
399
400    #[test]
401    fn test_operations_into_iter() {
402        let mut operations = Operations::new();
403        operations.insert(PathItemType::Get, Operation::new());
404        operations.insert(PathItemType::Post, Operation::new());
405        operations.insert(PathItemType::Head, Operation::new());
406
407        let mut iter = operations.into_iter();
408        assert_eq!((PathItemType::Get, Operation::new()), iter.next().unwrap());
409        assert_eq!((PathItemType::Post, Operation::new()), iter.next().unwrap());
410        assert_eq!((PathItemType::Head, Operation::new()), iter.next().unwrap());
411    }
412
413    #[test]
414    fn test_operations_then() {
415        let print_operation = |operation: Operation| {
416            println!("{:?}", operation);
417            operation
418        };
419        let operation = Operation::new();
420
421        operation.then(print_operation);
422    }
423}