use crate::infra::driving::cli::OutputFormat;
#[derive(Debug, Clone, serde::Serialize)]
pub(crate) struct CliError {
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
}
impl CliError {
pub fn new(message: impl Into<String>) -> Self {
Self {
error: message.into(),
kind: None,
hint: None,
}
}
pub fn kind(mut self, kind: &'static str) -> Self {
self.kind = Some(kind);
self
}
pub fn hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn not_found(subject: impl Into<String>, list_command: &str) -> Self {
Self::new(format!("{} not found", subject.into()))
.kind("not-found")
.hint(format!("Run `cartu {list_command}` to see existing IDs."))
}
}
pub(crate) fn die(err: CliError, output: OutputFormat, code: i32) -> ! {
match output {
OutputFormat::Json => {
let s = serde_json::to_string(&err)
.unwrap_or_else(|_| format!(r#"{{"error":"{}"}}"#, err.error.replace('"', "\\\"")));
eprintln!("{s}");
}
OutputFormat::Yaml => {
let s =
serde_yaml::to_string(&err).unwrap_or_else(|_| format!("error: {}\n", err.error));
eprint!("{s}");
}
OutputFormat::Human => {
eprintln!("error: {}", err.error);
if let Some(h) = &err.hint {
eprintln!(" hint: {h}");
}
}
}
std::process::exit(code);
}
pub(crate) fn die1(err: CliError, output: OutputFormat) -> ! {
die(err, output, 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_error_serialises_to_flat_json_envelope() {
let err = CliError::new("unknown status 'bogus'")
.kind("validation")
.hint("Known statuses: open, in-progress, closed");
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains(r#""error":"unknown status 'bogus'""#));
assert!(json.contains(r#""kind":"validation""#));
assert!(json.contains(r#""hint":"Known statuses"#));
}
#[test]
fn cli_error_omits_optional_fields_when_absent() {
let err = CliError::new("io failure");
let json = serde_json::to_string(&err).unwrap();
assert_eq!(json, r#"{"error":"io failure"}"#);
}
#[test]
fn cli_error_yaml_is_human_readable() {
let err = CliError::new("not found").kind("not-found");
let yaml = serde_yaml::to_string(&err).unwrap();
assert!(yaml.contains("error: not found"));
assert!(yaml.contains("kind: not-found"));
}
}