1use 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 #[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 std::fs::create_dir_all(output_dir)?;
27
28 let config = if Path::new("Actr.toml").exists() {
30 Some(ConfigParser::from_file("Actr.toml")?)
31 } else {
32 None
33 };
34
35 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 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 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 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 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 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 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 fn html_escape(text: &str) -> String {
463 text.replace("&", "&")
464 .replace("<", "<")
465 .replace(">", ">")
466 .replace("\"", """)
467 .replace("'", "'")
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}