use std::ffi::OsString;
use serde_json::json;
pub fn json_mode_in_argv(argv: &[OsString]) -> bool {
let tokens: Vec<String> = argv
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect();
let mut i = 0;
while i < tokens.len() {
let t = &tokens[i];
if t == "--json" {
return true;
}
if t == "--output" {
if let Some(next) = tokens.get(i + 1)
&& next == "json"
{
return true;
}
i += 2;
continue;
}
if let Some(v) = t.strip_prefix("--output=")
&& v == "json"
{
return true;
}
i += 1;
}
false
}
pub fn render_error(kind: &str, error: &str, message: &str, exit_code: i32) -> String {
json!({
"kind": kind,
"error": error,
"message": message,
"exit_code": exit_code,
})
.to_string()
}
pub fn render_help(help_text: &str) -> String {
json!({
"kind": "help",
"data": help_text,
})
.to_string()
}
pub fn render_version(name: &str, version: &str) -> String {
json!({
"kind": "version",
"data": {
"name": name,
"version": version,
},
})
.to_string()
}
pub fn classify_clap_error(kind: clap::error::ErrorKind) -> (&'static str, &'static str) {
use clap::error::ErrorKind as K;
let slug = match kind {
K::InvalidValue => "invalid-value",
K::UnknownArgument => "unknown-argument",
K::InvalidSubcommand => "invalid-subcommand",
K::NoEquals => "no-equals",
K::ValueValidation => "value-validation",
K::TooManyValues => "too-many-values",
K::TooFewValues => "too-few-values",
K::WrongNumberOfValues => "wrong-number-of-values",
K::ArgumentConflict => "argument-conflict",
K::MissingRequiredArgument => "missing-required-argument",
K::MissingSubcommand => "missing-subcommand",
K::InvalidUtf8 => "invalid-utf8",
K::DisplayHelp => "display-help",
K::DisplayVersion => "display-version",
K::DisplayHelpOnMissingArgumentOrSubcommand => "missing-subcommand",
K::Io => "io",
K::Format => "format",
_ => "usage-error",
};
("usage", slug)
}
#[cfg(test)]
mod tests {
use super::*;
fn osv(args: &[&str]) -> Vec<OsString> {
args.iter().map(OsString::from).collect()
}
#[test]
fn detects_json_global_flag() {
assert!(json_mode_in_argv(&osv(&["anc", "--json", "audit", "."])));
}
#[test]
fn detects_output_json_pair() {
assert!(json_mode_in_argv(&osv(&[
"anc", "audit", ".", "--output", "json"
])));
}
#[test]
fn detects_output_equals_json() {
assert!(json_mode_in_argv(&osv(&[
"anc",
"audit",
".",
"--output=json",
])));
}
#[test]
fn detects_output_json_at_end() {
assert!(json_mode_in_argv(&osv(&[
"anc",
"--output",
"json",
"--this-flag-does-not-exist-anc",
])));
}
#[test]
fn rejects_output_text() {
assert!(!json_mode_in_argv(&osv(&[
"anc", "audit", ".", "--output", "text"
])));
}
#[test]
fn rejects_no_output_or_json() {
assert!(!json_mode_in_argv(&osv(&["anc", "audit", "."])));
}
#[test]
fn rejects_json_substring_in_value() {
assert!(!json_mode_in_argv(&osv(&[
"anc",
"audit",
".",
"--command",
"json",
])));
}
fn parse_obj(s: &str) -> serde_json::Map<String, serde_json::Value> {
let v: serde_json::Value =
serde_json::from_str(s).expect("envelope under test is valid JSON");
v.as_object()
.expect("envelope under test serializes as a JSON object")
.clone()
}
#[test]
fn error_envelope_has_required_keys() {
let s = render_error("usage", "unknown-argument", "unknown flag --bad", 2);
let obj = parse_obj(&s);
assert!(obj.contains_key("error"));
assert!(obj.contains_key("kind"));
assert!(obj.contains_key("message"));
assert_eq!(obj["exit_code"], 2);
}
#[test]
fn help_envelope_is_a_json_object() {
let s = render_help("Usage: anc ...");
let obj = parse_obj(&s);
assert_eq!(obj["kind"], "help");
assert!(obj["data"].is_string());
}
#[test]
fn version_envelope_has_name_and_version() {
let s = render_version("anc", "0.5.0");
let obj = parse_obj(&s);
assert_eq!(obj["kind"], "version");
assert_eq!(obj["data"]["name"], "anc");
assert_eq!(obj["data"]["version"], "0.5.0");
}
#[test]
fn classify_display_help_is_usage() {
let (kind, slug) = classify_clap_error(clap::error::ErrorKind::DisplayHelp);
assert_eq!(kind, "usage");
assert_eq!(slug, "display-help");
}
}