ricecoder_github/managers/
documentation_generator.rs1use crate::errors::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tracing::{debug, info};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DocumentationSection {
11 pub title: String,
13 pub content: String,
15 pub order: u32,
17}
18
19impl DocumentationSection {
20 pub fn new(title: impl Into<String>, content: impl Into<String>, order: u32) -> Self {
22 Self {
23 title: title.into(),
24 content: content.into(),
25 order,
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ApiDocumentation {
33 pub name: String,
35 pub signature: String,
37 pub documentation: String,
39 pub parameters: Vec<ApiParameter>,
41 pub return_type: String,
43 pub examples: Vec<String>,
45}
46
47impl ApiDocumentation {
48 pub fn new(name: impl Into<String>, signature: impl Into<String>) -> Self {
50 Self {
51 name: name.into(),
52 signature: signature.into(),
53 documentation: String::new(),
54 parameters: Vec::new(),
55 return_type: String::new(),
56 examples: Vec::new(),
57 }
58 }
59
60 pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
62 self.documentation = doc.into();
63 self
64 }
65
66 pub fn with_parameter(mut self, param: ApiParameter) -> Self {
68 self.parameters.push(param);
69 self
70 }
71
72 pub fn with_return_type(mut self, return_type: impl Into<String>) -> Self {
74 self.return_type = return_type.into();
75 self
76 }
77
78 pub fn with_example(mut self, example: impl Into<String>) -> Self {
80 self.examples.push(example.into());
81 self
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ApiParameter {
88 pub name: String,
90 pub param_type: String,
92 pub description: String,
94}
95
96impl ApiParameter {
97 pub fn new(
99 name: impl Into<String>,
100 param_type: impl Into<String>,
101 description: impl Into<String>,
102 ) -> Self {
103 Self {
104 name: name.into(),
105 param_type: param_type.into(),
106 description: description.into(),
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct DocumentationCoverage {
114 pub total_items: u32,
116 pub documented_items: u32,
118 pub coverage_percentage: f32,
120 pub gaps: Vec<String>,
122}
123
124impl DocumentationCoverage {
125 pub fn new(total_items: u32, documented_items: u32) -> Self {
127 let coverage_percentage = if total_items > 0 {
128 (documented_items as f32 / total_items as f32) * 100.0
129 } else {
130 100.0
131 };
132
133 Self {
134 total_items,
135 documented_items,
136 coverage_percentage,
137 gaps: Vec::new(),
138 }
139 }
140
141 pub fn with_gap(mut self, gap: impl Into<String>) -> Self {
143 self.gaps.push(gap.into());
144 self
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ReadmeConfig {
151 pub project_name: String,
153 pub description: String,
155 pub include_toc: bool,
157 pub include_installation: bool,
159 pub include_usage: bool,
161 pub include_api: bool,
163 pub include_contributing: bool,
165 pub include_license: bool,
167}
168
169impl Default for ReadmeConfig {
170 fn default() -> Self {
171 Self {
172 project_name: "Project".to_string(),
173 description: String::new(),
174 include_toc: true,
175 include_installation: true,
176 include_usage: true,
177 include_api: true,
178 include_contributing: true,
179 include_license: true,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct SyncResult {
187 pub files_updated: Vec<String>,
189 pub files_added: Vec<String>,
191 pub files_deleted: Vec<String>,
193 pub success: bool,
195 pub error: Option<String>,
197}
198
199impl SyncResult {
200 pub fn success() -> Self {
202 Self {
203 files_updated: Vec::new(),
204 files_added: Vec::new(),
205 files_deleted: Vec::new(),
206 success: true,
207 error: None,
208 }
209 }
210
211 pub fn failure(error: impl Into<String>) -> Self {
213 Self {
214 files_updated: Vec::new(),
215 files_added: Vec::new(),
216 files_deleted: Vec::new(),
217 success: false,
218 error: Some(error.into()),
219 }
220 }
221
222 pub fn with_updated_file(mut self, file: impl Into<String>) -> Self {
224 self.files_updated.push(file.into());
225 self
226 }
227
228 pub fn with_added_file(mut self, file: impl Into<String>) -> Self {
230 self.files_added.push(file.into());
231 self
232 }
233
234 pub fn with_deleted_file(mut self, file: impl Into<String>) -> Self {
236 self.files_deleted.push(file.into());
237 self
238 }
239}
240
241#[derive(Debug, Clone)]
243pub struct DocumentationGenerator {
244 pub readme_config: ReadmeConfig,
246 pub sections: HashMap<String, DocumentationSection>,
248 pub api_docs: HashMap<String, ApiDocumentation>,
250}
251
252impl DocumentationGenerator {
253 pub fn new(config: ReadmeConfig) -> Self {
255 Self {
256 readme_config: config,
257 sections: HashMap::new(),
258 api_docs: HashMap::new(),
259 }
260 }
261
262 pub fn generate_readme(&self) -> Result<String> {
264 debug!("Generating README for project: {}", self.readme_config.project_name);
265
266 let mut readme = String::new();
267
268 readme.push_str(&format!("# {}\n\n", self.readme_config.project_name));
270
271 if !self.readme_config.description.is_empty() {
273 readme.push_str(&format!("{}\n\n", self.readme_config.description));
274 }
275
276 if self.readme_config.include_toc {
278 readme.push_str("## Table of Contents\n\n");
279 if self.readme_config.include_installation {
280 readme.push_str("- [Installation](#installation)\n");
281 }
282 if self.readme_config.include_usage {
283 readme.push_str("- [Usage](#usage)\n");
284 }
285 if self.readme_config.include_api {
286 readme.push_str("- [API Documentation](#api-documentation)\n");
287 }
288 if self.readme_config.include_contributing {
289 readme.push_str("- [Contributing](#contributing)\n");
290 }
291 if self.readme_config.include_license {
292 readme.push_str("- [License](#license)\n");
293 }
294 readme.push('\n');
295 }
296
297 if self.readme_config.include_installation {
299 readme.push_str("## Installation\n\n");
300 readme.push_str("Add this to your `Cargo.toml`:\n\n");
301 readme.push_str("```toml\n");
302 readme.push_str(&format!("{} = \"0.1\"\n", self.readme_config.project_name.to_lowercase()));
303 readme.push_str("```\n\n");
304 }
305
306 if self.readme_config.include_usage {
308 readme.push_str("## Usage\n\n");
309 readme.push_str("```rust\n");
310 readme.push_str("// Example usage\n");
311 readme.push_str("```\n\n");
312 }
313
314 if self.readme_config.include_api && !self.api_docs.is_empty() {
316 readme.push_str("## API Documentation\n\n");
317 for api_doc in self.api_docs.values() {
318 readme.push_str(&format!("### {}\n\n", api_doc.name));
319 readme.push_str(&format!("```rust\n{}\n```\n\n", api_doc.signature));
320 if !api_doc.documentation.is_empty() {
321 readme.push_str(&format!("{}\n\n", api_doc.documentation));
322 }
323 }
324 }
325
326 if self.readme_config.include_contributing {
328 readme.push_str("## Contributing\n\n");
329 readme.push_str("Contributions are welcome! Please see CONTRIBUTING.md for details.\n\n");
330 }
331
332 if self.readme_config.include_license {
334 readme.push_str("## License\n\n");
335 readme.push_str("This project is licensed under the MIT License.\n");
336 }
337
338 info!("README generated successfully");
339 Ok(readme)
340 }
341
342 pub fn extract_api_documentation(&mut self, code: &str) -> Result<Vec<ApiDocumentation>> {
344 debug!("Extracting API documentation from code");
345
346 let mut api_docs = Vec::new();
347
348 let lines: Vec<&str> = code.lines().collect();
350 let mut i = 0;
351
352 while i < lines.len() {
353 let line = lines[i];
354
355 if line.trim().starts_with("///") {
357 let mut doc_comment = String::new();
358 let mut j = i;
359
360 while j < lines.len() && lines[j].trim().starts_with("///") {
362 let content = lines[j].trim_start_matches("///").trim();
363 doc_comment.push_str(content);
364 doc_comment.push('\n');
365 j += 1;
366 }
367
368 if j < lines.len() {
370 let sig_line = lines[j];
371 if sig_line.trim().starts_with("pub fn ") || sig_line.trim().starts_with("fn ") {
372 if let Some(start) = sig_line.find("fn ") {
374 let after_fn = &sig_line[start + 3..];
375 if let Some(paren_pos) = after_fn.find('(') {
376 let func_name = after_fn[..paren_pos].trim();
377 let api_doc = ApiDocumentation::new(func_name, sig_line.trim())
378 .with_documentation(doc_comment);
379 api_docs.push(api_doc);
380 }
381 }
382 }
383 }
384
385 i = j;
386 } else {
387 i += 1;
388 }
389 }
390
391 info!("Extracted {} API documentation items", api_docs.len());
392 Ok(api_docs)
393 }
394
395 pub fn synchronize_documentation(&self, old_code: &str, new_code: &str) -> Result<SyncResult> {
397 debug!("Synchronizing documentation with code changes");
398
399 let mut result = SyncResult::success();
400
401 if old_code == new_code {
403 debug!("No code changes detected");
404 return Ok(result);
405 }
406
407 let mut generator = DocumentationGenerator::new(self.readme_config.clone());
409 let new_api_docs = generator.extract_api_documentation(new_code)?;
410
411 if !new_api_docs.is_empty() {
413 result = result.with_updated_file("API_DOCUMENTATION.md");
414 }
415
416 info!("Documentation synchronized successfully");
417 Ok(result)
418 }
419
420 pub fn calculate_coverage(&self, code: &str) -> Result<DocumentationCoverage> {
422 debug!("Calculating documentation coverage");
423
424 let total_functions = code.matches("fn ").count() as u32;
426
427 let documented_functions = code.matches("///").count() as u32 / 3; let mut coverage = DocumentationCoverage::new(total_functions, documented_functions);
431
432 let lines: Vec<&str> = code.lines().collect();
434 for i in 0..lines.len() {
435 let line = lines[i];
436 if line.trim().starts_with("pub fn ") && (i == 0 || !lines[i - 1].trim().starts_with("///")) {
437 if let Some(start) = line.find("fn ") {
438 let after_fn = &line[start + 3..];
439 if let Some(paren_pos) = after_fn.find('(') {
440 let func_name = after_fn[..paren_pos].trim();
441 coverage = coverage.with_gap(format!("Function '{}' is not documented", func_name));
442 }
443 }
444 }
445 }
446
447 info!("Documentation coverage: {:.1}%", coverage.coverage_percentage);
448 Ok(coverage)
449 }
450
451 pub fn add_section(&mut self, key: impl Into<String>, section: DocumentationSection) {
453 self.sections.insert(key.into(), section);
454 }
455
456 pub fn add_api_documentation(&mut self, key: impl Into<String>, api_doc: ApiDocumentation) {
458 self.api_docs.insert(key.into(), api_doc);
459 }
460
461 pub fn get_sorted_sections(&self) -> Vec<&DocumentationSection> {
463 let mut sections: Vec<_> = self.sections.values().collect();
464 sections.sort_by_key(|s| s.order);
465 sections
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_readme_generation() {
475 let config = ReadmeConfig {
476 project_name: "TestProject".to_string(),
477 description: "A test project".to_string(),
478 include_toc: true,
479 include_installation: true,
480 include_usage: true,
481 include_api: false,
482 include_contributing: true,
483 include_license: true,
484 };
485
486 let generator = DocumentationGenerator::new(config);
487 let readme = generator.generate_readme().expect("Failed to generate README");
488
489 assert!(readme.contains("# TestProject"));
490 assert!(readme.contains("A test project"));
491 assert!(readme.contains("## Table of Contents"));
492 assert!(readme.contains("## Installation"));
493 assert!(readme.contains("## Usage"));
494 assert!(readme.contains("## Contributing"));
495 assert!(readme.contains("## License"));
496 }
497
498 #[test]
499 fn test_api_documentation_extraction() {
500 let code = r#"
501/// This is a test function
502/// It does something useful
503pub fn test_function(x: i32) -> i32 {
504 x * 2
505}
506
507/// Another function
508pub fn another_function() {
509 println!("Hello");
510}
511"#;
512
513 let mut generator = DocumentationGenerator::new(ReadmeConfig::default());
514 let api_docs = generator.extract_api_documentation(code).expect("Failed to extract API docs");
515
516 assert!(!api_docs.is_empty());
517 assert!(api_docs.iter().any(|doc| doc.name.contains("test_function")));
518 }
519
520 #[test]
521 fn test_documentation_coverage() {
522 let code = r#"
523/// Documented function
524pub fn documented() {}
525
526pub fn undocumented() {}
527"#;
528
529 let generator = DocumentationGenerator::new(ReadmeConfig::default());
530 let coverage = generator.calculate_coverage(code).expect("Failed to calculate coverage");
531
532 assert!(coverage.total_items > 0);
533 assert!(coverage.coverage_percentage >= 0.0 && coverage.coverage_percentage <= 100.0);
534 }
535
536 #[test]
537 fn test_sync_result_builder() {
538 let result = SyncResult::success()
539 .with_updated_file("file1.md")
540 .with_added_file("file2.md")
541 .with_deleted_file("file3.md");
542
543 assert!(result.success);
544 assert_eq!(result.files_updated.len(), 1);
545 assert_eq!(result.files_added.len(), 1);
546 assert_eq!(result.files_deleted.len(), 1);
547 }
548
549 #[test]
550 fn test_documentation_coverage_calculation() {
551 let coverage = DocumentationCoverage::new(10, 7);
552 assert_eq!(coverage.total_items, 10);
553 assert_eq!(coverage.documented_items, 7);
554 assert!((coverage.coverage_percentage - 70.0).abs() < 0.1);
555 }
556}