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
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 #[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 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 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}