From 704c7333b66a402d6fba861c42c8112945cbe347 Mon Sep 17 00:00:00 2001 From: Danielle Jenkins Date: Thu, 13 Mar 2025 11:34:20 +0900 Subject: [PATCH] Add output formatting as a cli option --- .gitignore | 3 +- README.md | 30 ++++++-- src/bin/cratedocs.rs | 162 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 170 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 8dce1e6..aa8d4f1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ Thumbs.db .idea/ .vscode/ *.swp -*.swo \ No newline at end of file +*.swo +output_tests diff --git a/README.md b/README.md index 3ce9602..25f53d1 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +MIT License diff --git a/src/bin/cratedocs.rs b/src/bin/cratedocs.rs index 2b78bb8..977c346 100644 --- a/src/bin/cratedocs.rs +++ b/src/bin/cratedocs.rs @@ -63,6 +63,14 @@ enum Commands { #[arg(long)] limit: Option, + /// Output format (markdown, text, json) + #[arg(long, default_value = "markdown")] + format: Option, + + /// Output file path (if not specified, results will be printed to stdout) + #[arg(long)] + output: Option, + /// 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, item_path: Option, query: Option, version: Option, limit: Option, + format: Option, + output: Option, 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::(&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::(&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 {