vika_cli/generator/
swagger_parser.rs

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>, // Schemas shared across multiple modules
12}
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        // Try cache first if enabled
39        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        // Cache the content
50        if use_cache {
51            crate::cache::CacheManager::cache_spec_with_name(spec_path, &content, spec_name)?;
52        }
53
54        content
55    } else {
56        // Check if file exists before trying to read
57        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(), // Will be filtered based on selected modules
95    })
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        // Extract tags from operations if tags section is missing
120        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            // If no tags, use "default" module
180            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    // Extract request body schema
355    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                                // Inline schemas: These are schema definitions embedded directly
373                                // in the operation. We only track referenced schemas (from #/components/schemas)
374                                // to avoid generating duplicate types. Inline schemas are handled
375                                // at generation time where they appear.
376                            }
377                        }
378                    }
379                }
380            }
381        }
382    }
383
384    // Extract response schemas
385    for (_, response_ref) in operation.responses.responses.iter() {
386        match response_ref {
387            ReferenceOr::Reference { reference } => {
388                // Resolve response reference
389                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                                    // Inline schema - skip for now
402                                }
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                                // Inline schemas: These are schema definitions embedded directly
421                                // in the operation. We only track referenced schemas (from #/components/schemas)
422                                // to avoid generating duplicate types. Inline schemas are handled
423                                // at generation time where they appear.
424                            }
425                        }
426                    }
427                }
428            }
429        }
430    }
431
432    Ok(schema_names)
433}
434
435/// Recursively collect all schema dependencies for a given set of schema names
436pub 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        // Get dependencies of this schema
453        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
466/// Extract all schema references from a schema
467fn 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(); // Track which modules use each schema
632
633    // First pass: collect all schemas per module
634    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                    // Track schema usage
643                    schema_usage
644                        .entry(schema_name.clone())
645                        .or_default()
646                        .push(module.clone());
647                }
648            }
649        }
650
651        // Collect all dependencies for the schemas used by this module
652        let initial_schemas: Vec<String> = module_schema_set.into_iter().collect();
653        let all_dependencies = collect_all_dependencies(&initial_schemas, schemas, openapi)?;
654
655        // Track dependencies usage too
656        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    // Return without filtering - filtering will be done based on selected modules
667    Ok((module_schemas, Vec::new()))
668}
669
670/// Filter common schemas based on selected modules only
671/// Only creates common schemas when 2+ modules are selected
672pub 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    // Only create common module if 2+ modules are selected
679    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    // Track schema usage only for selected modules
686    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    // Identify shared schemas (used by 2+ selected modules)
698    // Only include schemas that are used by ALL selected modules or at least 2 of them
699    let mut shared_schemas = std::collections::HashSet::new();
700    for (schema_name, modules_using_it) in &schema_usage {
701        // Count unique modules using this schema
702        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    // Remove shared schemas from individual selected modules
710    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}