use serde_json::{Value, json};
use std::sync::OnceLock;
pub const OK: i32 = 0;
pub const ERROR: i32 = 1;
pub const REFUSED: i32 = 2;
pub const PENDING: i32 = 3;
#[derive(Debug)]
pub struct Refused(pub String);
impl std::fmt::Display for Refused {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for Refused {}
pub fn code_for(error: &anyhow::Error) -> i32 {
if error.downcast_ref::<Refused>().is_some() {
REFUSED
} else {
ERROR
}
}
static JSON: OnceLock<bool> = OnceLock::new();
pub fn set_json(on: bool) {
let _ = JSON.set(on);
}
pub fn json() -> bool {
*JSON.get().unwrap_or(&false)
}
pub fn emit(value: Value) {
if json() {
println!("{value}");
}
}
pub fn emit_error(error: &anyhow::Error, code: i32) {
emit(json!({
"ok": false,
"error": format!("{error:#}"),
"refused": code == REFUSED,
"exit_code": code,
}));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_refusal_is_not_an_ordinary_error() {
let refused = anyhow::Error::new(Refused("no seed on google".into()));
assert_eq!(code_for(&refused), REFUSED);
let broke = anyhow::anyhow!("the connection dropped");
assert_eq!(code_for(&broke), ERROR);
}
#[test]
fn a_refusal_carries_its_message_through_context() {
let refused = anyhow::Error::new(Refused("use comfyui, which is free".into()))
.context("generating an image");
assert_eq!(code_for(&refused), REFUSED, "context must not hide the kind");
assert!(
format!("{refused:#}").contains("use comfyui"),
"the way forward was lost: {refused:#}"
);
}
#[test]
fn every_outcome_has_its_own_code() {
let codes = [OK, ERROR, REFUSED, PENDING];
let unique: std::collections::BTreeSet<i32> = codes.into_iter().collect();
assert_eq!(unique.len(), codes.len(), "two outcomes share a code");
}
}