use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ExitKind {
Generic = 1,
NotFound = 3,
HashMismatch = 4,
Validation = 5,
Findings = 6,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorStream {
Stdout,
Stderr,
}
pub fn render_cli_error(
code: &str,
message: &str,
json_mode: bool,
details: Option<&serde_json::Value>,
) -> (ErrorStream, String) {
if json_mode {
let envelope = match details {
Some(d) => serde_json::json!({
"code": code,
"message": message,
"details": d,
}),
None => serde_json::json!({
"code": code,
"message": message,
}),
};
(
ErrorStream::Stdout,
serde_json::to_string(&envelope).unwrap_or_default(),
)
} else {
let mut line = format!("memstead: ERROR [{code}]: {message}");
if let Some(d) = details
&& !d.is_null()
&& let Ok(pretty) = serde_json::to_string_pretty(d)
{
line.push_str("\ndetails:");
for l in pretty.lines() {
line.push_str("\n ");
line.push_str(l);
}
}
(ErrorStream::Stderr, line)
}
}
pub fn print_cli_error(
code: &str,
message: &str,
_kind: ExitKind,
json_mode: bool,
details: Option<&serde_json::Value>,
) {
let (stream, line) = render_cli_error(code, message, json_mode, details);
match stream {
ErrorStream::Stdout => println!("{line}"),
ErrorStream::Stderr => eprintln!("{line}"),
}
}
pub fn print_markdown(markdown: &str) {
println!("{markdown}");
}
pub fn print_json<T: Serialize>(value: &T) -> anyhow::Result<()> {
let s = serde_json::to_string_pretty(value)?;
println!("{s}");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_error_routes_to_stdout_with_code() {
let details = serde_json::json!({ "id": "test--missing" });
let (stream, line) =
render_cli_error("ENTITY_NOT_FOUND", "not found", true, Some(&details));
assert_eq!(stream, ErrorStream::Stdout);
let parsed: serde_json::Value = serde_json::from_str(&line).expect("valid JSON line");
assert_eq!(parsed["code"], "ENTITY_NOT_FOUND");
assert_eq!(parsed["details"]["id"], "test--missing");
}
#[test]
fn markdown_error_routes_to_stderr() {
let (stream, line) = render_cli_error("ENTITY_NOT_FOUND", "not found", false, None);
assert_eq!(stream, ErrorStream::Stderr);
assert!(line.starts_with("memstead: ERROR [ENTITY_NOT_FOUND]: "));
}
#[test]
fn markdown_error_carries_details_block() {
let details = serde_json::json!({ "allowed_source_types": ["spec", "contract"] });
let (stream, line) =
render_cli_error("INVALID_REL_SHAPE", "bad edge", false, Some(&details));
assert_eq!(stream, ErrorStream::Stderr);
let mut lines = line.lines();
assert_eq!(
lines.next(),
Some("memstead: ERROR [INVALID_REL_SHAPE]: bad edge")
);
assert_eq!(lines.next(), Some("details:"));
assert!(line.contains("allowed_source_types"));
assert!(line.contains("contract"));
}
}