1use crate::model::ProbeResult;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum DocFormat {
6 Markdown,
8 Html,
10 OpenApi,
12 JsonSchema,
14}
15
16pub fn generate_api_docs(result: &ProbeResult, format: DocFormat) -> String {
47 match format {
48 DocFormat::Markdown => generate_markdown(result),
49 DocFormat::Html => generate_html(result),
50 DocFormat::OpenApi => generate_openapi(result),
51 DocFormat::JsonSchema => generate_json_schema(result),
52 }
53}
54
55fn generate_markdown(result: &ProbeResult) -> String {
57 let mut doc = String::new();
58
59 doc.push_str(&format!("# {}\n\n", result.command));
61
62 if !result.raw_stdout.is_empty() {
64 doc.push_str("## Description\n\n");
65 let desc_lines: Vec<&str> = result.raw_stdout.lines().take(5).collect();
67 doc.push_str(&desc_lines.join("\n"));
68 doc.push_str("\n\n");
69 }
70
71 if !result.usage_blocks.is_empty() {
73 doc.push_str("## Usage\n\n");
74 for usage in &result.usage_blocks {
75 doc.push_str("```\n");
76 doc.push_str(usage);
77 doc.push_str("\n```\n\n");
78 }
79 }
80
81 if !result.options.is_empty() {
83 doc.push_str("## Options\n\n");
84 doc.push_str("| Flag | Description | Type | Required | Default |\n");
85 doc.push_str("|------|-------------|------|----------|----------|\n");
86 for opt in &result.options {
87 let flags = format!(
88 "{}, {}",
89 opt.short_flags.join(", "),
90 opt.long_flags.join(", ")
91 );
92 let desc = opt.description.as_deref().unwrap_or("").replace("|", "\\|");
93 let opt_type = format!("{:?}", opt.option_type);
94 let required = if opt.required { "Yes" } else { "No" };
95 let default = opt.default_value.as_deref().unwrap_or("-");
96 doc.push_str(&format!(
97 "| {} | {} | {} | {} | {} |\n",
98 flags, desc, opt_type, required, default
99 ));
100 }
101 doc.push_str("\n");
102 }
103
104 if !result.arguments.is_empty() {
106 doc.push_str("## Arguments\n\n");
107 doc.push_str("| Name | Description | Type | Required |\n");
108 doc.push_str("|------|-------------|------|----------|\n");
109 for arg in &result.arguments {
110 let desc = arg.description.as_deref().unwrap_or("").replace("|", "\\|");
111 let arg_type = arg
112 .arg_type
113 .as_ref()
114 .map(|t| format!("{:?}", t))
115 .unwrap_or_else(|| "String".to_string());
116 let required = if arg.required { "Yes" } else { "No" };
117 doc.push_str(&format!(
118 "| {} | {} | {} | {} |\n",
119 arg.name, desc, arg_type, required
120 ));
121 }
122 doc.push_str("\n");
123 }
124
125 if !result.subcommands.is_empty() {
127 doc.push_str("## Subcommands\n\n");
128 for subcmd in &result.subcommands {
129 doc.push_str(&format!("### {}\n\n", subcmd.name));
130 if let Some(desc) = &subcmd.description {
131 doc.push_str(&format!("{}\n\n", desc));
132 }
133 }
134 doc.push_str("\n");
135 }
136
137 if !result.environment_variables.is_empty() {
139 doc.push_str("## Environment Variables\n\n");
140 for env_var in &result.environment_variables {
141 doc.push_str(&format!("### {}\n\n", env_var.name));
142 if let Some(desc) = &env_var.description {
143 doc.push_str(&format!("{}\n\n", desc));
144 }
145 if let Some(opt) = &env_var.option_mapped {
146 doc.push_str(&format!("Maps to: `{}`\n\n", opt));
147 }
148 if let Some(default) = &env_var.default_value {
149 doc.push_str(&format!("Default: `{}`\n\n", default));
150 }
151 }
152 }
153
154 if !result.validation_rules.is_empty() {
156 doc.push_str("## Validation Rules\n\n");
157 for rule in &result.validation_rules {
158 doc.push_str(&format!("### {}\n\n", rule.target));
159 doc.push_str(&format!("Type: {:?}\n\n", rule.rule_type));
160 if let Some(pattern) = &rule.pattern {
161 doc.push_str(&format!("Pattern: `{}`\n\n", pattern));
162 }
163 if let Some(min) = rule.min {
164 doc.push_str(&format!("Min: {}\n\n", min));
165 }
166 if let Some(max) = rule.max {
167 doc.push_str(&format!("Max: {}\n\n", max));
168 }
169 if let Some(msg) = &rule.message {
170 doc.push_str(&format!("Message: {}\n\n", msg));
171 }
172 }
173 }
174
175 if !result.examples.is_empty() {
177 doc.push_str("## Examples\n\n");
178 for example in &result.examples {
179 doc.push_str("```bash\n");
180 doc.push_str(&example.command);
181 doc.push_str("\n```\n\n");
182 if let Some(desc) = &example.description {
183 doc.push_str(&format!("{}\n\n", desc));
184 }
185 }
186 }
187
188 doc
189}
190
191fn generate_html(result: &ProbeResult) -> String {
193 let mut doc = String::new();
194
195 doc.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
196 doc.push_str("<meta charset=\"utf-8\">\n");
197 doc.push_str(&format!("<title>{}</title>\n", result.command));
198 doc.push_str("<style>\n");
199 doc.push_str(
200 "body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }\n",
201 );
202 doc.push_str("table { border-collapse: collapse; width: 100%; margin: 20px 0; }\n");
203 doc.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
204 doc.push_str("th { background-color: #f2f2f2; }\n");
205 doc.push_str("code { background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; }\n");
206 doc.push_str(
207 "pre { background-color: #f4f4f4; padding: 10px; border-radius: 5px; overflow-x: auto; }\n",
208 );
209 doc.push_str("</style>\n");
210 doc.push_str("</head>\n<body>\n");
211
212 doc.push_str(&format!("<h1>{}</h1>\n", result.command));
214
215 if !result.raw_stdout.is_empty() {
217 doc.push_str("<h2>Description</h2>\n");
218 let desc_lines: Vec<&str> = result.raw_stdout.lines().take(5).collect();
219 doc.push_str(&format!("<p>{}</p>\n", desc_lines.join("<br>\n")));
220 }
221
222 if !result.usage_blocks.is_empty() {
224 doc.push_str("<h2>Usage</h2>\n");
225 for usage in &result.usage_blocks {
226 doc.push_str("<pre>");
227 doc.push_str(&usage.replace("<", "<").replace(">", ">"));
228 doc.push_str("</pre>\n");
229 }
230 }
231
232 if !result.options.is_empty() {
234 doc.push_str("<h2>Options</h2>\n");
235 doc.push_str("<table>\n");
236 doc.push_str("<tr><th>Flag</th><th>Description</th><th>Type</th><th>Required</th><th>Default</th></tr>\n");
237 for opt in &result.options {
238 let flags = format!(
239 "{}, {}",
240 opt.short_flags.join(", "),
241 opt.long_flags.join(", ")
242 );
243 let desc = opt
244 .description
245 .as_deref()
246 .unwrap_or("")
247 .replace("<", "<")
248 .replace(">", ">");
249 let opt_type = format!("{:?}", opt.option_type);
250 let required = if opt.required { "Yes" } else { "No" };
251 let default = opt.default_value.as_deref().unwrap_or("-");
252 doc.push_str(&format!(
253 "<tr><td><code>{}</code></td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n",
254 flags, desc, opt_type, required, default
255 ));
256 }
257 doc.push_str("</table>\n");
258 }
259
260 if !result.arguments.is_empty() {
262 doc.push_str("<h2>Arguments</h2>\n");
263 doc.push_str("<table>\n");
264 doc.push_str("<tr><th>Name</th><th>Description</th><th>Type</th><th>Required</th></tr>\n");
265 for arg in &result.arguments {
266 let desc = arg
267 .description
268 .as_deref()
269 .unwrap_or("")
270 .replace("<", "<")
271 .replace(">", ">");
272 let arg_type = arg
273 .arg_type
274 .as_ref()
275 .map(|t| format!("{:?}", t))
276 .unwrap_or_else(|| "String".to_string());
277 let required = if arg.required { "Yes" } else { "No" };
278 doc.push_str(&format!(
279 "<tr><td><code>{}</code></td><td>{}</td><td>{}</td><td>{}</td></tr>\n",
280 arg.name, desc, arg_type, required
281 ));
282 }
283 doc.push_str("</table>\n");
284 }
285
286 if !result.subcommands.is_empty() {
288 doc.push_str("<h2>Subcommands</h2>\n");
289 doc.push_str("<ul>\n");
290 for subcmd in &result.subcommands {
291 doc.push_str(&format!("<li><strong>{}</strong>", subcmd.name));
292 if let Some(desc) = &subcmd.description {
293 doc.push_str(&format!(" - {}", desc));
294 }
295 doc.push_str("</li>\n");
296 }
297 doc.push_str("</ul>\n");
298 }
299
300 if !result.examples.is_empty() {
302 doc.push_str("<h2>Examples</h2>\n");
303 for example in &result.examples {
304 doc.push_str("<pre>");
305 doc.push_str(&example.command.replace("<", "<").replace(">", ">"));
306 doc.push_str("</pre>\n");
307 if let Some(desc) = &example.description {
308 doc.push_str(&format!("<p>{}</p>\n", desc));
309 }
310 }
311 }
312
313 doc.push_str("</body>\n</html>\n");
314 doc
315}
316
317fn generate_openapi(result: &ProbeResult) -> String {
319 use serde_json::json;
320
321 let mut paths = serde_json::Map::new();
322 let mut components = serde_json::Map::new();
323 let mut schemas = serde_json::Map::new();
324
325 let mut parameters = Vec::new();
327 for opt in &result.options {
328 let param = json!({
329 "name": opt.long_flags.first().unwrap_or(&String::new()).trim_start_matches("--"),
330 "in": "query",
331 "description": opt.description,
332 "required": opt.required,
333 "schema": {
334 "type": match opt.option_type {
335 crate::model::OptionType::Boolean => "boolean",
336 crate::model::OptionType::Number => "number",
337 _ => "string"
338 }
339 }
340 });
341 parameters.push(param);
342 }
343
344 for arg in &result.arguments {
345 let param = json!({
346 "name": arg.name,
347 "in": "path",
348 "description": arg.description,
349 "required": arg.required,
350 "schema": {
351 "type": match arg.arg_type {
352 Some(crate::model::ArgumentType::Number) => "number",
353 _ => "string"
354 }
355 }
356 });
357 parameters.push(param);
358 }
359
360 let mut properties = serde_json::Map::new();
362 for opt in &result.options {
363 if opt.takes_argument {
364 properties.insert(
365 opt.long_flags
366 .first()
367 .unwrap_or(&String::new())
368 .trim_start_matches("--")
369 .to_string(),
370 json!({
371 "type": match opt.option_type {
372 crate::model::OptionType::Boolean => "boolean",
373 crate::model::OptionType::Number => "number",
374 _ => "string"
375 },
376 "description": opt.description
377 }),
378 );
379 }
380 }
381
382 if !properties.is_empty() {
383 schemas.insert(
384 "CommandRequest".to_string(),
385 json!({
386 "type": "object",
387 "properties": properties
388 }),
389 );
390 }
391
392 components.insert("schemas".to_string(), json!(schemas));
393
394 let path_item = json!({
396 "post": {
397 "summary": format!("Execute {}", result.command),
398 "description": result.raw_stdout.lines().take(3).collect::<Vec<_>>().join("\n"),
399 "parameters": parameters,
400 "requestBody": if !properties.is_empty() {
401 json!({
402 "content": {
403 "application/json": {
404 "schema": {
405 "$ref": "#/components/schemas/CommandRequest"
406 }
407 }
408 }
409 })
410 } else {
411 json!(null)
412 },
413 "responses": {
414 "200": {
415 "description": "Command executed successfully"
416 }
417 }
418 }
419 });
420
421 paths.insert(format!("/{}", result.command.replace(" ", "/")), path_item);
422
423 let openapi = json!({
424 "openapi": "3.0.0",
425 "info": {
426 "title": result.command,
427 "version": "1.0.0",
428 "description": result.raw_stdout.lines().take(5).collect::<Vec<_>>().join("\n")
429 },
430 "paths": paths,
431 "components": components
432 });
433
434 serde_json::to_string_pretty(&openapi).unwrap_or_else(|_| "{}".to_string())
435}
436
437fn generate_json_schema(result: &ProbeResult) -> String {
439 use serde_json::json;
440
441 let mut properties = serde_json::Map::new();
442 let mut required = Vec::new();
443
444 for opt in &result.options {
446 if opt.takes_argument {
447 let prop_name = opt
448 .long_flags
449 .first()
450 .unwrap_or(&String::new())
451 .trim_start_matches("--")
452 .to_string();
453 let mut prop = serde_json::Map::new();
454 prop.insert(
455 "type".to_string(),
456 json!(match opt.option_type {
457 crate::model::OptionType::Boolean => "boolean",
458 crate::model::OptionType::Number => "number",
459 _ => "string",
460 }),
461 );
462 if let Some(desc) = &opt.description {
463 prop.insert("description".to_string(), json!(desc));
464 }
465 if let Some(default) = &opt.default_value {
466 prop.insert("default".to_string(), json!(default));
467 }
468 if !opt.choices.is_empty() {
469 prop.insert("enum".to_string(), json!(opt.choices));
470 }
471 properties.insert(prop_name.clone(), json!(prop));
472 if opt.required {
473 required.push(prop_name);
474 }
475 }
476 }
477
478 for arg in &result.arguments {
480 let mut prop = serde_json::Map::new();
481 prop.insert(
482 "type".to_string(),
483 json!(match arg.arg_type {
484 Some(crate::model::ArgumentType::Number) => "number",
485 _ => "string",
486 }),
487 );
488 if let Some(desc) = &arg.description {
489 prop.insert("description".to_string(), json!(desc));
490 }
491 properties.insert(arg.name.clone(), json!(prop));
492 if arg.required {
493 required.push(arg.name.clone());
494 }
495 }
496
497 let schema = json!({
498 "$schema": "http://json-schema.org/draft-07/schema#",
499 "title": result.command,
500 "type": "object",
501 "properties": properties,
502 "required": required
503 });
504
505 serde_json::to_string_pretty(&schema).unwrap_or_else(|_| "{}".to_string())
506}