Skip to main content

torrust_tracker_deployer_lib/presentation/cli/controllers/docs/
handler.rs

1//! Docs Command Controller (Presentation Layer)
2//!
3//! Handles the presentation layer concerns for CLI JSON documentation generation,
4//! including user output and progress reporting.
5//!
6//! ## Architecture Note
7//!
8//! This controller directly uses infrastructure-layer services without going
9//! through the application layer. This is architecturally correct because:
10//!
11//! - CLI documentation generation is a **presentation concern** (self-documentation)
12//! - There is no business logic or orchestration (not a use case)
13//! - Application layer would be unnecessary indirection
14//!
15//! Compare with `create schema` which generates business DTOs - that correctly
16//! goes through application layer because it documents business configuration.
17
18use std::cell::RefCell;
19use std::path::PathBuf;
20use std::sync::Arc;
21
22use parking_lot::ReentrantMutex;
23
24use crate::infrastructure::cli_docs::CliDocsGenerator;
25use crate::presentation::cli::input::cli::Cli;
26use crate::presentation::cli::views::progress::ProgressReporter;
27use crate::presentation::cli::views::UserOutput;
28
29use super::errors::DocsCommandError;
30
31/// Steps for CLI documentation generation workflow
32enum DocsStep {
33    GenerateDocs,
34}
35
36impl DocsStep {
37    fn description(&self) -> &str {
38        match self {
39            Self::GenerateDocs => "Generating CLI JSON documentation",
40        }
41    }
42
43    fn count() -> usize {
44        1
45    }
46}
47
48/// Controller for docs command
49///
50/// Handles the presentation layer for CLI JSON documentation generation,
51/// coordinating between the command handler and user output.
52pub struct DocsCommandController {
53    progress: ProgressReporter,
54}
55
56impl DocsCommandController {
57    /// Create a new CLI documentation generation command controller
58    pub fn new(user_output: &Arc<ReentrantMutex<RefCell<UserOutput>>>) -> Self {
59        let progress = ProgressReporter::new(user_output.clone(), DocsStep::count());
60
61        Self { progress }
62    }
63
64    /// Execute the CLI documentation generation command
65    ///
66    /// Generates CLI JSON documentation and either writes to file or outputs to stdout.
67    ///
68    /// # Arguments
69    ///
70    /// * `output_path` - Optional path to write schema file. If `None`, outputs to stdout.
71    ///
72    /// # Returns
73    ///
74    /// Returns `Ok(())` on success, or error if generation or output fails.
75    ///
76    /// # Errors
77    ///
78    /// Returns error if:
79    /// - CLI documentation generation fails
80    /// - File write fails (when path provided)
81    /// - Parent directory creation fails (when path provided)
82    /// - Stdout write fails (when no path provided)
83    pub fn execute(&mut self, output_path: Option<&PathBuf>) -> Result<(), DocsCommandError> {
84        // Generate CLI documentation using infrastructure layer directly
85        let docs = CliDocsGenerator::generate::<Cli>()
86            .map_err(|source| DocsCommandError::SchemaGenerationFailed { source })?;
87
88        // Handle output based on destination
89        if let Some(path) = output_path {
90            // When writing to file, show progress to user
91            self.progress
92                .start_step(DocsStep::GenerateDocs.description())?;
93
94            // Create parent directories if needed
95            if let Some(parent) = path.parent() {
96                std::fs::create_dir_all(parent).map_err(|source| {
97                    DocsCommandError::DirectoryCreationFailed {
98                        path: parent.to_path_buf(),
99                        source,
100                    }
101                })?;
102            }
103
104            // Write documentation to file
105            std::fs::write(path, &docs).map_err(|source| DocsCommandError::FileWriteFailed {
106                path: path.clone(),
107                source,
108            })?;
109
110            self.progress
111                .complete_step(Some("CLI documentation written to file successfully"))?;
112            self.progress
113                .complete("CLI documentation generation completed successfully")?;
114        } else {
115            // When writing to stdout, only output the documentation (no progress messages)
116            // This enables clean piping: `cmd docs > file.json`
117            self.progress.result(&docs)?;
118        }
119
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::presentation::cli::views::testing::test_user_output::TestUserOutput;
128    use crate::presentation::cli::views::VerbosityLevel;
129    use tempfile::TempDir;
130
131    #[test]
132    fn it_should_generate_cli_schema_to_file_when_path_provided() {
133        let temp_dir = TempDir::new().unwrap();
134        let schema_path = temp_dir.path().join("docs.json");
135
136        let (user_output, _capture, _capture_stderr) =
137            TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
138        let mut controller = DocsCommandController::new(&user_output);
139
140        let result = controller.execute(Some(&schema_path));
141        assert!(result.is_ok());
142
143        // Verify file was created
144        assert!(schema_path.exists());
145
146        // Verify file contains valid CLI documentation
147        let content = std::fs::read_to_string(&schema_path).unwrap();
148        assert!(content.contains("\"format\""));
149        assert!(content.contains("\"format_version\""));
150        assert!(content.contains("\"cli\""));
151    }
152
153    #[test]
154    fn it_should_complete_progress_when_generating_cli_schema() {
155        let (user_output, _capture, _capture_stderr) =
156            TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
157        let mut controller = DocsCommandController::new(&user_output);
158
159        let temp_dir = TempDir::new().unwrap();
160        let schema_path = temp_dir.path().join("docs.json");
161
162        let result = controller.execute(Some(&schema_path));
163        assert!(result.is_ok());
164    }
165
166    #[test]
167    fn it_should_output_to_stdout_when_no_path_provided() {
168        let (user_output, capture, _capture_stderr) =
169            TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
170        let mut controller = DocsCommandController::new(&user_output);
171
172        let result = controller.execute(None);
173        assert!(result.is_ok());
174
175        // Verify documentation was written to stdout
176        let output = String::from_utf8(capture.lock().clone()).unwrap();
177        assert!(output.contains("\"format\""));
178        assert!(output.contains("\"format_version\""));
179        assert!(output.contains("\"cli\""));
180    }
181
182    #[test]
183    fn it_should_generate_valid_cli_schema_structure() {
184        let (user_output, capture, _capture_stderr) =
185            TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
186        let mut controller = DocsCommandController::new(&user_output);
187
188        let result = controller.execute(None);
189        assert!(result.is_ok());
190
191        let output = String::from_utf8(capture.lock().clone()).unwrap();
192        let json: serde_json::Value = serde_json::from_str(&output).unwrap();
193
194        // Verify CLI documentation structure
195        assert!(json.get("format").is_some());
196        assert_eq!(json.get("format").unwrap(), "cli-documentation");
197        assert!(json.get("format_version").is_some());
198        assert!(json.get("cli").is_some());
199
200        let cli = json.get("cli").unwrap();
201        assert!(cli.get("name").is_some());
202        assert!(cli.get("version").is_some());
203        assert!(cli.get("commands").is_some());
204    }
205}