ricecoder_github/managers/
documentation_generator.rs

1//! Documentation Generator - Generates and maintains project documentation
2
3use crate::errors::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tracing::{debug, info};
7
8/// Documentation section
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DocumentationSection {
11    /// Section title
12    pub title: String,
13    /// Section content
14    pub content: String,
15    /// Section order (for sorting)
16    pub order: u32,
17}
18
19impl DocumentationSection {
20    /// Create a new documentation section
21    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/// API documentation for a function or method
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ApiDocumentation {
33    /// Function/method name
34    pub name: String,
35    /// Function signature
36    pub signature: String,
37    /// Documentation comment
38    pub documentation: String,
39    /// Parameters
40    pub parameters: Vec<ApiParameter>,
41    /// Return type
42    pub return_type: String,
43    /// Examples
44    pub examples: Vec<String>,
45}
46
47impl ApiDocumentation {
48    /// Create new API documentation
49    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    /// Add documentation
61    pub fn with_documentation(mut self, doc: impl Into<String>) -> Self {
62        self.documentation = doc.into();
63        self
64    }
65
66    /// Add a parameter
67    pub fn with_parameter(mut self, param: ApiParameter) -> Self {
68        self.parameters.push(param);
69        self
70    }
71
72    /// Set return type
73    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    /// Add an example
79    pub fn with_example(mut self, example: impl Into<String>) -> Self {
80        self.examples.push(example.into());
81        self
82    }
83}
84
85/// API parameter documentation
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ApiParameter {
88    /// Parameter name
89    pub name: String,
90    /// Parameter type
91    pub param_type: String,
92    /// Parameter description
93    pub description: String,
94}
95
96impl ApiParameter {
97    /// Create new API parameter
98    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/// Documentation coverage metrics
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct DocumentationCoverage {
114    /// Total items (functions, modules, etc.)
115    pub total_items: u32,
116    /// Documented items
117    pub documented_items: u32,
118    /// Coverage percentage (0-100)
119    pub coverage_percentage: f32,
120    /// Gaps (undocumented items)
121    pub gaps: Vec<String>,
122}
123
124impl DocumentationCoverage {
125    /// Create new coverage metrics
126    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    /// Add a gap
142    pub fn with_gap(mut self, gap: impl Into<String>) -> Self {
143        self.gaps.push(gap.into());
144        self
145    }
146}
147
148/// README generation configuration
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ReadmeConfig {
151    /// Project name
152    pub project_name: String,
153    /// Project description
154    pub description: String,
155    /// Include table of contents
156    pub include_toc: bool,
157    /// Include installation section
158    pub include_installation: bool,
159    /// Include usage section
160    pub include_usage: bool,
161    /// Include API documentation
162    pub include_api: bool,
163    /// Include contributing section
164    pub include_contributing: bool,
165    /// Include license section
166    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/// Documentation synchronization result
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct SyncResult {
187    /// Files updated
188    pub files_updated: Vec<String>,
189    /// Files added
190    pub files_added: Vec<String>,
191    /// Files deleted
192    pub files_deleted: Vec<String>,
193    /// Synchronization successful
194    pub success: bool,
195    /// Error message if failed
196    pub error: Option<String>,
197}
198
199impl SyncResult {
200    /// Create a successful sync result
201    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    /// Create a failed sync result
212    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    /// Add updated file
223    pub fn with_updated_file(mut self, file: impl Into<String>) -> Self {
224        self.files_updated.push(file.into());
225        self
226    }
227
228    /// Add added file
229    pub fn with_added_file(mut self, file: impl Into<String>) -> Self {
230        self.files_added.push(file.into());
231        self
232    }
233
234    /// Add deleted file
235    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/// Documentation Generator
242#[derive(Debug, Clone)]
243pub struct DocumentationGenerator {
244    /// README configuration
245    pub readme_config: ReadmeConfig,
246    /// Documentation sections
247    pub sections: HashMap<String, DocumentationSection>,
248    /// API documentation
249    pub api_docs: HashMap<String, ApiDocumentation>,
250}
251
252impl DocumentationGenerator {
253    /// Create a new documentation generator
254    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    /// Generate README from project structure
263    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        // Title
269        readme.push_str(&format!("# {}\n\n", self.readme_config.project_name));
270
271        // Description
272        if !self.readme_config.description.is_empty() {
273            readme.push_str(&format!("{}\n\n", self.readme_config.description));
274        }
275
276        // Table of Contents
277        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        // Installation
298        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        // Usage
307        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        // API Documentation
315        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        // Contributing
327        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        // License
333        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    /// Extract API documentation from code
343    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        // Simple pattern matching for Rust doc comments and function signatures
349        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            // Look for doc comments
356            if line.trim().starts_with("///") {
357                let mut doc_comment = String::new();
358                let mut j = i;
359
360                // Collect all consecutive doc comments
361                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                // Look for function signature after doc comment
369                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                        // Extract function name
373                        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    /// Synchronize documentation with code changes
396    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        // Check if code has changed
402        if old_code == new_code {
403            debug!("No code changes detected");
404            return Ok(result);
405        }
406
407        // Extract API docs from new code
408        let mut generator = DocumentationGenerator::new(self.readme_config.clone());
409        let new_api_docs = generator.extract_api_documentation(new_code)?;
410
411        // Mark documentation as updated
412        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    /// Calculate documentation coverage
421    pub fn calculate_coverage(&self, code: &str) -> Result<DocumentationCoverage> {
422        debug!("Calculating documentation coverage");
423
424        // Count functions in code
425        let total_functions = code.matches("fn ").count() as u32;
426
427        // Count documented functions (with ///)
428        let documented_functions = code.matches("///").count() as u32 / 3; // Rough estimate
429
430        let mut coverage = DocumentationCoverage::new(total_functions, documented_functions);
431
432        // Identify gaps
433        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    /// Add a documentation section
452    pub fn add_section(&mut self, key: impl Into<String>, section: DocumentationSection) {
453        self.sections.insert(key.into(), section);
454    }
455
456    /// Add API documentation
457    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    /// Get all sections sorted by order
462    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}