1use crate::error::{FileSystemError, NetworkError, Result, SchemaError};
2use openapiv3::{OpenAPI, Operation, Parameter, PathItem, ReferenceOr, Schema};
3use std::collections::HashMap;
4
5pub struct ParsedSpec {
6 pub openapi: OpenAPI,
7 pub modules: Vec<String>,
8 pub operations_by_tag: HashMap<String, Vec<OperationInfo>>,
9 pub schemas: HashMap<String, Schema>,
10 pub module_schemas: HashMap<String, Vec<String>>,
11 pub common_schemas: Vec<String>, }
13
14#[derive(Debug, Clone)]
15pub struct OperationInfo {
16 pub method: String,
17 pub path: String,
18 pub operation: Operation,
19}
20
21pub async fn fetch_and_parse_spec(spec_path: &str) -> Result<ParsedSpec> {
22 fetch_and_parse_spec_with_cache(spec_path, false).await
23}
24
25pub async fn fetch_and_parse_spec_with_cache(
26 spec_path: &str,
27 use_cache: bool,
28) -> Result<ParsedSpec> {
29 let content = if spec_path.starts_with("http://") || spec_path.starts_with("https://") {
30 if use_cache {
32 if let Some(cached) = crate::cache::CacheManager::get_cached_spec(spec_path)? {
33 return parse_spec_content(&cached, spec_path);
34 }
35 }
36
37 let content = fetch_remote_spec(spec_path).await?;
38
39 if use_cache {
41 crate::cache::CacheManager::cache_spec(spec_path, &content)?;
42 }
43
44 content
45 } else {
46 std::fs::read_to_string(spec_path).map_err(|e| FileSystemError::ReadFileFailed {
47 path: spec_path.to_string(),
48 source: e,
49 })?
50 };
51
52 parse_spec_content(&content, spec_path)
53}
54
55fn parse_spec_content(content: &str, spec_path: &str) -> Result<ParsedSpec> {
56 let openapi: OpenAPI = if spec_path.ends_with(".yaml") || spec_path.ends_with(".yml") {
57 serde_yaml::from_str(content).map_err(|e| SchemaError::UnsupportedType {
58 schema_type: format!("Failed to parse YAML spec: {}", e),
59 })?
60 } else {
61 serde_json::from_str(content).map_err(|e| SchemaError::UnsupportedType {
62 schema_type: format!("Failed to parse JSON spec: {}", e),
63 })?
64 };
65
66 let modules = extract_modules(&openapi);
67 let operations_by_tag = extract_operations_by_tag(&openapi);
68 let schemas = extract_schemas(&openapi);
69 let (module_schemas, _) = map_modules_to_schemas(&openapi, &operations_by_tag, &schemas)?;
70
71 Ok(ParsedSpec {
72 openapi,
73 modules,
74 operations_by_tag,
75 schemas,
76 module_schemas,
77 common_schemas: Vec::new(), })
79}
80
81async fn fetch_remote_spec(url: &str) -> Result<String> {
82 let response = reqwest::get(url)
83 .await
84 .map_err(|e| NetworkError::FetchFailed {
85 url: url.to_string(),
86 source: e,
87 })?;
88
89 response.text().await.map_err(|e| {
90 NetworkError::ReadResponseFailed {
91 url: url.to_string(),
92 source: e,
93 }
94 .into()
95 })
96}
97
98pub fn extract_modules(openapi: &OpenAPI) -> Vec<String> {
99 if !openapi.tags.is_empty() {
100 openapi.tags.iter().map(|tag| tag.name.clone()).collect()
101 } else {
102 let mut tag_set = std::collections::HashSet::new();
104 for (_, path_item) in openapi.paths.iter() {
105 if let ReferenceOr::Item(path_item) = path_item {
106 extract_tags_from_path_item(path_item, &mut tag_set);
107 }
108 }
109 tag_set.into_iter().collect()
110 }
111}
112
113fn extract_tags_from_path_item(
114 path_item: &PathItem,
115 tag_set: &mut std::collections::HashSet<String>,
116) {
117 let operations = [
118 path_item.get.as_ref(),
119 path_item.post.as_ref(),
120 path_item.put.as_ref(),
121 path_item.delete.as_ref(),
122 path_item.patch.as_ref(),
123 path_item.head.as_ref(),
124 path_item.options.as_ref(),
125 ];
126
127 for op in operations.iter().flatten() {
128 for tag in &op.tags {
129 tag_set.insert(tag.clone());
130 }
131 }
132}
133
134pub fn extract_operations_by_tag(openapi: &OpenAPI) -> HashMap<String, Vec<OperationInfo>> {
135 let mut result: HashMap<String, Vec<OperationInfo>> = HashMap::new();
136
137 for (path, path_item) in openapi.paths.iter() {
138 if let ReferenceOr::Item(path_item) = path_item {
139 add_operation(&mut result, "GET", path, path_item.get.as_ref());
140 add_operation(&mut result, "POST", path, path_item.post.as_ref());
141 add_operation(&mut result, "PUT", path, path_item.put.as_ref());
142 add_operation(&mut result, "DELETE", path, path_item.delete.as_ref());
143 add_operation(&mut result, "PATCH", path, path_item.patch.as_ref());
144 add_operation(&mut result, "HEAD", path, path_item.head.as_ref());
145 add_operation(&mut result, "OPTIONS", path, path_item.options.as_ref());
146 }
147 }
148
149 result
150}
151
152fn add_operation(
153 result: &mut HashMap<String, Vec<OperationInfo>>,
154 method: &str,
155 path: &str,
156 operation: Option<&Operation>,
157) {
158 if let Some(op) = operation {
159 let tags = op.tags.clone();
160
161 if tags.is_empty() {
162 result
164 .entry("default".to_string())
165 .or_default()
166 .push(OperationInfo {
167 method: method.to_string(),
168 path: path.to_string(),
169 operation: op.clone(),
170 });
171 } else {
172 for tag in tags {
173 result.entry(tag.clone()).or_default().push(OperationInfo {
174 method: method.to_string(),
175 path: path.to_string(),
176 operation: op.clone(),
177 });
178 }
179 }
180 }
181}
182
183pub fn extract_schemas(openapi: &OpenAPI) -> HashMap<String, Schema> {
184 let mut schemas = HashMap::new();
185
186 if let Some(components) = &openapi.components {
187 for (name, schema_ref) in components.schemas.iter() {
188 if let ReferenceOr::Item(schema) = schema_ref {
189 schemas.insert(name.clone(), schema.clone());
190 }
191 }
192 }
193
194 schemas
195}
196
197pub fn resolve_ref(openapi: &OpenAPI, ref_path: &str) -> Result<ReferenceOr<Schema>> {
198 if !ref_path.starts_with("#/") {
199 return Err(SchemaError::InvalidReference {
200 ref_path: ref_path.to_string(),
201 }
202 .into());
203 }
204
205 let parts: Vec<&str> = ref_path.trim_start_matches("#/").split('/').collect();
206
207 match parts.as_slice() {
208 ["components", "schemas", name] => {
209 if let Some(components) = &openapi.components {
210 if let Some(schema_ref) = components.schemas.get(*name) {
211 return Ok(schema_ref.clone());
212 }
213 }
214 Err(SchemaError::NotFound {
215 name: name.to_string(),
216 }
217 .into())
218 }
219 _ => Err(SchemaError::UnsupportedReferencePath {
220 ref_path: ref_path.to_string(),
221 }
222 .into()),
223 }
224}
225
226pub fn resolve_parameter_ref(openapi: &OpenAPI, ref_path: &str) -> Result<ReferenceOr<Parameter>> {
227 if !ref_path.starts_with("#/") {
228 return Err(SchemaError::InvalidReference {
229 ref_path: ref_path.to_string(),
230 }
231 .into());
232 }
233
234 let parts: Vec<&str> = ref_path.trim_start_matches("#/").split('/').collect();
235
236 match parts.as_slice() {
237 ["components", "parameters", name] => {
238 if let Some(components) = &openapi.components {
239 if let Some(param_ref) = components.parameters.get(*name) {
240 return Ok(param_ref.clone());
241 }
242 }
243 Err(SchemaError::ParameterNotFound {
244 name: name.to_string(),
245 }
246 .into())
247 }
248 _ => Err(SchemaError::UnsupportedReferencePath {
249 ref_path: ref_path.to_string(),
250 }
251 .into()),
252 }
253}
254
255pub fn resolve_request_body_ref(
256 openapi: &OpenAPI,
257 ref_path: &str,
258) -> Result<ReferenceOr<openapiv3::RequestBody>> {
259 if !ref_path.starts_with("#/") {
260 return Err(SchemaError::InvalidReference {
261 ref_path: ref_path.to_string(),
262 }
263 .into());
264 }
265
266 let parts: Vec<&str> = ref_path.trim_start_matches("#/").split('/').collect();
267
268 match parts.as_slice() {
269 ["components", "requestBodies", name] => {
270 if let Some(components) = &openapi.components {
271 if let Some(body_ref) = components.request_bodies.get(*name) {
272 return Ok(body_ref.clone());
273 }
274 }
275 Err(SchemaError::RequestBodyNotFound {
276 name: name.to_string(),
277 }
278 .into())
279 }
280 _ => Err(SchemaError::UnsupportedReferencePath {
281 ref_path: ref_path.to_string(),
282 }
283 .into()),
284 }
285}
286
287pub fn resolve_response_ref(
288 openapi: &OpenAPI,
289 ref_path: &str,
290) -> Result<ReferenceOr<openapiv3::Response>> {
291 if !ref_path.starts_with("#/") {
292 return Err(SchemaError::InvalidReference {
293 ref_path: ref_path.to_string(),
294 }
295 .into());
296 }
297
298 let parts: Vec<&str> = ref_path.trim_start_matches("#/").split('/').collect();
299
300 match parts.as_slice() {
301 ["components", "responses", name] => {
302 if let Some(components) = &openapi.components {
303 if let Some(response_ref) = components.responses.get(*name) {
304 return Ok(response_ref.clone());
305 }
306 }
307 Err(SchemaError::ResponseNotFound {
308 name: name.to_string(),
309 }
310 .into())
311 }
312 _ => Err(SchemaError::UnsupportedReferencePath {
313 ref_path: ref_path.to_string(),
314 }
315 .into()),
316 }
317}
318
319pub fn get_schema_name_from_ref(ref_path: &str) -> Option<String> {
320 if ref_path.starts_with("#/components/schemas/") {
321 Some(
322 ref_path
323 .trim_start_matches("#/components/schemas/")
324 .to_string(),
325 )
326 } else {
327 None
328 }
329}
330
331pub fn extract_schemas_for_operation(
332 operation: &Operation,
333 openapi: &OpenAPI,
334) -> Result<Vec<String>> {
335 let mut schema_names = Vec::new();
336
337 if let Some(request_body) = &operation.request_body {
339 match request_body {
340 ReferenceOr::Reference { reference } => {
341 if let Some(ref_name) = get_schema_name_from_ref(reference) {
342 schema_names.push(ref_name);
343 }
344 }
345 ReferenceOr::Item(body) => {
346 if let Some(json_media) = body.content.get("application/json") {
347 if let Some(schema_ref) = &json_media.schema {
348 match schema_ref {
349 ReferenceOr::Reference { reference } => {
350 if let Some(ref_name) = get_schema_name_from_ref(reference) {
351 schema_names.push(ref_name);
352 }
353 }
354 ReferenceOr::Item(_) => {
355 }
360 }
361 }
362 }
363 }
364 }
365 }
366
367 for (_, response_ref) in operation.responses.responses.iter() {
369 match response_ref {
370 ReferenceOr::Reference { reference } => {
371 if let Ok(ReferenceOr::Item(response)) = resolve_response_ref(openapi, reference) {
373 if let Some(json_media) = response.content.get("application/json") {
374 if let Some(schema_ref) = &json_media.schema {
375 match schema_ref {
376 ReferenceOr::Reference { reference } => {
377 if let Some(ref_name) = get_schema_name_from_ref(reference) {
378 if !schema_names.contains(&ref_name) {
379 schema_names.push(ref_name);
380 }
381 }
382 }
383 ReferenceOr::Item(_) => {
384 }
386 }
387 }
388 }
389 }
390 }
391 ReferenceOr::Item(response) => {
392 if let Some(json_media) = response.content.get("application/json") {
393 if let Some(schema_ref) = &json_media.schema {
394 match schema_ref {
395 ReferenceOr::Reference { reference } => {
396 if let Some(ref_name) = get_schema_name_from_ref(reference) {
397 if !schema_names.contains(&ref_name) {
398 schema_names.push(ref_name);
399 }
400 }
401 }
402 ReferenceOr::Item(_) => {
403 }
408 }
409 }
410 }
411 }
412 }
413 }
414
415 Ok(schema_names)
416}
417
418pub fn collect_all_dependencies(
420 schema_names: &[String],
421 schemas: &HashMap<String, Schema>,
422 openapi: &OpenAPI,
423) -> Result<Vec<String>> {
424 let mut all_schemas = std::collections::HashSet::new();
425 let mut to_process: Vec<String> = schema_names.to_vec();
426 let mut processed = std::collections::HashSet::new();
427
428 while let Some(schema_name) = to_process.pop() {
429 if processed.contains(&schema_name) {
430 continue;
431 }
432 processed.insert(schema_name.clone());
433 all_schemas.insert(schema_name.clone());
434
435 if let Some(schema) = schemas.get(&schema_name) {
437 let deps = extract_schema_dependencies(schema, openapi)?;
438 for dep in deps {
439 if schemas.contains_key(&dep) && !processed.contains(&dep) {
440 to_process.push(dep);
441 }
442 }
443 }
444 }
445
446 Ok(all_schemas.into_iter().collect())
447}
448
449fn extract_schema_dependencies(schema: &Schema, openapi: &OpenAPI) -> Result<Vec<String>> {
451 let mut deps = Vec::new();
452 let mut visited = std::collections::HashSet::new();
453 extract_schema_refs_recursive(schema, openapi, &mut deps, &mut visited)?;
454 Ok(deps)
455}
456
457fn extract_schema_refs_recursive(
458 schema: &Schema,
459 openapi: &OpenAPI,
460 deps: &mut Vec<String>,
461 visited: &mut std::collections::HashSet<String>,
462) -> Result<()> {
463 match &schema.schema_kind {
464 openapiv3::SchemaKind::Type(type_) => match type_ {
465 openapiv3::Type::Array(array) => {
466 if let Some(items) = &array.items {
467 match items {
468 ReferenceOr::Reference { reference } => {
469 if let Some(ref_name) = get_schema_name_from_ref(reference) {
470 if !visited.contains(&ref_name) {
471 visited.insert(ref_name.clone());
472 deps.push(ref_name.clone());
473 if let Ok(ReferenceOr::Item(dep_schema)) =
474 resolve_ref(openapi, reference)
475 {
476 extract_schema_refs_recursive(
477 &dep_schema,
478 openapi,
479 deps,
480 visited,
481 )?;
482 }
483 }
484 }
485 }
486 ReferenceOr::Item(item_schema) => {
487 extract_schema_refs_recursive(item_schema, openapi, deps, visited)?;
488 }
489 }
490 }
491 }
492 openapiv3::Type::Object(object_type) => {
493 for (_, prop_schema_ref) in object_type.properties.iter() {
494 match prop_schema_ref {
495 ReferenceOr::Reference { reference } => {
496 if let Some(ref_name) = get_schema_name_from_ref(reference) {
497 if !visited.contains(&ref_name) {
498 visited.insert(ref_name.clone());
499 deps.push(ref_name.clone());
500 if let Ok(ReferenceOr::Item(dep_schema)) =
501 resolve_ref(openapi, reference)
502 {
503 extract_schema_refs_recursive(
504 &dep_schema,
505 openapi,
506 deps,
507 visited,
508 )?;
509 }
510 }
511 }
512 }
513 ReferenceOr::Item(prop_schema) => {
514 extract_schema_refs_recursive(prop_schema, openapi, deps, visited)?;
515 }
516 }
517 }
518 }
519 _ => {}
520 },
521 openapiv3::SchemaKind::OneOf { one_of, .. } => {
522 for item in one_of {
523 match item {
524 ReferenceOr::Reference { reference } => {
525 if let Some(ref_name) = get_schema_name_from_ref(reference) {
526 if !visited.contains(&ref_name) {
527 visited.insert(ref_name.clone());
528 deps.push(ref_name.clone());
529 if let Ok(ReferenceOr::Item(dep_schema)) =
530 resolve_ref(openapi, reference)
531 {
532 extract_schema_refs_recursive(
533 &dep_schema,
534 openapi,
535 deps,
536 visited,
537 )?;
538 }
539 }
540 }
541 }
542 ReferenceOr::Item(item_schema) => {
543 extract_schema_refs_recursive(item_schema, openapi, deps, visited)?;
544 }
545 }
546 }
547 }
548 openapiv3::SchemaKind::AllOf { all_of, .. } => {
549 for item in all_of {
550 match item {
551 ReferenceOr::Reference { reference } => {
552 if let Some(ref_name) = get_schema_name_from_ref(reference) {
553 if !visited.contains(&ref_name) {
554 visited.insert(ref_name.clone());
555 deps.push(ref_name.clone());
556 if let Ok(ReferenceOr::Item(dep_schema)) =
557 resolve_ref(openapi, reference)
558 {
559 extract_schema_refs_recursive(
560 &dep_schema,
561 openapi,
562 deps,
563 visited,
564 )?;
565 }
566 }
567 }
568 }
569 ReferenceOr::Item(item_schema) => {
570 extract_schema_refs_recursive(item_schema, openapi, deps, visited)?;
571 }
572 }
573 }
574 }
575 openapiv3::SchemaKind::AnyOf { any_of, .. } => {
576 for item in any_of {
577 match item {
578 ReferenceOr::Reference { reference } => {
579 if let Some(ref_name) = get_schema_name_from_ref(reference) {
580 if !visited.contains(&ref_name) {
581 visited.insert(ref_name.clone());
582 deps.push(ref_name.clone());
583 if let Ok(ReferenceOr::Item(dep_schema)) =
584 resolve_ref(openapi, reference)
585 {
586 extract_schema_refs_recursive(
587 &dep_schema,
588 openapi,
589 deps,
590 visited,
591 )?;
592 }
593 }
594 }
595 }
596 ReferenceOr::Item(item_schema) => {
597 extract_schema_refs_recursive(item_schema, openapi, deps, visited)?;
598 }
599 }
600 }
601 }
602 _ => {}
603 }
604 Ok(())
605}
606
607#[allow(clippy::type_complexity)]
608pub fn map_modules_to_schemas(
609 openapi: &OpenAPI,
610 operations_by_tag: &HashMap<String, Vec<OperationInfo>>,
611 schemas: &HashMap<String, Schema>,
612) -> Result<(HashMap<String, Vec<String>>, Vec<String>)> {
613 let mut module_schemas: HashMap<String, Vec<String>> = HashMap::new();
614 let mut schema_usage: HashMap<String, Vec<String>> = HashMap::new(); for (module, operations) in operations_by_tag {
618 let mut module_schema_set = std::collections::HashSet::new();
619
620 for op_info in operations {
621 let op_schemas = extract_schemas_for_operation(&op_info.operation, openapi)?;
622 for schema_name in op_schemas {
623 if schemas.contains_key(&schema_name) {
624 module_schema_set.insert(schema_name.clone());
625 schema_usage
627 .entry(schema_name.clone())
628 .or_default()
629 .push(module.clone());
630 }
631 }
632 }
633
634 let initial_schemas: Vec<String> = module_schema_set.into_iter().collect();
636 let all_dependencies = collect_all_dependencies(&initial_schemas, schemas, openapi)?;
637
638 for dep in &all_dependencies {
640 schema_usage
641 .entry(dep.clone())
642 .or_default()
643 .push(module.clone());
644 }
645
646 module_schemas.insert(module.clone(), all_dependencies);
647 }
648
649 Ok((module_schemas, Vec::new()))
651}
652
653pub fn filter_common_schemas(
656 module_schemas: &HashMap<String, Vec<String>>,
657 selected_modules: &[String],
658) -> (HashMap<String, Vec<String>>, Vec<String>) {
659 let mut filtered_module_schemas = module_schemas.clone();
660
661 if selected_modules.len() < 2 {
663 return (filtered_module_schemas, Vec::new());
664 }
665
666 let mut schema_usage: HashMap<String, Vec<String>> = HashMap::new();
667
668 for module in selected_modules {
670 if let Some(schemas) = filtered_module_schemas.get(module) {
671 for schema_name in schemas {
672 schema_usage
673 .entry(schema_name.clone())
674 .or_default()
675 .push(module.clone());
676 }
677 }
678 }
679
680 let mut shared_schemas = std::collections::HashSet::new();
683 for (schema_name, modules_using_it) in &schema_usage {
684 let unique_modules: std::collections::HashSet<String> =
686 modules_using_it.iter().cloned().collect();
687 if unique_modules.len() >= 2 {
688 shared_schemas.insert(schema_name.clone());
689 }
690 }
691
692 let common_schemas: Vec<String> = shared_schemas.iter().cloned().collect();
694 if !shared_schemas.is_empty() {
695 for module in selected_modules {
696 if let Some(module_schema_list) = filtered_module_schemas.get_mut(module) {
697 module_schema_list.retain(|s| !shared_schemas.contains(s));
698 }
699 }
700 }
701
702 (filtered_module_schemas, common_schemas)
703}