help_probe/
api_docs.rs

1use crate::model::ProbeResult;
2
3/// Format for API documentation generation.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum DocFormat {
6    /// Markdown format
7    Markdown,
8    /// HTML format
9    Html,
10    /// OpenAPI 3.0 specification (JSON)
11    OpenApi,
12    /// JSON Schema format
13    JsonSchema,
14}
15
16/// Generate API documentation from a ProbeResult.
17///
18/// # Examples
19///
20/// ```
21/// use help_probe::{api_docs::{generate_api_docs, DocFormat}, model::ProbeResult};
22///
23/// let result = ProbeResult {
24///     command: "mytool".to_string(),
25///     args: vec![],
26///     exit_code: Some(0),
27///     timed_out: false,
28///     help_flag_detected: true,
29///     usage_blocks: vec![],
30///     options: vec![],
31///     subcommands: vec![],
32///     arguments: vec![],
33///     examples: vec![],
34///     environment_variables: vec![],
35///     validation_rules: vec![],
36///     raw_stdout: String::new(),
37///     raw_stderr: String::new(),
38/// };
39///
40/// let markdown = generate_api_docs(&result, DocFormat::Markdown);
41/// assert!(markdown.contains("# mytool"));
42///
43/// let html = generate_api_docs(&result, DocFormat::Html);
44/// assert!(html.contains("<!DOCTYPE html>"));
45/// ```
46pub 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
55/// Generate Markdown documentation.
56fn generate_markdown(result: &ProbeResult) -> String {
57    let mut doc = String::new();
58
59    // Title
60    doc.push_str(&format!("# {}\n\n", result.command));
61
62    // Description
63    if !result.raw_stdout.is_empty() {
64        doc.push_str("## Description\n\n");
65        // Extract first few lines as description
66        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    // Usage
72    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    // Options
82    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    // Arguments
105    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    // Subcommands
126    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    // Environment Variables
138    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    // Validation Rules
155    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    // Examples
176    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
191/// Generate HTML documentation.
192fn 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    // Title
213    doc.push_str(&format!("<h1>{}</h1>\n", result.command));
214
215    // Description
216    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    // Usage
223    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("<", "&lt;").replace(">", "&gt;"));
228            doc.push_str("</pre>\n");
229        }
230    }
231
232    // Options
233    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("<", "&lt;")
248                .replace(">", "&gt;");
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    // Arguments
261    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("<", "&lt;")
271                .replace(">", "&gt;");
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    // Subcommands
287    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    // Examples
301    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("<", "&lt;").replace(">", "&gt;"));
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
317/// Generate OpenAPI 3.0 specification.
318fn 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    // Build parameters schema
326    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    // Build request body schema
361    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    // Build paths
395    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
437/// Generate JSON Schema.
438fn 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    // Add options as properties
445    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    // Add arguments as properties
479    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}