use crate::model::ProbeResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocFormat {
Markdown,
Html,
OpenApi,
JsonSchema,
}
pub fn generate_api_docs(result: &ProbeResult, format: DocFormat) -> String {
match format {
DocFormat::Markdown => generate_markdown(result),
DocFormat::Html => generate_html(result),
DocFormat::OpenApi => generate_openapi(result),
DocFormat::JsonSchema => generate_json_schema(result),
}
}
fn generate_markdown(result: &ProbeResult) -> String {
let mut doc = String::new();
doc.push_str(&format!("# {}\n\n", result.command));
if !result.raw_stdout.is_empty() {
doc.push_str("## Description\n\n");
let desc_lines: Vec<&str> = result.raw_stdout.lines().take(5).collect();
doc.push_str(&desc_lines.join("\n"));
doc.push_str("\n\n");
}
if !result.usage_blocks.is_empty() {
doc.push_str("## Usage\n\n");
for usage in &result.usage_blocks {
doc.push_str("```\n");
doc.push_str(usage);
doc.push_str("\n```\n\n");
}
}
if !result.options.is_empty() {
doc.push_str("## Options\n\n");
doc.push_str("| Flag | Description | Type | Required | Default |\n");
doc.push_str("|------|-------------|------|----------|----------|\n");
for opt in &result.options {
let flags = format!(
"{}, {}",
opt.short_flags.join(", "),
opt.long_flags.join(", ")
);
let desc = opt.description.as_deref().unwrap_or("").replace("|", "\\|");
let opt_type = format!("{:?}", opt.option_type);
let required = if opt.required { "Yes" } else { "No" };
let default = opt.default_value.as_deref().unwrap_or("-");
doc.push_str(&format!(
"| {} | {} | {} | {} | {} |\n",
flags, desc, opt_type, required, default
));
}
doc.push_str("\n");
}
if !result.arguments.is_empty() {
doc.push_str("## Arguments\n\n");
doc.push_str("| Name | Description | Type | Required |\n");
doc.push_str("|------|-------------|------|----------|\n");
for arg in &result.arguments {
let desc = arg.description.as_deref().unwrap_or("").replace("|", "\\|");
let arg_type = arg
.arg_type
.as_ref()
.map(|t| format!("{:?}", t))
.unwrap_or_else(|| "String".to_string());
let required = if arg.required { "Yes" } else { "No" };
doc.push_str(&format!(
"| {} | {} | {} | {} |\n",
arg.name, desc, arg_type, required
));
}
doc.push_str("\n");
}
if !result.subcommands.is_empty() {
doc.push_str("## Subcommands\n\n");
for subcmd in &result.subcommands {
doc.push_str(&format!("### {}\n\n", subcmd.name));
if let Some(desc) = &subcmd.description {
doc.push_str(&format!("{}\n\n", desc));
}
}
doc.push_str("\n");
}
if !result.environment_variables.is_empty() {
doc.push_str("## Environment Variables\n\n");
for env_var in &result.environment_variables {
doc.push_str(&format!("### {}\n\n", env_var.name));
if let Some(desc) = &env_var.description {
doc.push_str(&format!("{}\n\n", desc));
}
if let Some(opt) = &env_var.option_mapped {
doc.push_str(&format!("Maps to: `{}`\n\n", opt));
}
if let Some(default) = &env_var.default_value {
doc.push_str(&format!("Default: `{}`\n\n", default));
}
}
}
if !result.validation_rules.is_empty() {
doc.push_str("## Validation Rules\n\n");
for rule in &result.validation_rules {
doc.push_str(&format!("### {}\n\n", rule.target));
doc.push_str(&format!("Type: {:?}\n\n", rule.rule_type));
if let Some(pattern) = &rule.pattern {
doc.push_str(&format!("Pattern: `{}`\n\n", pattern));
}
if let Some(min) = rule.min {
doc.push_str(&format!("Min: {}\n\n", min));
}
if let Some(max) = rule.max {
doc.push_str(&format!("Max: {}\n\n", max));
}
if let Some(msg) = &rule.message {
doc.push_str(&format!("Message: {}\n\n", msg));
}
}
}
if !result.examples.is_empty() {
doc.push_str("## Examples\n\n");
for example in &result.examples {
doc.push_str("```bash\n");
doc.push_str(&example.command);
doc.push_str("\n```\n\n");
if let Some(desc) = &example.description {
doc.push_str(&format!("{}\n\n", desc));
}
}
}
doc
}
fn generate_html(result: &ProbeResult) -> String {
let mut doc = String::new();
doc.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
doc.push_str("<meta charset=\"utf-8\">\n");
doc.push_str(&format!("<title>{}</title>\n", result.command));
doc.push_str("<style>\n");
doc.push_str(
"body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }\n",
);
doc.push_str("table { border-collapse: collapse; width: 100%; margin: 20px 0; }\n");
doc.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
doc.push_str("th { background-color: #f2f2f2; }\n");
doc.push_str("code { background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; }\n");
doc.push_str(
"pre { background-color: #f4f4f4; padding: 10px; border-radius: 5px; overflow-x: auto; }\n",
);
doc.push_str("</style>\n");
doc.push_str("</head>\n<body>\n");
doc.push_str(&format!("<h1>{}</h1>\n", result.command));
if !result.raw_stdout.is_empty() {
doc.push_str("<h2>Description</h2>\n");
let desc_lines: Vec<&str> = result.raw_stdout.lines().take(5).collect();
doc.push_str(&format!("<p>{}</p>\n", desc_lines.join("<br>\n")));
}
if !result.usage_blocks.is_empty() {
doc.push_str("<h2>Usage</h2>\n");
for usage in &result.usage_blocks {
doc.push_str("<pre>");
doc.push_str(&usage.replace("<", "<").replace(">", ">"));
doc.push_str("</pre>\n");
}
}
if !result.options.is_empty() {
doc.push_str("<h2>Options</h2>\n");
doc.push_str("<table>\n");
doc.push_str("<tr><th>Flag</th><th>Description</th><th>Type</th><th>Required</th><th>Default</th></tr>\n");
for opt in &result.options {
let flags = format!(
"{}, {}",
opt.short_flags.join(", "),
opt.long_flags.join(", ")
);
let desc = opt
.description
.as_deref()
.unwrap_or("")
.replace("<", "<")
.replace(">", ">");
let opt_type = format!("{:?}", opt.option_type);
let required = if opt.required { "Yes" } else { "No" };
let default = opt.default_value.as_deref().unwrap_or("-");
doc.push_str(&format!(
"<tr><td><code>{}</code></td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n",
flags, desc, opt_type, required, default
));
}
doc.push_str("</table>\n");
}
if !result.arguments.is_empty() {
doc.push_str("<h2>Arguments</h2>\n");
doc.push_str("<table>\n");
doc.push_str("<tr><th>Name</th><th>Description</th><th>Type</th><th>Required</th></tr>\n");
for arg in &result.arguments {
let desc = arg
.description
.as_deref()
.unwrap_or("")
.replace("<", "<")
.replace(">", ">");
let arg_type = arg
.arg_type
.as_ref()
.map(|t| format!("{:?}", t))
.unwrap_or_else(|| "String".to_string());
let required = if arg.required { "Yes" } else { "No" };
doc.push_str(&format!(
"<tr><td><code>{}</code></td><td>{}</td><td>{}</td><td>{}</td></tr>\n",
arg.name, desc, arg_type, required
));
}
doc.push_str("</table>\n");
}
if !result.subcommands.is_empty() {
doc.push_str("<h2>Subcommands</h2>\n");
doc.push_str("<ul>\n");
for subcmd in &result.subcommands {
doc.push_str(&format!("<li><strong>{}</strong>", subcmd.name));
if let Some(desc) = &subcmd.description {
doc.push_str(&format!(" - {}", desc));
}
doc.push_str("</li>\n");
}
doc.push_str("</ul>\n");
}
if !result.examples.is_empty() {
doc.push_str("<h2>Examples</h2>\n");
for example in &result.examples {
doc.push_str("<pre>");
doc.push_str(&example.command.replace("<", "<").replace(">", ">"));
doc.push_str("</pre>\n");
if let Some(desc) = &example.description {
doc.push_str(&format!("<p>{}</p>\n", desc));
}
}
}
doc.push_str("</body>\n</html>\n");
doc
}
fn generate_openapi(result: &ProbeResult) -> String {
use serde_json::json;
let mut paths = serde_json::Map::new();
let mut components = serde_json::Map::new();
let mut schemas = serde_json::Map::new();
let mut parameters = Vec::new();
for opt in &result.options {
let param = json!({
"name": opt.long_flags.first().unwrap_or(&String::new()).trim_start_matches("--"),
"in": "query",
"description": opt.description,
"required": opt.required,
"schema": {
"type": match opt.option_type {
crate::model::OptionType::Boolean => "boolean",
crate::model::OptionType::Number => "number",
_ => "string"
}
}
});
parameters.push(param);
}
for arg in &result.arguments {
let param = json!({
"name": arg.name,
"in": "path",
"description": arg.description,
"required": arg.required,
"schema": {
"type": match arg.arg_type {
Some(crate::model::ArgumentType::Number) => "number",
_ => "string"
}
}
});
parameters.push(param);
}
let mut properties = serde_json::Map::new();
for opt in &result.options {
if opt.takes_argument {
properties.insert(
opt.long_flags
.first()
.unwrap_or(&String::new())
.trim_start_matches("--")
.to_string(),
json!({
"type": match opt.option_type {
crate::model::OptionType::Boolean => "boolean",
crate::model::OptionType::Number => "number",
_ => "string"
},
"description": opt.description
}),
);
}
}
if !properties.is_empty() {
schemas.insert(
"CommandRequest".to_string(),
json!({
"type": "object",
"properties": properties
}),
);
}
components.insert("schemas".to_string(), json!(schemas));
let path_item = json!({
"post": {
"summary": format!("Execute {}", result.command),
"description": result.raw_stdout.lines().take(3).collect::<Vec<_>>().join("\n"),
"parameters": parameters,
"requestBody": if !properties.is_empty() {
json!({
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandRequest"
}
}
}
})
} else {
json!(null)
},
"responses": {
"200": {
"description": "Command executed successfully"
}
}
}
});
paths.insert(format!("/{}", result.command.replace(" ", "/")), path_item);
let openapi = json!({
"openapi": "3.0.0",
"info": {
"title": result.command,
"version": "1.0.0",
"description": result.raw_stdout.lines().take(5).collect::<Vec<_>>().join("\n")
},
"paths": paths,
"components": components
});
serde_json::to_string_pretty(&openapi).unwrap_or_else(|_| "{}".to_string())
}
fn generate_json_schema(result: &ProbeResult) -> String {
use serde_json::json;
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for opt in &result.options {
if opt.takes_argument {
let prop_name = opt
.long_flags
.first()
.unwrap_or(&String::new())
.trim_start_matches("--")
.to_string();
let mut prop = serde_json::Map::new();
prop.insert(
"type".to_string(),
json!(match opt.option_type {
crate::model::OptionType::Boolean => "boolean",
crate::model::OptionType::Number => "number",
_ => "string",
}),
);
if let Some(desc) = &opt.description {
prop.insert("description".to_string(), json!(desc));
}
if let Some(default) = &opt.default_value {
prop.insert("default".to_string(), json!(default));
}
if !opt.choices.is_empty() {
prop.insert("enum".to_string(), json!(opt.choices));
}
properties.insert(prop_name.clone(), json!(prop));
if opt.required {
required.push(prop_name);
}
}
}
for arg in &result.arguments {
let mut prop = serde_json::Map::new();
prop.insert(
"type".to_string(),
json!(match arg.arg_type {
Some(crate::model::ArgumentType::Number) => "number",
_ => "string",
}),
);
if let Some(desc) = &arg.description {
prop.insert("description".to_string(), json!(desc));
}
properties.insert(arg.name.clone(), json!(prop));
if arg.required {
required.push(arg.name.clone());
}
}
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": result.command,
"type": "object",
"properties": properties,
"required": required
});
serde_json::to_string_pretty(&schema).unwrap_or_else(|_| "{}".to_string())
}