actr_cli/commands/
doc.rs

1//! Doc command implementation - generate project documentation
2
3use crate::commands::Command;
4use crate::error::Result;
5use actr_config::{Config, ConfigParser};
6use async_trait::async_trait;
7use clap::Args;
8use std::path::Path;
9use tracing::{debug, info};
10
11#[derive(Args)]
12pub struct DocCommand {
13    /// Output directory for documentation (defaults to "./docs")
14    #[arg(short = 'o', long = "output")]
15    pub output_dir: Option<String>,
16}
17
18#[async_trait]
19impl Command for DocCommand {
20    async fn execute(&self) -> Result<()> {
21        let output_dir = self.output_dir.as_deref().unwrap_or("docs");
22
23        info!("📚 Generating project documentation to: {}", output_dir);
24
25        // Create output directory
26        std::fs::create_dir_all(output_dir)?;
27
28        // Load project configuration
29        let config = if Path::new("Actr.toml").exists() {
30            Some(ConfigParser::from_file("Actr.toml")?)
31        } else {
32            None
33        };
34
35        // Generate documentation files
36        self.generate_index_html(output_dir, &config).await?;
37        self.generate_api_html(output_dir, &config).await?;
38        self.generate_config_html(output_dir, &config).await?;
39
40        info!("✅ Documentation generated successfully");
41        info!("📄 Generated files:");
42        info!("  - {}/index.html (project overview)", output_dir);
43        info!("  - {}/api.html (API interface documentation)", output_dir);
44        info!(
45            "  - {}/config.html (configuration documentation)",
46            output_dir
47        );
48
49        Ok(())
50    }
51}
52
53impl DocCommand {
54    /// Generate project overview documentation
55    async fn generate_index_html(&self, output_dir: &str, config: &Option<Config>) -> Result<()> {
56        debug!("Generating index.html...");
57
58        let project_name = config
59            .as_ref()
60            .map(|c| c.package.name.as_str())
61            .unwrap_or("Actor-RTC Project");
62        // Config does not expose a version; fall back to Cargo.toml when available.
63        let project_version = Self::read_cargo_version().unwrap_or_else(|| "unknown".to_string());
64        let project_description = config
65            .as_ref()
66            .and_then(|c| c.package.description.as_ref())
67            .map(|s| s.as_str())
68            .unwrap_or("An Actor-RTC project");
69
70        let html_content = format!(
71            r#"<!DOCTYPE html>
72<html lang="en">
73<head>
74    <meta charset="UTF-8">
75    <meta name="viewport" content="width=device-width, initial-scale=1.0">
76    <title>{project_name} - Project Overview</title>
77    <style>
78        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; line-height: 1.6; }}
79        .header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
80        .content {{ max-width: 800px; margin: 0 auto; }}
81        .section {{ background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
82        .nav {{ display: flex; gap: 10px; margin: 20px 0; }}
83        .nav a {{ padding: 10px 20px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 4px; }}
84        .nav a:hover {{ background: #667eea; color: white; }}
85        h1, h2 {{ margin-top: 0; }}
86        .badge {{ background: #667eea; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }}
87    </style>
88</head>
89<body>
90    <div class="content">
91        <div class="header">
92            <h1>{project_name}</h1>
93            <p>{project_description}</p>
94            <span class="badge">v{project_version}</span>
95        </div>
96
97        <div class="nav">
98            <a href="index.html">Overview</a>
99            <a href="api.html">API Docs</a>
100            <a href="config.html">Configuration</a>
101        </div>
102
103        <div class="section">
104            <h2>Project Info</h2>
105            <p><strong>Name:</strong> {project_name}</p>
106            <p><strong>Version:</strong> {project_version}</p>
107            <p><strong>Description:</strong> {project_description}</p>
108        </div>
109
110        <div class="section">
111            <h2>Common Commands</h2>
112            <p>Run these from the project root:</p>
113            <pre><code># Generate code from proto files
114actr gen --input proto --output src/generated
115
116# Install dependencies from Actr.toml
117actr install
118
119# Discover services on the network
120actr discovery
121
122# Validate dependencies (currently a placeholder command)
123actr check --verbose</code></pre>
124        </div>
125
126        <div class="section">
127            <h2>Project Structure</h2>
128            <pre><code>{project_name}/
129├── Actr.toml          # Project configuration
130├── src/               # Source code
131│   ├── main.rs        # Entrypoint
132│   └── generated/     # Generated code
133├── protos/            # Protocol Buffers definitions
134└── docs/              # Project documentation</code></pre>
135        </div>
136
137        <div class="section">
138            <h2>Related Links</h2>
139            <ul>
140                <li><a href="api.html">API Documentation</a> - Service interface definitions</li>
141                <li><a href="config.html">Configuration</a> - Project configuration reference</li>
142            </ul>
143        </div>
144    </div>
145</body>
146</html>"#
147        );
148
149        let index_path = Path::new(output_dir).join("index.html");
150        std::fs::write(index_path, html_content)?;
151
152        Ok(())
153    }
154
155    /// Generate API documentation
156    async fn generate_api_html(&self, output_dir: &str, config: &Option<Config>) -> Result<()> {
157        debug!("Generating api.html...");
158
159        let project_name = config
160            .as_ref()
161            .map(|c| c.package.name.as_str())
162            .unwrap_or("Actor-RTC Project");
163
164        // Collect proto files information
165        let mut proto_info = Vec::new();
166        let proto_dir = Path::new("protos");
167
168        if proto_dir.exists()
169            && let Ok(entries) = std::fs::read_dir(proto_dir)
170        {
171            for entry in entries.flatten() {
172                let path = entry.path();
173                if path.extension().and_then(|s| s.to_str()) == Some("proto") {
174                    let filename = path.file_name().unwrap().to_string_lossy();
175                    let content = std::fs::read_to_string(&path).unwrap_or_default();
176                    proto_info.push((filename.to_string(), content));
177                }
178            }
179        }
180
181        let mut proto_sections = String::new();
182        if proto_info.is_empty() {
183            proto_sections.push_str(
184                r#"<div class="section">
185                <p>No Protocol Buffers files found.</p>
186            </div>"#,
187            );
188        } else {
189            for (filename, content) in proto_info {
190                proto_sections.push_str(&format!(
191                    r#"<div class="section">
192                    <h3>{}</h3>
193                    <pre><code>{}</code></pre>
194                </div>"#,
195                    filename,
196                    Self::html_escape(&content)
197                ));
198            }
199        }
200
201        let html_content = format!(
202            r#"<!DOCTYPE html>
203<html lang="en">
204<head>
205    <meta charset="UTF-8">
206    <meta name="viewport" content="width=device-width, initial-scale=1.0">
207    <title>{project_name} - API Documentation</title>
208    <style>
209        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; line-height: 1.6; }}
210        .header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
211        .content {{ max-width: 1000px; margin: 0 auto; }}
212        .section {{ background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
213        .nav {{ display: flex; gap: 10px; margin: 20px 0; }}
214        .nav a {{ padding: 10px 20px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 4px; }}
215        .nav a:hover {{ background: #667eea; color: white; }}
216        .nav a.active {{ background: #667eea; color: white; }}
217        h1, h2, h3 {{ margin-top: 0; }}
218        pre {{ background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto; }}
219        code {{ font-family: 'Monaco', 'Consolas', monospace; }}
220    </style>
221</head>
222<body>
223    <div class="content">
224        <div class="header">
225            <h1>{project_name} - API Documentation</h1>
226            <p>Service interfaces and protocol definitions</p>
227        </div>
228
229        <div class="nav">
230            <a href="index.html">Overview</a>
231            <a href="api.html" class="active">API Docs</a>
232            <a href="config.html">Configuration</a>
233        </div>
234
235        <div class="section">
236            <h2>Protocol Buffers Definitions</h2>
237            <p>Protocol Buffers files found in this project:</p>
238        </div>
239
240        {proto_sections}
241    </div>
242</body>
243</html>"#
244        );
245
246        let api_path = Path::new(output_dir).join("api.html");
247        std::fs::write(api_path, html_content)?;
248
249        Ok(())
250    }
251
252    /// Generate configuration documentation
253    async fn generate_config_html(&self, output_dir: &str, config: &Option<Config>) -> Result<()> {
254        debug!("Generating config.html...");
255
256        let project_name = config
257            .as_ref()
258            .map(|c| c.package.name.as_str())
259            .unwrap_or("Actor-RTC Project");
260
261        // Generate configuration example
262        // Note: Config doesn't implement Serialize, read raw Actr.toml instead
263        let config_example = if Path::new("Actr.toml").exists() {
264            std::fs::read_to_string("Actr.toml").unwrap_or_default()
265        } else {
266            r#"edition = 1
267exports = []
268
269[package]
270name = "my-actor-service"
271description = "An Actor-RTC service"
272authors = []
273license = "Apache-2.0"
274tags = ["latest"]
275
276[package.actr_type]
277manufacturer = "my-company"
278name = "my-actor-service"
279
280[dependencies]
281
282[system.signaling]
283url = "ws://127.0.0.1:8080"
284
285[system.deployment]
286realm_id = 1001
287
288[system.discovery]
289visible = true
290
291[scripts]
292dev = "cargo run"
293test = "cargo test""#
294                .to_string()
295        };
296
297        let html_content = format!(
298            r#"<!DOCTYPE html>
299<html lang="en">
300<head>
301    <meta charset="UTF-8">
302    <meta name="viewport" content="width=device-width, initial-scale=1.0">
303    <title>{} - Configuration</title>
304    <style>
305        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; line-height: 1.6; }}
306        .header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
307        .content {{ max-width: 1000px; margin: 0 auto; }}
308        .section {{ background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
309        .nav {{ display: flex; gap: 10px; margin: 20px 0; }}
310        .nav a {{ padding: 10px 20px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 4px; }}
311        .nav a:hover {{ background: #667eea; color: white; }}
312        .nav a.active {{ background: #667eea; color: white; }}
313        h1, h2, h3 {{ margin-top: 0; }}
314        pre {{ background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto; }}
315        code {{ font-family: 'Monaco', 'Consolas', monospace; background: #f0f0f0; padding: 2px 4px; border-radius: 2px; }}
316        .config-table {{ width: 100%; border-collapse: collapse; margin: 15px 0; }}
317        .config-table th, .config-table td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
318        .config-table th {{ background: #f5f5f5; font-weight: bold; }}
319    </style>
320</head>
321<body>
322    <div class="content">
323        <div class="header">
324            <h1>{} - Configuration</h1>
325            <p>Project configuration reference</p>
326        </div>
327
328        <div class="nav">
329            <a href="index.html">Overview</a>
330            <a href="api.html">API Docs</a>
331            <a href="config.html" class="active">Configuration</a>
332        </div>
333
334        <div class="section">
335            <h2>Configuration Layout</h2>
336            <p><code>Actr.toml</code> is the main configuration file for the project.</p>
337
338            <table class="config-table">
339                <tr>
340                    <th>Key</th>
341                    <th>Purpose</th>
342                    <th>Notes</th>
343                </tr>
344                <tr>
345                    <td><code>edition</code></td>
346                    <td>Config format version</td>
347                    <td>Optional</td>
348                </tr>
349                <tr>
350                    <td><code>inherit</code></td>
351                    <td>Parent config file path</td>
352                    <td>Optional</td>
353                </tr>
354                <tr>
355                    <td><code>exports</code></td>
356                    <td>Exported proto files for service specs</td>
357                    <td>Optional</td>
358                </tr>
359                <tr>
360                    <td><code>[package]</code></td>
361                    <td>Package metadata (name, description, authors, license, tags)</td>
362                    <td>Required</td>
363                </tr>
364                <tr>
365                    <td><code>[package.actr_type]</code></td>
366                    <td>Actor type definition (manufacturer, name)</td>
367                    <td>Required</td>
368                </tr>
369                <tr>
370                    <td><code>[dependencies]</code></td>
371                    <td>Dependency map (empty or fingerprinted entries)</td>
372                    <td>Optional</td>
373                </tr>
374                <tr>
375                    <td><code>[system.signaling]</code></td>
376                    <td>Signaling server configuration</td>
377                    <td>Optional</td>
378                </tr>
379                <tr>
380                    <td><code>[system.deployment]</code></td>
381                    <td>Deployment configuration</td>
382                    <td>Optional</td>
383                </tr>
384                <tr>
385                    <td><code>[system.discovery]</code></td>
386                    <td>Discovery configuration</td>
387                    <td>Optional</td>
388                </tr>
389                <tr>
390                    <td><code>[system.storage]</code></td>
391                    <td>Storage configuration (mailbox path)</td>
392                    <td>Optional</td>
393                </tr>
394                <tr>
395                    <td><code>[system.webrtc]</code></td>
396                    <td>WebRTC configuration (STUN/TURN/relay)</td>
397                    <td>Optional</td>
398                </tr>
399                <tr>
400                    <td><code>[system.observability]</code></td>
401                    <td>Tracing and logging configuration</td>
402                    <td>Optional</td>
403                </tr>
404                <tr>
405                    <td><code>[acl]</code> / <code>[[acl.rules]]</code></td>
406                    <td>Access control rules</td>
407                    <td>Optional</td>
408                </tr>
409                <tr>
410                    <td><code>[scripts]</code></td>
411                    <td>Custom script commands</td>
412                    <td>Optional</td>
413                </tr>
414            </table>
415        </div>
416
417        <div class="section">
418            <h2>Example</h2>
419            <pre><code>{}</code></pre>
420        </div>
421
422        <div class="section">
423            <h2>Managing Dependencies</h2>
424            <p>Use the install command to add or install dependencies:</p>
425            <pre><code># Add a dependency and update Actr.toml
426actr install user-service
427
428# Install dependencies listed in Actr.toml
429actr install</code></pre>
430        </div>
431
432        <div class="section">
433            <h2>Dependency Formats</h2>
434            <p>Define Protocol Buffers dependencies under <code>[dependencies]</code>:</p>
435            <pre><code># Local file path
436user_service = "protos/user.proto"
437
438# HTTP URL
439api_service = "https://example.com/api/service.proto"
440
441# Actor registry
442[dependencies.payment]
443name = "payment-service"
444actr_type = "payment"
445fingerprint = "sha256:a1b2c3d4..."</code></pre>
446        </div>
447    </div>
448</body>
449</html>"#,
450            project_name,
451            project_name,
452            Self::html_escape(&config_example)
453        );
454
455        let config_path = Path::new(output_dir).join("config.html");
456        std::fs::write(config_path, html_content)?;
457
458        Ok(())
459    }
460
461    /// Simple HTML escape function
462    fn html_escape(text: &str) -> String {
463        text.replace("&", "&amp;")
464            .replace("<", "&lt;")
465            .replace(">", "&gt;")
466            .replace("\"", "&quot;")
467            .replace("'", "&#x27;")
468    }
469
470    fn read_cargo_version() -> Option<String> {
471        let cargo_toml = std::fs::read_to_string("Cargo.toml").ok()?;
472        let value: toml::Value = cargo_toml.parse().ok()?;
473        value
474            .get("package")
475            .and_then(|package| package.get("version"))
476            .and_then(|version| version.as_str())
477            .map(|version| version.to_string())
478    }
479}