diff --git a/src/main.rs b/src/main.rs index aaf5840..fe84332 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,9 @@ mod config; mod output; mod shell; -use std::process::ExitCode; +use std::{ffi::OsStr, process::ExitCode}; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, error::ErrorKind}; use output::OutputFormat; #[derive(Parser)] @@ -68,7 +68,31 @@ enum Commands { #[tokio::main] async fn main() -> ExitCode { - let cli = Cli::parse(); + let args: Vec<_> = std::env::args_os().collect(); + let requested_output = requested_output_format(&args[1..]); + let cli = match Cli::try_parse_from(args) { + Ok(cli) => cli, + Err(error) => { + if matches!( + error.kind(), + ErrorKind::DisplayHelp | ErrorKind::DisplayVersion + ) { + error.exit(); + } + + if matches!(requested_output, OutputFormat::Json) { + println!( + "{}", + serde_json::json!({ + "error": error.to_string(), + }) + ); + return ExitCode::from(u8::try_from(error.exit_code()).unwrap_or(1)); + } + + error.exit(); + } + }; let output = cli.output; if let Err(e) = run(cli).await { @@ -79,6 +103,40 @@ async fn main() -> ExitCode { ExitCode::SUCCESS } +fn requested_output_format(args: &[impl AsRef]) -> OutputFormat { + let mut requested = OutputFormat::Table; + let mut args = args.iter(); + + while let Some(arg) = args.next() { + let arg = arg.as_ref(); + if arg == "--" { + break; + } + + if arg == "--output" || arg == "-o" { + if args.next().is_some_and(|value| value.as_ref() == "json") { + requested = OutputFormat::Json; + } + continue; + } + + let Some(arg) = arg.to_str() else { + continue; + }; + if arg + .strip_prefix("--output=") + .is_some_and(|value| value == "json") + || arg + .strip_prefix("-o") + .is_some_and(|value| !value.is_empty() && value == "json") + { + requested = OutputFormat::Json; + } + } + + requested +} + #[allow(clippy::too_many_lines)] pub(crate) async fn run(cli: Cli) -> anyhow::Result<()> { // Lazy-init so we only pay for the client we actually use. diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 41d3d11..eb861db 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -91,11 +91,67 @@ fn no_args_shows_usage() { .stderr(predicate::str::contains("Usage")); } +fn assert_json_parse_error(args: &[&str]) { + let output = polymarket().args(args).output().unwrap(); + + assert_eq!(output.status.code(), Some(2)); + assert!( + output.stderr.is_empty(), + "stderr should be empty in JSON mode: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = serde_json::from_str(stdout.trim()) + .unwrap_or_else(|e| panic!("stdout not valid JSON: {e}\nstdout: {stdout}")); + assert!( + parsed + .get("error") + .is_some_and(serde_json::Value::is_string), + "missing string 'error' key: {parsed}" + ); +} + #[test] fn unknown_command_fails() { polymarket().arg("nonexistent").assert().failure(); } +#[test] +fn json_mode_formats_unknown_command_parse_error() { + assert_json_parse_error(&["--output", "json", "nonexistent"]); +} + +#[test] +fn json_mode_is_detected_after_invalid_command() { + assert_json_parse_error(&["nonexistent", "--output=json"]); +} + +#[test] +fn json_short_form_formats_validation_error() { + assert_json_parse_error(&["markets", "get", "-ojson"]); +} + +#[test] +fn table_mode_parse_error_keeps_clap_output() { + polymarket() + .arg("nonexistent") + .assert() + .code(2) + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::starts_with("error:").and(predicate::str::contains("Usage:"))); +} + +#[test] +fn json_flag_does_not_change_help_semantics() { + polymarket() + .args(["--output", "json", "--help"]) + .assert() + .success() + .stdout(predicate::str::contains("Usage:")) + .stderr(predicate::str::is_empty()); +} + #[test] fn invalid_output_format_rejected() { polymarket()