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