Skip to main content

api_testing_core/
cli_contract.rs

1//! Shared CLI output contract glue for the five api-* binaries.
2//!
3//! All five `api-rest` / `api-gql` / `api-grpc` / `api-websocket` / `api-test`
4//! binaries route their output through this module so the JSON envelope and
5//! exit codes stay aligned with `nils_common::cli_contract`. Each binary's
6//! tests still pin the literal `schema_version` per subcommand; this module
7//! intentionally adds no new schema versions of its own.
8
9use std::ffi::OsString;
10
11use clap::error::ErrorKind;
12
13pub use nils_common::cli_contract::{
14    Envelope, EnvelopeError, OutputFormat, emit_parse_error, exit, schema_version_for,
15};
16
17/// Route a clap parse error through the shared output contract.
18///
19/// Help and version exits keep clap's native behavior; everything else lands
20/// through `emit_parse_error` with raw-argv format detection so `--format json`
21/// consumers see a JSON envelope on parse and unknown-subcommand errors
22/// instead of clap's text-only error.
23pub fn handle_parse_error<I>(binary: &str, argv: I, err: clap::Error) -> i32
24where
25    I: IntoIterator<Item = OsString>,
26{
27    let kind = err.kind();
28    if matches!(
29        kind,
30        ErrorKind::DisplayHelp
31            | ErrorKind::DisplayVersion
32            | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
33    ) {
34        let _ = err.print();
35        return err.exit_code();
36    }
37
38    let argv: Vec<OsString> = argv.into_iter().collect();
39    let format = detect_format_from_argv(&argv);
40    let code = match kind {
41        ErrorKind::InvalidSubcommand => "unknown-subcommand",
42        _ => "parse-error",
43    };
44    let message = render_clap_message(&err);
45    emit_parse_error(binary, format, code, &message)
46}
47
48fn detect_format_from_argv(argv: &[OsString]) -> OutputFormat {
49    let mut iter = argv.iter().skip(1);
50    while let Some(arg) = iter.next() {
51        let arg = arg.to_string_lossy();
52        if arg == "--json" {
53            return OutputFormat::Json;
54        }
55        if arg == "--format"
56            && let Some(next) = iter.next()
57            && next.to_string_lossy().eq_ignore_ascii_case("json")
58        {
59            return OutputFormat::Json;
60        }
61        if let Some(rest) = arg.strip_prefix("--format=")
62            && rest.eq_ignore_ascii_case("json")
63        {
64            return OutputFormat::Json;
65        }
66    }
67    OutputFormat::Text
68}
69
70fn render_clap_message(err: &clap::Error) -> String {
71    err.to_string()
72        .lines()
73        .find(|line| !line.trim().is_empty())
74        .map(|line| {
75            let line = line.trim();
76            line.strip_prefix("error:")
77                .map(str::trim)
78                .unwrap_or(line)
79                .to_string()
80        })
81        .unwrap_or_else(|| "command-line parse failed".to_string())
82}