openapi_from_source/
cli.rs1use anyhow::Result;
2use clap::{Parser, ValueEnum};
3use log::{debug, info};
4use std::path::PathBuf;
5
6#[derive(Parser, Debug)]
8#[command(name = "openapi-from-source")]
9#[command(author, version, about, long_about = None)]
10pub struct CliArgs {
11 #[arg(value_name = "PROJECT_PATH")]
13 pub project_path: PathBuf,
14
15 #[arg(short = 'f', long = "format", value_enum, default_value = "yaml")]
17 pub output_format: OutputFormat,
18
19 #[arg(short = 'o', long = "output", value_name = "FILE")]
21 pub output_path: Option<PathBuf>,
22
23 #[arg(short = 'w', long = "framework", value_enum)]
25 pub framework: Option<Framework>,
26
27 #[arg(short = 'v', long = "verbose")]
29 pub verbose: bool,
30}
31
32#[derive(Debug, Clone, Copy, ValueEnum)]
34pub enum OutputFormat {
35 Yaml,
37 Json,
39}
40
41#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Hash)]
43pub enum Framework {
44 Axum,
46 #[value(name = "actix-web")]
48 ActixWeb,
49}
50
51pub fn parse_args() -> Result<CliArgs> {
53 let args = CliArgs::parse();
54 parse_args_from_parsed(args)
55}
56
57pub fn parse_args_from_parsed(args: CliArgs) -> Result<CliArgs> {
59 debug!("Parsed arguments: {:?}", args);
60
61 if !args.project_path.exists() {
63 anyhow::bail!(
64 "Project path does not exist: {}",
65 args.project_path.display()
66 );
67 }
68
69 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
93pub 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 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 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 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 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 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 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 info!("Initializing type resolver...");
206 let type_resolver = TypeResolver::new(parsed_files);
207 let mut schema_gen = SchemaGenerator::new(type_resolver);
208
209 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 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 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 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