openapi_from_source/
cli.rs

1use anyhow::Result;
2use clap::{Parser, ValueEnum};
3use log::{debug, info};
4use std::path::PathBuf;
5
6/// Rust OpenAPI Generator - Automatically generate OpenAPI documentation from Rust web projects
7#[derive(Parser, Debug)]
8#[command(name = "openapi-from-source")]
9#[command(author, version, about, long_about = None)]
10pub struct CliArgs {
11    /// Path to the Rust project directory
12    #[arg(value_name = "PROJECT_PATH")]
13    pub project_path: PathBuf,
14
15    /// Output format (yaml or json)
16    #[arg(short = 'f', long = "format", value_enum, default_value = "yaml")]
17    pub output_format: OutputFormat,
18
19    /// Output file path (if not specified, outputs to stdout)
20    #[arg(short = 'o', long = "output", value_name = "FILE")]
21    pub output_path: Option<PathBuf>,
22
23    /// Specify the web framework to parse (if not specified, auto-detect)
24    #[arg(short = 'w', long = "framework", value_enum)]
25    pub framework: Option<Framework>,
26
27    /// Enable verbose output
28    #[arg(short = 'v', long = "verbose")]
29    pub verbose: bool,
30}
31
32/// Output format options
33#[derive(Debug, Clone, Copy, ValueEnum)]
34pub enum OutputFormat {
35    /// YAML format
36    Yaml,
37    /// JSON format
38    Json,
39}
40
41/// Supported web frameworks
42#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Hash)]
43pub enum Framework {
44    /// Axum framework
45    Axum,
46    /// Actix-Web framework
47    #[value(name = "actix-web")]
48    ActixWeb,
49}
50
51/// Parse command line arguments
52pub fn parse_args() -> Result<CliArgs> {
53    let args = CliArgs::parse();
54    parse_args_from_parsed(args)
55}
56
57/// Validate and log already-parsed arguments
58pub fn parse_args_from_parsed(args: CliArgs) -> Result<CliArgs> {
59    debug!("Parsed arguments: {:?}", args);
60
61    // Validate project path exists
62    if !args.project_path.exists() {
63        anyhow::bail!(
64            "Project path does not exist: {}",
65            args.project_path.display()
66        );
67    }
68
69    // Validate project path is a directory
70    if !args.project_path.is_dir() {
71        anyhow::bail!(
72            "Project path is not a directory: {}",
73            args.project_path.display()
74        );
75    }
76
77    info!("Project path: {}", args.project_path.display());
78    info!("Output format: {:?}", args.output_format);
79    if let Some(ref output) = args.output_path {
80        info!("Output file: {}", output.display());
81    } else {
82        info!("Output: stdout");
83    }
84    if let Some(ref framework) = args.framework {
85        info!("Framework: {:?}", framework);
86    } else {
87        info!("Framework: auto-detect");
88    }
89
90    Ok(args)
91}
92
93/// Run the main workflow
94pub fn run(args: CliArgs) -> Result<()> {
95    use crate::detector::{DetectionResult, FrameworkDetector};
96    use crate::extractor::actix::ActixExtractor;
97    use crate::extractor::axum::AxumExtractor;
98    use crate::extractor::{HttpMethod, RouteExtractor, RouteInfo};
99    use crate::openapi_builder::OpenApiBuilder;
100    use crate::parser::{AstParser, ParsedFile};
101    use crate::scanner::FileScanner;
102    use crate::schema_generator::SchemaGenerator;
103    use crate::serializer::{serialize_json, serialize_yaml, write_to_file};
104    use crate::type_resolver::TypeResolver;
105    
106    // Helper function to convert HTTP method to string
107    let method_str = |method: &HttpMethod| -> &str {
108        match method {
109            HttpMethod::Get => "GET",
110            HttpMethod::Post => "POST",
111            HttpMethod::Put => "PUT",
112            HttpMethod::Delete => "DELETE",
113            HttpMethod::Patch => "PATCH",
114            HttpMethod::Options => "OPTIONS",
115            HttpMethod::Head => "HEAD",
116        }
117    };
118    
119    info!("Starting OpenAPI document generation...");
120    info!("Project path: {}", args.project_path.display());
121    
122    // Step 1: Scan directory for Rust files
123    info!("Scanning project directory...");
124    let scanner = FileScanner::new(args.project_path.clone());
125    let scan_result = scanner.scan()?;
126    
127    info!("Found {} Rust files", scan_result.rust_files.len());
128    if !scan_result.warnings.is_empty() {
129        for warning in &scan_result.warnings {
130            log::warn!("{}", warning);
131        }
132    }
133    
134    if scan_result.rust_files.is_empty() {
135        anyhow::bail!("No Rust files found in the project directory");
136    }
137    
138    // Step 2: Parse files into AST
139    info!("Parsing Rust files...");
140    let parse_results = AstParser::parse_files(&scan_result.rust_files);
141    
142    let parsed_files: Vec<ParsedFile> = parse_results
143        .into_iter()
144        .filter_map(|r| {
145            match r {
146                Ok(parsed) => Some(parsed),
147                Err(e) => {
148                    debug!("Skipping file due to parse error: {}", e);
149                    None
150                }
151            }
152        })
153        .collect();
154    
155    info!("Successfully parsed {} files", parsed_files.len());
156    
157    if parsed_files.is_empty() {
158        anyhow::bail!("No files could be parsed successfully");
159    }
160    
161    // Step 3: Detect framework (or use user-specified framework)
162    let frameworks = if let Some(framework) = args.framework {
163        info!("Using user-specified framework: {:?}", framework);
164        vec![framework]
165    } else {
166        info!("Detecting web frameworks...");
167        let detection_result: DetectionResult = FrameworkDetector::detect(&parsed_files);
168        
169        if detection_result.frameworks.is_empty() {
170            anyhow::bail!(
171                "No supported web framework detected. Please specify a framework using --framework option.\n\
172                 Supported frameworks: axum, actix-web"
173            );
174        }
175        
176        info!("Detected frameworks: {:?}", detection_result.frameworks);
177        detection_result.frameworks
178    };
179    
180    // Step 4: Extract routes using appropriate extractors
181    info!("Extracting routes...");
182    let mut all_routes: Vec<RouteInfo> = Vec::new();
183    
184    for framework in &frameworks {
185        debug!("Extracting routes for framework: {:?}", framework);
186        
187        let extractor: Box<dyn RouteExtractor> = match framework {
188            Framework::Axum => Box::new(AxumExtractor),
189            Framework::ActixWeb => Box::new(ActixExtractor),
190        };
191        
192        // Extract routes from all files at once (extractor needs access to all functions)
193        let routes = extractor.extract_routes(&parsed_files);
194        debug!("Extracted {} routes for {:?}", routes.len(), framework);
195        all_routes.extend(routes);
196    }
197    
198    info!("Extracted {} total routes", all_routes.len());
199    
200    if all_routes.is_empty() {
201        log::warn!("No routes found in the project");
202    }
203    
204    // Step 5: Initialize type resolver and schema generator
205    info!("Initializing type resolver...");
206    let type_resolver = TypeResolver::new(parsed_files);
207    let mut schema_gen = SchemaGenerator::new(type_resolver);
208    
209    // Step 6: Build OpenAPI document
210    info!("Building OpenAPI document...");
211    let mut builder = OpenApiBuilder::new();
212    
213    for route in &all_routes {
214        debug!("Adding route: {} {}", method_str(&route.method), route.path);
215        builder.add_route(route, &mut schema_gen);
216    }
217    
218    let document = builder.build(schema_gen);
219    info!("OpenAPI document built successfully");
220    
221    // Step 7: Serialize to requested format
222    info!("Serializing to {:?} format...", args.output_format);
223    let content = match args.output_format {
224        OutputFormat::Yaml => serialize_yaml(&document)?,
225        OutputFormat::Json => serialize_json(&document)?,
226    };
227    
228    // Step 8: Output to file or stdout
229    if let Some(output_path) = &args.output_path {
230        info!("Writing output to: {}", output_path.display());
231        write_to_file(&content, output_path)?;
232        info!("Successfully wrote OpenAPI document to {}", output_path.display());
233    } else {
234        println!("{}", content);
235    }
236    
237    // Step 9: Display summary
238    info!("Generation complete!");
239    info!("Summary:");
240    info!("  - Files scanned: {}", scan_result.rust_files.len());
241    info!("  - Files parsed: {}", all_routes.len());
242    info!("  - Routes found: {}", all_routes.len());
243    info!("  - Frameworks: {:?}", frameworks);
244    
245    Ok(())
246}
247
248