Skip to main content

mycelium_openapi/dtos/
openapi_schema.rs

1use crate::{
2    dtos::operation::Operation,
3    entities::{DepthTracker, ReferenceResolver},
4};
5
6use mycelium_base::utils::errors::{execution_err, MappedErrors};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
11#[serde(rename_all = "camelCase")]
12pub struct MethodOperation {
13    /// The operations
14    ///
15    /// This is the operations of the OpenAPI specification.
16    ///
17    /// Example:
18    ///
19    /// ```json
20    /// {
21    ///     "get": {
22    ///         "operationId": "get_record"
23    ///     }
24    /// }
25    /// ```
26    #[serde(default, flatten)]
27    pub operations: HashMap<String, Operation>,
28}
29
30impl MethodOperation {
31    /// Find an operation by operation id
32    ///
33    /// This function finds an operation by operation id.
34    ///
35    pub fn find_operation(&self, operation_id: &str) -> Option<&Operation> {
36        self.operations.values().find(|operation| {
37            operation.operation_id == Some(operation_id.to_string())
38        })
39    }
40}
41
42#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)]
43#[serde(rename_all = "camelCase")]
44pub struct Paths {
45    /// The paths
46    ///
47    /// This is the paths of the OpenAPI specification.
48    ///
49    /// Example:
50    ///
51    /// ```json
52    /// {
53    ///     "/path/to/route": {
54    ///         "get": {
55    ///             "operationId": "get_record"
56    ///         },
57    ///         "post": {
58    ///             "operationId": "create_record"
59    ///         }
60    ///     }
61    /// }
62    /// ```
63    #[serde(default, flatten)]
64    pub paths: HashMap<String, MethodOperation>,
65}
66
67impl Paths {
68    /// Find an operation by operation id
69    ///
70    /// This function finds an operation by operation id.
71    ///
72    pub fn find_operation(&self, operation_id: &str) -> Option<&Operation> {
73        self.paths
74            .values()
75            .find_map(|path| path.find_operation(operation_id))
76    }
77}
78
79/// OpenAPI schema
80///
81/// This is the main schema for the OpenAPI specification.
82///
83#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
84#[serde(rename_all = "camelCase")]
85pub struct OpenApiSchema {
86    /// The OpenAPI version
87    ///
88    /// This is the version of the OpenAPI specification.
89    ///
90    pub openapi: String,
91
92    /// The info
93    ///
94    /// This is the info of the OpenAPI specification.
95    ///
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub info: Option<serde_json::Value>,
98
99    /// The paths
100    ///
101    /// This is the paths of the OpenAPI specification.
102    ///
103    /// Paths are indexed by route and method.
104    ///
105    /// Example:
106    ///
107    /// ```json
108    /// {
109    ///     "/path/to/route": {
110    ///         "get": {
111    ///             "operationId": "get_record"
112    ///         },
113    ///         "post": {
114    ///             "operationId": "create_record"
115    ///         }
116    ///     }
117    /// }
118    /// ```
119    ///
120    #[serde(default)]
121    pub paths: Paths,
122
123    /// The components
124    ///
125    /// This is the components of the OpenAPI specification.
126    ///
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub components: Option<serde_json::Value>,
129
130    /// The security
131    ///
132    /// This is the security of the OpenAPI specification.
133    ///
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub security: Option<serde_json::Value>,
136}
137
138impl OpenApiSchema {
139    #[tracing::instrument(name = "load_doc_from_string", skip_all)]
140    pub fn load_doc_from_string(
141        content: &str,
142    ) -> Result<OpenApiSchema, MappedErrors> {
143        let doc =
144            serde_json::from_str::<OpenApiSchema>(&content).map_err(|e| {
145                execution_err(format!("Failed to parse OpenAPI document: {e}"))
146            })?;
147
148        Ok(doc)
149    }
150
151    /// Resolve the input refs
152    ///
153    /// This function resolves the references from input elements like
154    /// parameters, request bodies, headers, etc.
155    ///
156    /// Client methods should simple call this method with the operation id
157    /// and the input element name.
158    ///
159    #[tracing::instrument(
160        name = "resolve_input_refs_from_operation_id",
161        skip_all
162    )]
163    pub fn resolve_input_refs_from_operation_id(
164        &self,
165        operation_id: &str,
166    ) -> Result<serde_json::Value, MappedErrors> {
167        let operation = self.paths.find_operation(operation_id);
168
169        let operation = operation.ok_or(execution_err(format!(
170            "Operation {operation_id} not found"
171        )))?;
172
173        let mut depth_tracker = DepthTracker::new(25);
174
175        let resolved_operation = operation.resolve_ref(
176            &self.components.clone().unwrap_or_default(),
177            &mut depth_tracker,
178        )?;
179
180        Ok(resolved_operation)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    /// Get the example OpenAPI spec file
189    ///
190    /// This is used to make the JSON object deterministic.
191    ///
192    fn get_spec_example_file() -> &'static str {
193        include_str!("./mock/example-openapi.json")
194    }
195
196    #[test]
197    fn test_load_doc_from_string() {
198        let doc = OpenApiSchema::load_doc_from_string(get_spec_example_file());
199
200        if doc.is_err() {
201            println!("doc: {:?}", doc);
202        }
203
204        assert!(doc.is_ok());
205
206        let example_doc =
207            OpenApiSchema::load_doc_from_string(get_spec_example_file());
208
209        assert!(example_doc.is_ok());
210
211        let doc = doc.unwrap();
212
213        // Test if the loaded document is the same as the example document
214        assert_eq!(doc, example_doc.unwrap());
215    }
216
217    #[test]
218    fn test_resolve_input_refs_from_operation_id() {
219        let doc = OpenApiSchema::load_doc_from_string(get_spec_example_file());
220
221        if doc.is_err() {
222            println!("doc: {:?}", doc);
223        }
224
225        assert!(doc.is_ok());
226
227        let doc = doc.unwrap();
228
229        for operation_id in
230            ["register_tenant_tag_url", "list_accounts_by_type_url"]
231        {
232            let operation =
233                doc.resolve_input_refs_from_operation_id(operation_id);
234
235            assert!(operation.is_ok());
236        }
237    }
238}