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 = "0.1.0";
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="zh">
73<head>
74 <meta charset="UTF-8">
75 <meta name="viewport" content="width=device-width, initial-scale=1.0">
76 <title>{project_name} - 项目概览</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">项目概览</a>
99 <a href="api.html">API 文档</a>
100 <a href="config.html">配置说明</a>
101 </div>
102
103 <div class="section">
104 <h2>📋 项目信息</h2>
105 <p><strong>名称:</strong> {project_name}</p>
106 <p><strong>版本:</strong> {project_version}</p>
107 <p><strong>描述:</strong> {project_description}</p>
108 </div>
109
110 <div class="section">
111 <h2>🚀 快速开始</h2>
112 <p>这是一个基于 Actor-RTC 框架的项目。以下是一些常用的开发命令:</p>
113 <pre><code># 生成代码
114actr gen --input proto --output src/generated
115
116# 运行项目
117actr run
118
119# 安装依赖
120actr install
121
122# 检查配置
123actr check</code></pre>
124 </div>
125
126 <div class="section">
127 <h2>📁 项目结构</h2>
128 <pre><code>{project_name}/
129├── Actr.toml # 项目配置文件
130├── src/ # 源代码目录
131│ ├── main.rs # 程序入口点
132│ └── generated/ # 自动生成的代码
133├── proto/ # Protocol Buffers 定义
134└── docs/ # 项目文档</code></pre>
135 </div>
136
137 <div class="section">
138 <h2>🔗 相关链接</h2>
139 <ul>
140 <li><a href="api.html">API 接口文档</a> - 查看服务接口定义</li>
141 <li><a href="config.html">配置说明</a> - 了解项目配置选项</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("proto");
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>暂无 Protocol Buffers 定义文件。</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="zh">
204<head>
205 <meta charset="UTF-8">
206 <meta name="viewport" content="width=device-width, initial-scale=1.0">
207 <title>{project_name} - API 文档</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 接口文档</h1>
226 <p>服务接口定义和协议规范</p>
227 </div>
228
229 <div class="nav">
230 <a href="index.html">项目概览</a>
231 <a href="api.html" class="active">API 文档</a>
232 <a href="config.html">配置说明</a>
233 </div>
234
235 <div class="section">
236 <h2>📋 Protocol Buffers 定义</h2>
237 <p>以下是项目中定义的 Protocol Buffers 文件:</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#"[project]
267name = "my-actor-service"
268version = "0.1.0"
269description = "An example Actor-RTC service"
270
271[build]
272output_dir = "generated"
273
274[dependencies]
275# Add your proto dependencies here
276
277[system.signaling]
278url = "ws://localhost:8081"
279
280[scripts]
281run = "cargo run"
282build = "cargo build"
283test = "cargo test""#
284 .to_string()
285 };
286
287 let html_content = format!(
288 r#"<!DOCTYPE html>
289<html lang="zh">
290<head>
291 <meta charset="UTF-8">
292 <meta name="viewport" content="width=device-width, initial-scale=1.0">
293 <title>{} - 配置说明</title>
294 <style>
295 body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; line-height: 1.6; }}
296 .header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }}
297 .content {{ max-width: 1000px; margin: 0 auto; }}
298 .section {{ background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
299 .nav {{ display: flex; gap: 10px; margin: 20px 0; }}
300 .nav a {{ padding: 10px 20px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 4px; }}
301 .nav a:hover {{ background: #667eea; color: white; }}
302 .nav a.active {{ background: #667eea; color: white; }}
303 h1, h2, h3 {{ margin-top: 0; }}
304 pre {{ background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto; }}
305 code {{ font-family: 'Monaco', 'Consolas', monospace; background: #f0f0f0; padding: 2px 4px; border-radius: 2px; }}
306 .config-table {{ width: 100%; border-collapse: collapse; margin: 15px 0; }}
307 .config-table th, .config-table td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
308 .config-table th {{ background: #f5f5f5; font-weight: bold; }}
309 </style>
310</head>
311<body>
312 <div class="content">
313 <div class="header">
314 <h1>{} - 配置说明</h1>
315 <p>项目配置选项和使用说明</p>
316 </div>
317
318 <div class="nav">
319 <a href="index.html">项目概览</a>
320 <a href="api.html">API 文档</a>
321 <a href="config.html" class="active">配置说明</a>
322 </div>
323
324 <div class="section">
325 <h2>📋 配置文件结构</h2>
326 <p><code>Actr.toml</code> 是项目的核心配置文件,包含以下主要部分:</p>
327
328 <table class="config-table">
329 <tr>
330 <th>配置段</th>
331 <th>作用</th>
332 <th>必需</th>
333 </tr>
334 <tr>
335 <td><code>[project]</code></td>
336 <td>项目基本信息(名称、版本、描述等)</td>
337 <td>是</td>
338 </tr>
339 <tr>
340 <td><code>[build]</code></td>
341 <td>构建配置(输出目录等)</td>
342 <td>是</td>
343 </tr>
344 <tr>
345 <td><code>[dependencies]</code></td>
346 <td>Protocol Buffers 依赖定义</td>
347 <td>否</td>
348 </tr>
349 <tr>
350 <td><code>[system.signaling]</code></td>
351 <td>信令服务器配置</td>
352 <td>否</td>
353 </tr>
354 <tr>
355 <td><code>[system.routing]</code></td>
356 <td>高级路由规则配置</td>
357 <td>否</td>
358 </tr>
359 <tr>
360 <td><code>[scripts]</code></td>
361 <td>自定义脚本命令</td>
362 <td>否</td>
363 </tr>
364 </table>
365 </div>
366
367 <div class="section">
368 <h2>⚙️ 配置示例</h2>
369 <pre><code>{}</code></pre>
370 </div>
371
372 <div class="section">
373 <h2>🔧 配置管理命令</h2>
374 <p>使用 <code>actr config</code> 命令可以方便地管理项目配置:</p>
375 <pre><code># 设置配置值
376actr config set project.description "我的Actor服务"
377actr config set system.signaling.url "wss://signal.example.com"
378
379# 查看配置值
380actr config get project.name
381actr config list
382
383# 查看完整配置
384actr config show
385
386# 删除配置项
387actr config unset system.signaling.url</code></pre>
388 </div>
389
390 <div class="section">
391 <h2>📝 依赖配置</h2>
392 <p>在 <code>[dependencies]</code> 段中配置 Protocol Buffers 依赖:</p>
393 <pre><code># 本地文件路径
394user_service = "proto/user.proto"
395
396# HTTP URL
397api_service = "https://example.com/api/service.proto"
398
399# Actor 注册表
400[dependencies.payment]
401uri = "actr://101:acme+payment-service@v1/payment.proto"
402fingerprint = "sha256:a1b2c3d4..."</code></pre>
403 </div>
404 </div>
405</body>
406</html>"#,
407 project_name,
408 project_name,
409 Self::html_escape(&config_example)
410 );
411
412 let config_path = Path::new(output_dir).join("config.html");
413 std::fs::write(config_path, html_content)?;
414
415 Ok(())
416 }
417
418 fn html_escape(text: &str) -> String {
420 text.replace("&", "&")
421 .replace("<", "<")
422 .replace(">", ">")
423 .replace("\"", """)
424 .replace("'", "'")
425 }
426}