openapi_from_source/
parser.rs1use anyhow::{Context, Result};
2use log::{debug, warn};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6pub struct AstParser;
21
22#[derive(Debug)]
26pub struct ParsedFile {
27 pub path: PathBuf,
29 pub syntax_tree: syn::File,
31}
32
33impl AstParser {
34 pub fn parse_file(path: &Path) -> Result<ParsedFile> {
53 debug!("Parsing file: {}", path.display());
54
55 let content = fs::read_to_string(path)
57 .with_context(|| format!("Failed to read file: {}", path.display()))?;
58
59 let syntax_tree = syn::parse_file(&content)
61 .with_context(|| format!("Failed to parse Rust syntax in file: {}", path.display()))?;
62
63 debug!("Successfully parsed file: {}", path.display());
64
65 Ok(ParsedFile {
66 path: path.to_path_buf(),
67 syntax_tree,
68 })
69 }
70
71 pub fn parse_files(paths: &[PathBuf]) -> Vec<Result<ParsedFile>> {
86 debug!("Parsing {} files", paths.len());
87
88 let results: Vec<Result<ParsedFile>> = paths
89 .iter()
90 .map(|path| {
91 match Self::parse_file(path) {
92 Ok(parsed) => Ok(parsed),
93 Err(e) => {
94 warn!("Failed to parse {}: {}", path.display(), e);
95 Err(e)
96 }
97 }
98 })
99 .collect();
100
101 let success_count = results.iter().filter(|r| r.is_ok()).count();
102 let failure_count = results.len() - success_count;
103
104 debug!(
105 "Parsing complete: {} succeeded, {} failed",
106 success_count, failure_count
107 );
108
109 results
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::fs;
117 use std::io::Write;
118 use tempfile::TempDir;
119
120 fn create_temp_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
122 let file_path = dir.path().join(name);
123 let mut file = fs::File::create(&file_path).unwrap();
124 file.write_all(content.as_bytes()).unwrap();
125 file_path
126 }
127
128 #[test]
129 fn test_parse_valid_rust_file() {
130 let temp_dir = TempDir::new().unwrap();
131 let valid_code = r#"
132 use std::collections::HashMap;
133
134 pub struct User {
135 pub id: u32,
136 pub name: String,
137 }
138
139 pub fn get_user(id: u32) -> Option<User> {
140 None
141 }
142 "#;
143
144 let file_path = create_temp_file(&temp_dir, "valid.rs", valid_code);
145 let result = AstParser::parse_file(&file_path);
146
147 assert!(result.is_ok());
148 let parsed = result.unwrap();
149 assert_eq!(parsed.path, file_path);
150 assert!(!parsed.syntax_tree.items.is_empty());
151 }
152
153 #[test]
154 fn test_parse_invalid_rust_file() {
155 let temp_dir = TempDir::new().unwrap();
156 let invalid_code = r#"
157 pub struct User {
158 pub id: u32
159 pub name: String // Missing comma
160 }
161
162 fn broken( { // Invalid syntax
163 let x = ;
164 }
165 "#;
166
167 let file_path = create_temp_file(&temp_dir, "invalid.rs", invalid_code);
168 let result = AstParser::parse_file(&file_path);
169
170 assert!(result.is_err());
171 let err_msg = result.unwrap_err().to_string();
172 assert!(err_msg.contains("Failed to parse Rust syntax"));
173 }
174
175 #[test]
176 fn test_parse_nonexistent_file() {
177 let result = AstParser::parse_file(Path::new("/nonexistent/file.rs"));
178
179 assert!(result.is_err());
180 let err_msg = result.unwrap_err().to_string();
181 assert!(err_msg.contains("Failed to read file"));
182 }
183
184 #[test]
185 fn test_parse_empty_file() {
186 let temp_dir = TempDir::new().unwrap();
187 let file_path = create_temp_file(&temp_dir, "empty.rs", "");
188 let result = AstParser::parse_file(&file_path);
189
190 assert!(result.is_ok());
191 let parsed = result.unwrap();
192 assert!(parsed.syntax_tree.items.is_empty());
193 }
194
195 #[test]
196 fn test_parse_files_batch() {
197 let temp_dir = TempDir::new().unwrap();
198
199 let valid_code1 = "pub fn hello() {}";
200 let valid_code2 = "pub struct World;";
201 let invalid_code = "pub fn broken( {";
202
203 let file1 = create_temp_file(&temp_dir, "file1.rs", valid_code1);
204 let file2 = create_temp_file(&temp_dir, "file2.rs", valid_code2);
205 let file3 = create_temp_file(&temp_dir, "file3.rs", invalid_code);
206
207 let paths = vec![file1.clone(), file2.clone(), file3.clone()];
208 let results = AstParser::parse_files(&paths);
209
210 assert_eq!(results.len(), 3);
211
212 assert!(results[0].is_ok());
214 assert!(results[1].is_ok());
215
216 assert!(results[2].is_err());
218
219 assert_eq!(results[0].as_ref().unwrap().path, file1);
221 assert_eq!(results[1].as_ref().unwrap().path, file2);
222 }
223
224 #[test]
225 fn test_parse_files_all_valid() {
226 let temp_dir = TempDir::new().unwrap();
227
228 let code1 = "pub fn func1() {}";
229 let code2 = "pub fn func2() {}";
230 let code3 = "pub fn func3() {}";
231
232 let file1 = create_temp_file(&temp_dir, "a.rs", code1);
233 let file2 = create_temp_file(&temp_dir, "b.rs", code2);
234 let file3 = create_temp_file(&temp_dir, "c.rs", code3);
235
236 let paths = vec![file1, file2, file3];
237 let results = AstParser::parse_files(&paths);
238
239 assert_eq!(results.len(), 3);
240 assert!(results.iter().all(|r| r.is_ok()));
241 }
242
243 #[test]
244 fn test_parse_files_all_invalid() {
245 let temp_dir = TempDir::new().unwrap();
246
247 let invalid1 = "pub fn broken( {";
248 let invalid2 = "struct Missing }";
249 let invalid3 = "let x = ;";
250
251 let file1 = create_temp_file(&temp_dir, "bad1.rs", invalid1);
252 let file2 = create_temp_file(&temp_dir, "bad2.rs", invalid2);
253 let file3 = create_temp_file(&temp_dir, "bad3.rs", invalid3);
254
255 let paths = vec![file1, file2, file3];
256 let results = AstParser::parse_files(&paths);
257
258 assert_eq!(results.len(), 3);
259 assert!(results.iter().all(|r| r.is_err()));
260 }
261
262 #[test]
263 fn test_parse_files_empty_list() {
264 let paths: Vec<PathBuf> = vec![];
265 let results = AstParser::parse_files(&paths);
266
267 assert_eq!(results.len(), 0);
268 }
269
270 #[test]
271 fn test_parse_file_with_complex_syntax() {
272 let temp_dir = TempDir::new().unwrap();
273 let complex_code = r#"
274 use serde::{Deserialize, Serialize};
275 use std::collections::HashMap;
276
277 #[derive(Debug, Serialize, Deserialize)]
278 pub struct User {
279 pub id: u32,
280 #[serde(rename = "userName")]
281 pub name: String,
282 pub email: Option<String>,
283 }
284
285 impl User {
286 pub fn new(id: u32, name: String) -> Self {
287 Self {
288 id,
289 name,
290 email: None,
291 }
292 }
293 }
294
295 pub async fn get_users() -> Vec<User> {
296 vec![]
297 }
298 "#;
299
300 let file_path = create_temp_file(&temp_dir, "complex.rs", complex_code);
301 let result = AstParser::parse_file(&file_path);
302
303 assert!(result.is_ok());
304 let parsed = result.unwrap();
305
306 assert!(parsed.syntax_tree.items.len() >= 4);
308 }
309}