Add output formatting as a cli option

This commit is contained in:
Danielle Jenkins 2025-03-13 11:34:20 +09:00
parent f50ac58a24
commit 704c7333b6
3 changed files with 170 additions and 25 deletions

3
.gitignore vendored
View file

@ -21,4 +21,5 @@ Thumbs.db
.idea/
.vscode/
*.swp
*.swo
*.swo
output_tests

View file

@ -38,16 +38,32 @@ cargo run --bin cratedocs http --address 0.0.0.0:3000
cargo run --bin cratedocs http --debug
```
### Legacy Commands
### Directly Testing Documentation Tools
For backward compatibility, you can still use the original binaries:
You can directly test the documentation tools from the command line without starting a server:
```bash
# STDIN/STDOUT Mode
cargo run --bin stdio-server
# Get help for the test command
cargo run --bin cratedocs test --tool help
# HTTP/SSE Mode
cargo run --bin axum-docs
# Look up crate documentation
cargo run --bin cratedocs test --tool lookup_crate --crate-name tokio
# Look up item documentation
cargo run --bin cratedocs test --tool lookup_item --crate-name tokio --item-path sync::mpsc::Sender
# Look up documentation for a specific version
cargo run --bin cratedocs test --tool lookup_item --crate-name serde --item-path Serialize --version 1.0.147
# Search for crates
cargo run --bin cratedocs test --tool search_crates --query logger --limit 5
# Output in different formats (markdown, text, json)
cargo run --bin cratedocs test --tool search_crates --query logger --format json
cargo run --bin cratedocs test --tool lookup_crate --crate-name tokio --format text
# Save output to a file
cargo run --bin cratedocs test --tool lookup_crate --crate-name tokio --output tokio-docs.md
```
By default, the HTTP server will listen on `http://127.0.0.1:8080/sse`.
@ -127,4 +143,4 @@ This server implements the Model Context Protocol (MCP) which allows it to be ea
## License
MIT License
MIT License

View file

@ -63,6 +63,14 @@ enum Commands {
#[arg(long)]
limit: Option<u32>,
/// Output format (markdown, text, json)
#[arg(long, default_value = "markdown")]
format: Option<String>,
/// Output file path (if not specified, results will be printed to stdout)
#[arg(long)]
output: Option<String>,
/// Enable debug logging
#[arg(short, long)]
debug: bool,
@ -82,9 +90,21 @@ async fn main() -> Result<()> {
item_path,
query,
version,
limit,
limit,
format,
output,
debug
} => run_test_tool(tool, crate_name, item_path, query, version, limit, debug).await,
} => run_test_tool(TestToolConfig {
tool,
crate_name,
item_path,
query,
version,
limit,
format,
output,
debug
}).await,
}
}
@ -143,16 +163,32 @@ async fn run_http_server(address: String, debug: bool) -> Result<()> {
Ok(())
}
/// Run a direct test of a documentation tool from the CLI
async fn run_test_tool(
/// Configuration for the test tool
struct TestToolConfig {
tool: String,
crate_name: Option<String>,
item_path: Option<String>,
query: Option<String>,
version: Option<String>,
limit: Option<u32>,
format: Option<String>,
output: Option<String>,
debug: bool,
) -> Result<()> {
}
/// Run a direct test of a documentation tool from the CLI
async fn run_test_tool(config: TestToolConfig) -> Result<()> {
let TestToolConfig {
tool,
crate_name,
item_path,
query,
version,
limit,
format,
output,
debug,
} = config;
// Print help information if the tool is "help"
if tool == "help" {
println!("CrateDocs CLI Tool Tester\n");
@ -161,16 +197,22 @@ async fn run_test_tool(
println!(" cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio --version 1.35.0");
println!(" cargo run --bin cratedocs -- test --tool lookup_item --crate-name tokio --item-path sync::mpsc::Sender");
println!(" cargo run --bin cratedocs -- test --tool lookup_item --crate-name serde --item-path Serialize --version 1.0.147");
println!(" cargo run --bin cratedocs -- test --tool search_crates --query logger\n");
println!("Available tools:");
println!(" cargo run --bin cratedocs -- test --tool search_crates --query logger --limit 5");
println!(" cargo run --bin cratedocs -- test --tool search_crates --query logger --format json");
println!(" cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio --output tokio-docs.md");
println!("\nAvailable tools:");
println!(" lookup_crate - Look up documentation for a Rust crate");
println!(" lookup_item - Look up documentation for a specific item in a crate");
println!(" Format: 'module::path::ItemName' (e.g., 'sync::mpsc::Sender')");
println!(" The tool will try to detect if it's a struct, enum, trait, fn, or macro");
println!(" search_crates - Search for crates on crates.io");
println!(" help - Show this help information\n");
println!(" help - Show this help information");
println!("\nOutput options:");
println!(" --format - Output format: markdown (default), text, json");
println!(" --output - Write output to a file instead of stdout");
return Ok(());
}
// Set up console logging
let level = if debug { tracing::Level::DEBUG } else { tracing::Level::INFO };
@ -185,6 +227,9 @@ async fn run_test_tool(
tracing::info!("Testing tool: {}", tool);
// Get format option (default to markdown)
let format = format.unwrap_or_else(|| "markdown".to_string());
// Prepare arguments based on the tool being tested
let arguments = match tool.as_str() {
"lookup_crate" => {
@ -233,22 +278,105 @@ async fn run_test_tool(
eprintln!(" - For item lookup: cargo run --bin cratedocs -- test --tool lookup_item --crate-name tokio --item-path sync::mpsc::Sender");
eprintln!(" - For item lookup with version: cargo run --bin cratedocs -- test --tool lookup_item --crate-name serde --item-path Serialize --version 1.0.147");
eprintln!(" - For crate search: cargo run --bin cratedocs -- test --tool search_crates --query logger --limit 5");
eprintln!(" - For output format: cargo run --bin cratedocs -- test --tool search_crates --query logger --format json");
eprintln!(" - For file output: cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio --output tokio-docs.md");
eprintln!(" - For help: cargo run --bin cratedocs -- test --tool help");
return Ok(());
}
};
// Print results
// Process and output results
if !result.is_empty() {
for content in result {
match content {
Content::Text(text) => {
println!("\n--- TOOL RESULT ---\n");
// Access the raw string from TextContent.text field
println!("{}", text.text);
println!("\n--- END RESULT ---");
},
_ => println!("Received non-text content"),
if let Content::Text(text) = content {
let content_str = text.text;
let formatted_output = match format.as_str() {
"json" => {
// For search_crates, which may return JSON content
if tool == "search_crates" && content_str.trim().starts_with('{') {
// If content is already valid JSON, pretty print it
match serde_json::from_str::<serde_json::Value>(&content_str) {
Ok(json_value) => serde_json::to_string_pretty(&json_value)
.unwrap_or_else(|_| content_str.clone()),
Err(_) => {
// If it's not JSON, wrap it in a simple JSON object
json!({ "content": content_str }).to_string()
}
}
} else {
// For non-JSON content, wrap in a JSON object
json!({ "content": content_str }).to_string()
}
},
"text" => {
// For JSON content, try to extract plain text
if content_str.trim().starts_with('{') && tool == "search_crates" {
match serde_json::from_str::<serde_json::Value>(&content_str) {
Ok(json_value) => {
// Try to create a simple text representation of search results
if let Some(crates) = json_value.get("crates").and_then(|v| v.as_array()) {
let mut text_output = String::from("Search Results:\n\n");
for (i, crate_info) in crates.iter().enumerate() {
let name = crate_info.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
let description = crate_info.get("description").and_then(|v| v.as_str()).unwrap_or("No description");
let downloads = crate_info.get("downloads").and_then(|v| v.as_u64()).unwrap_or(0);
text_output.push_str(&format!("{}. {} - {} (Downloads: {})\n",
i + 1, name, description, downloads));
}
text_output
} else {
content_str
}
},
Err(_) => content_str,
}
} else {
// For markdown content, use a simple approach to convert to plain text
// This is a very basic conversion - more sophisticated would need a proper markdown parser
content_str
.replace("# ", "")
.replace("## ", "")
.replace("### ", "")
.replace("#### ", "")
.replace("##### ", "")
.replace("###### ", "")
.replace("**", "")
.replace("*", "")
.replace("`", "")
}
},
_ => content_str, // Default to original markdown for "markdown" or any other format
};
// Output to file or stdout
match &output {
Some(file_path) => {
use std::fs;
use std::io::Write;
tracing::info!("Writing output to file: {}", file_path);
// Ensure parent directory exists
if let Some(parent) = std::path::Path::new(file_path).parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
let mut file = fs::File::create(file_path)?;
file.write_all(formatted_output.as_bytes())?;
println!("Results written to file: {}", file_path);
},
None => {
// Print to stdout
println!("\n--- TOOL RESULT ---\n");
println!("{}", formatted_output);
println!("\n--- END RESULT ---");
}
}
} else {
println!("Received non-text content");
}
}
} else {