Skip to content

Commit 704c733

Browse files
author
Danielle Jenkins
committed
Add output formatting as a cli option
1 parent f50ac58 commit 704c733

File tree

3 files changed

+170
-25
lines changed

3 files changed

+170
-25
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ Thumbs.db
2121
.idea/
2222
.vscode/
2323
*.swp
24-
*.swo
24+
*.swo
25+
output_tests

README.md

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,32 @@ cargo run --bin cratedocs http --address 0.0.0.0:3000
3838
cargo run --bin cratedocs http --debug
3939
```
4040

41-
### Legacy Commands
41+
### Directly Testing Documentation Tools
4242

43-
For backward compatibility, you can still use the original binaries:
43+
You can directly test the documentation tools from the command line without starting a server:
4444

4545
```bash
46-
# STDIN/STDOUT Mode
47-
cargo run --bin stdio-server
46+
# Get help for the test command
47+
cargo run --bin cratedocs test --tool help
4848

49-
# HTTP/SSE Mode
50-
cargo run --bin axum-docs
49+
# Look up crate documentation
50+
cargo run --bin cratedocs test --tool lookup_crate --crate-name tokio
51+
52+
# Look up item documentation
53+
cargo run --bin cratedocs test --tool lookup_item --crate-name tokio --item-path sync::mpsc::Sender
54+
55+
# Look up documentation for a specific version
56+
cargo run --bin cratedocs test --tool lookup_item --crate-name serde --item-path Serialize --version 1.0.147
57+
58+
# Search for crates
59+
cargo run --bin cratedocs test --tool search_crates --query logger --limit 5
60+
61+
# Output in different formats (markdown, text, json)
62+
cargo run --bin cratedocs test --tool search_crates --query logger --format json
63+
cargo run --bin cratedocs test --tool lookup_crate --crate-name tokio --format text
64+
65+
# Save output to a file
66+
cargo run --bin cratedocs test --tool lookup_crate --crate-name tokio --output tokio-docs.md
5167
```
5268

5369
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
127143

128144
## License
129145

130-
MIT License
146+
MIT License

src/bin/cratedocs.rs

Lines changed: 145 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ enum Commands {
6363
#[arg(long)]
6464
limit: Option<u32>,
6565

66+
/// Output format (markdown, text, json)
67+
#[arg(long, default_value = "markdown")]
68+
format: Option<String>,
69+
70+
/// Output file path (if not specified, results will be printed to stdout)
71+
#[arg(long)]
72+
output: Option<String>,
73+
6674
/// Enable debug logging
6775
#[arg(short, long)]
6876
debug: bool,
@@ -82,9 +90,21 @@ async fn main() -> Result<()> {
8290
item_path,
8391
query,
8492
version,
85-
limit,
93+
limit,
94+
format,
95+
output,
8696
debug
87-
} => run_test_tool(tool, crate_name, item_path, query, version, limit, debug).await,
97+
} => run_test_tool(TestToolConfig {
98+
tool,
99+
crate_name,
100+
item_path,
101+
query,
102+
version,
103+
limit,
104+
format,
105+
output,
106+
debug
107+
}).await,
88108
}
89109
}
90110

@@ -143,16 +163,32 @@ async fn run_http_server(address: String, debug: bool) -> Result<()> {
143163
Ok(())
144164
}
145165

146-
/// Run a direct test of a documentation tool from the CLI
147-
async fn run_test_tool(
166+
/// Configuration for the test tool
167+
struct TestToolConfig {
148168
tool: String,
149169
crate_name: Option<String>,
150170
item_path: Option<String>,
151171
query: Option<String>,
152172
version: Option<String>,
153173
limit: Option<u32>,
174+
format: Option<String>,
175+
output: Option<String>,
154176
debug: bool,
155-
) -> Result<()> {
177+
}
178+
179+
/// Run a direct test of a documentation tool from the CLI
180+
async fn run_test_tool(config: TestToolConfig) -> Result<()> {
181+
let TestToolConfig {
182+
tool,
183+
crate_name,
184+
item_path,
185+
query,
186+
version,
187+
limit,
188+
format,
189+
output,
190+
debug,
191+
} = config;
156192
// Print help information if the tool is "help"
157193
if tool == "help" {
158194
println!("CrateDocs CLI Tool Tester\n");
@@ -161,16 +197,22 @@ async fn run_test_tool(
161197
println!(" cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio --version 1.35.0");
162198
println!(" cargo run --bin cratedocs -- test --tool lookup_item --crate-name tokio --item-path sync::mpsc::Sender");
163199
println!(" cargo run --bin cratedocs -- test --tool lookup_item --crate-name serde --item-path Serialize --version 1.0.147");
164-
println!(" cargo run --bin cratedocs -- test --tool search_crates --query logger\n");
165-
println!("Available tools:");
200+
println!(" cargo run --bin cratedocs -- test --tool search_crates --query logger --limit 5");
201+
println!(" cargo run --bin cratedocs -- test --tool search_crates --query logger --format json");
202+
println!(" cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio --output tokio-docs.md");
203+
println!("\nAvailable tools:");
166204
println!(" lookup_crate - Look up documentation for a Rust crate");
167205
println!(" lookup_item - Look up documentation for a specific item in a crate");
168206
println!(" Format: 'module::path::ItemName' (e.g., 'sync::mpsc::Sender')");
169207
println!(" The tool will try to detect if it's a struct, enum, trait, fn, or macro");
170208
println!(" search_crates - Search for crates on crates.io");
171-
println!(" help - Show this help information\n");
209+
println!(" help - Show this help information");
210+
println!("\nOutput options:");
211+
println!(" --format - Output format: markdown (default), text, json");
212+
println!(" --output - Write output to a file instead of stdout");
172213
return Ok(());
173214
}
215+
174216
// Set up console logging
175217
let level = if debug { tracing::Level::DEBUG } else { tracing::Level::INFO };
176218

@@ -185,6 +227,9 @@ async fn run_test_tool(
185227

186228
tracing::info!("Testing tool: {}", tool);
187229

230+
// Get format option (default to markdown)
231+
let format = format.unwrap_or_else(|| "markdown".to_string());
232+
188233
// Prepare arguments based on the tool being tested
189234
let arguments = match tool.as_str() {
190235
"lookup_crate" => {
@@ -233,22 +278,105 @@ async fn run_test_tool(
233278
eprintln!(" - For item lookup: cargo run --bin cratedocs -- test --tool lookup_item --crate-name tokio --item-path sync::mpsc::Sender");
234279
eprintln!(" - For item lookup with version: cargo run --bin cratedocs -- test --tool lookup_item --crate-name serde --item-path Serialize --version 1.0.147");
235280
eprintln!(" - For crate search: cargo run --bin cratedocs -- test --tool search_crates --query logger --limit 5");
281+
eprintln!(" - For output format: cargo run --bin cratedocs -- test --tool search_crates --query logger --format json");
282+
eprintln!(" - For file output: cargo run --bin cratedocs -- test --tool lookup_crate --crate-name tokio --output tokio-docs.md");
236283
eprintln!(" - For help: cargo run --bin cratedocs -- test --tool help");
237284
return Ok(());
238285
}
239286
};
240287

241-
// Print results
288+
// Process and output results
242289
if !result.is_empty() {
243290
for content in result {
244-
match content {
245-
Content::Text(text) => {
246-
println!("\n--- TOOL RESULT ---\n");
247-
// Access the raw string from TextContent.text field
248-
println!("{}", text.text);
249-
println!("\n--- END RESULT ---");
250-
},
251-
_ => println!("Received non-text content"),
291+
if let Content::Text(text) = content {
292+
let content_str = text.text;
293+
let formatted_output = match format.as_str() {
294+
"json" => {
295+
// For search_crates, which may return JSON content
296+
if tool == "search_crates" && content_str.trim().starts_with('{') {
297+
// If content is already valid JSON, pretty print it
298+
match serde_json::from_str::<serde_json::Value>(&content_str) {
299+
Ok(json_value) => serde_json::to_string_pretty(&json_value)
300+
.unwrap_or_else(|_| content_str.clone()),
301+
Err(_) => {
302+
// If it's not JSON, wrap it in a simple JSON object
303+
json!({ "content": content_str }).to_string()
304+
}
305+
}
306+
} else {
307+
// For non-JSON content, wrap in a JSON object
308+
json!({ "content": content_str }).to_string()
309+
}
310+
},
311+
"text" => {
312+
// For JSON content, try to extract plain text
313+
if content_str.trim().starts_with('{') && tool == "search_crates" {
314+
match serde_json::from_str::<serde_json::Value>(&content_str) {
315+
Ok(json_value) => {
316+
// Try to create a simple text representation of search results
317+
if let Some(crates) = json_value.get("crates").and_then(|v| v.as_array()) {
318+
let mut text_output = String::from("Search Results:\n\n");
319+
for (i, crate_info) in crates.iter().enumerate() {
320+
let name = crate_info.get("name").and_then(|v| v.as_str()).unwrap_or("Unknown");
321+
let description = crate_info.get("description").and_then(|v| v.as_str()).unwrap_or("No description");
322+
let downloads = crate_info.get("downloads").and_then(|v| v.as_u64()).unwrap_or(0);
323+
324+
text_output.push_str(&format!("{}. {} - {} (Downloads: {})\n",
325+
i + 1, name, description, downloads));
326+
}
327+
text_output
328+
} else {
329+
content_str
330+
}
331+
},
332+
Err(_) => content_str,
333+
}
334+
} else {
335+
// For markdown content, use a simple approach to convert to plain text
336+
// This is a very basic conversion - more sophisticated would need a proper markdown parser
337+
content_str
338+
.replace("# ", "")
339+
.replace("## ", "")
340+
.replace("### ", "")
341+
.replace("#### ", "")
342+
.replace("##### ", "")
343+
.replace("###### ", "")
344+
.replace("**", "")
345+
.replace("*", "")
346+
.replace("`", "")
347+
}
348+
},
349+
_ => content_str, // Default to original markdown for "markdown" or any other format
350+
};
351+
352+
// Output to file or stdout
353+
match &output {
354+
Some(file_path) => {
355+
use std::fs;
356+
use std::io::Write;
357+
358+
tracing::info!("Writing output to file: {}", file_path);
359+
360+
// Ensure parent directory exists
361+
if let Some(parent) = std::path::Path::new(file_path).parent() {
362+
if !parent.exists() {
363+
fs::create_dir_all(parent)?;
364+
}
365+
}
366+
367+
let mut file = fs::File::create(file_path)?;
368+
file.write_all(formatted_output.as_bytes())?;
369+
println!("Results written to file: {}", file_path);
370+
},
371+
None => {
372+
// Print to stdout
373+
println!("\n--- TOOL RESULT ---\n");
374+
println!("{}", formatted_output);
375+
println!("\n--- END RESULT ---");
376+
}
377+
}
378+
} else {
379+
println!("Received non-text content");
252380
}
253381
}
254382
} else {

0 commit comments

Comments
 (0)