Skip to main content

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