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
131impl OpenApiSchema {
132    #[tracing::instrument(name = "load_doc_from_string", skip_all)]
133    pub fn load_doc_from_string(
134        content: &str,
135    ) -> Result<OpenApiSchema, MappedErrors> {
136        let doc =
137            serde_json::from_str::<OpenApiSchema>(&content).map_err(|e| {
138                execution_err(format!("Failed to parse OpenAPI document: {e}"))
139            })?;
140
141        Ok(doc)
142    }
143
144    /// Resolve the input refs
145    ///
146    /// This function resolves the references from input elements like
147    /// parameters, request bodies, headers, etc.
148    ///
149    /// Client methods should simple call this method with the operation id
150    /// and the input element name.
151    ///
152    #[tracing::instrument(
153        name = "resolve_input_refs_from_operation_id",
154        skip_all
155    )]
156    pub fn resolve_input_refs_from_operation_id(
157        &self,
158        operation_id: &str,
159    ) -> Result<serde_json::Value, MappedErrors> {
160        let operation = self.paths.find_operation(operation_id);
161
162        let operation = operation.ok_or(execution_err(format!(
163            "Operation {operation_id} not found"
164        )))?;
165
166        let mut depth_tracker = DepthTracker::new(25);
167
168        let resolved_operation = operation.resolve_ref(
169            &self.components.clone().unwrap_or_default(),
170            &mut depth_tracker,
171        )?;
172
173        Ok(resolved_operation)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    /// Get the example OpenAPI spec file
182    ///
183    /// This is used to make the JSON object deterministic.
184    ///
185    fn get_spec_example_file() -> &'static str {
186        include_str!("./mock/example-openapi.json")
187    }
188
189    #[test]
190    fn test_load_doc_from_string() {
191        let doc = OpenApiSchema::load_doc_from_string(get_spec_example_file());
192
193        if doc.is_err() {
194            println!("doc: {:?}", doc);
195        }
196
197        assert!(doc.is_ok());
198
199        let example_doc =
200            OpenApiSchema::load_doc_from_string(get_spec_example_file());
201
202        assert!(example_doc.is_ok());
203
204        let doc = doc.unwrap();
205
206        // Test if the loaded document is the same as the example document
207        assert_eq!(doc, example_doc.unwrap());
208    }
209
210    #[test]
211    fn test_resolve_input_refs_from_operation_id() {
212        let doc = OpenApiSchema::load_doc_from_string(get_spec_example_file());
213
214        if doc.is_err() {
215            println!("doc: {:?}", doc);
216        }
217
218        assert!(doc.is_ok());
219
220        let doc = doc.unwrap();
221
222        for operation_id in
223            ["register_tenant_tag_url", "list_accounts_by_type_url"]
224        {
225            let operation =
226                doc.resolve_input_refs_from_operation_id(operation_id);
227
228            assert!(operation.is_ok());
229        }
230    }
231}