mycelium_openapi/dtos/
openapi_schema.rs1use 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 #[serde(default, flatten)]
27 pub operations: HashMap<String, Operation>,
28}
29
30impl MethodOperation {
31 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 #[serde(default, flatten)]
64 pub paths: HashMap<String, MethodOperation>,
65}
66
67impl Paths {
68 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#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
84#[serde(rename_all = "camelCase")]
85pub struct OpenApiSchema {
86 pub openapi: String,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub info: Option<serde_json::Value>,
98
99 #[serde(default)]
121 pub paths: Paths,
122
123 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub components: Option<serde_json::Value>,
129
130 #[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 #[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 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 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}