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    let content = if spec_path.starts_with("http://") || spec_path.starts_with("https://") {
30        // Try cache first if enabled
31        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        // Cache the content
40        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(), // Will be filtered based on selected modules
78    })
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        // Extract tags from operations if tags section is missing
103        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            // If no tags, use "default" module
163            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    // Extract request body schema
338    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                                // Inline schemas: These are schema definitions embedded directly
356                                // in the operation. We only track referenced schemas (from #/components/schemas)
357                                // to avoid generating duplicate types. Inline schemas are handled
358                                // at generation time where they appear.
359                            }
360                        }
361                    }
362                }
363            }
364        }
365    }
366
367    // Extract response schemas
368    for (_, response_ref) in operation.responses.responses.iter() {
369        match response_ref {
370            ReferenceOr::Reference { reference } => {
371                // Resolve response reference
372                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                                    // Inline schema - skip for now
385                                }
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                                // Inline schemas: These are schema definitions embedded directly
404                                // in the operation. We only track referenced schemas (from #/components/schemas)
405                                // to avoid generating duplicate types. Inline schemas are handled
406                                // at generation time where they appear.
407                            }
408                        }
409                    }
410                }
411            }
412        }
413    }
414
415    Ok(schema_names)
416}
417
418/// Recursively collect all schema dependencies for a given set of schema names
419pub 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        // Get dependencies of this schema
436        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
449/// Extract all schema references from a schema
450fn 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(); // Track which modules use each schema
615
616    // First pass: collect all schemas per module
617    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                    // Track schema usage
626                    schema_usage
627                        .entry(schema_name.clone())
628                        .or_default()
629                        .push(module.clone());
630                }
631            }
632        }
633
634        // Collect all dependencies for the schemas used by this module
635        let initial_schemas: Vec<String> = module_schema_set.into_iter().collect();
636        let all_dependencies = collect_all_dependencies(&initial_schemas, schemas, openapi)?;
637
638        // Track dependencies usage too
639        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    // Return without filtering - filtering will be done based on selected modules
650    Ok((module_schemas, Vec::new()))
651}
652
653/// Filter common schemas based on selected modules only
654/// Only creates common schemas when 2+ modules are selected
655pub 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    // Only create common module if 2+ modules are selected
662    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    // Track schema usage only for selected modules
669    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    // Identify shared schemas (used by 2+ selected modules)
681    // Only include schemas that are used by ALL selected modules or at least 2 of them
682    let mut shared_schemas = std::collections::HashSet::new();
683    for (schema_name, modules_using_it) in &schema_usage {
684        // Count unique modules using this schema
685        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    // Remove shared schemas from individual selected modules
693    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}