haematite-cli 0.21.8

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::{fmt, io};

use serde::Serialize;

#[derive(Debug)]
enum Cause {
    Io(io::ErrorKind, Option<i32>),
    /// The store's own refusal name, so a log line says why it refused.
    Store(String),
}

impl fmt::Display for Cause {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(kind, number) => {
                write!(f, "io:{kind:?}")?;
                if let Some(number) = number {
                    write!(f, ";errno={number}")?;
                }
                Ok(())
            }
            Self::Store(code) => write!(f, "store:{code}"),
        }
    }
}

#[derive(Debug, Serialize)]
pub(super) struct Error {
    pub(super) code: &'static str,
    pub(super) message: &'static str,
    #[serde(skip)]
    pub(super) status: u16,
    #[serde(skip)]
    cause: Option<Cause>,
}

impl Error {
    pub(super) const fn new(status: u16, code: &'static str, message: &'static str) -> Self {
        Self {
            code,
            message,
            status,
            cause: None,
        }
    }

    pub(super) const fn config(message: &'static str) -> Self {
        Self::new(500, "mcp_http_config_invalid", message)
    }

    pub(super) fn with_io(mut self, error: &io::Error) -> Self {
        self.cause = Some(Cause::Io(error.kind(), error.raw_os_error()));
        self
    }

    /// Names the store's refusal in the log line, never in the answer: only
    /// a plain name of at most 96 bytes, anything else as `unnamed`.
    pub(super) fn with_store_refusal(mut self, code: &str) -> Self {
        let named = !code.is_empty()
            && code.len() <= 96
            && code
                .bytes()
                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_');
        self.cause = Some(Cause::Store(if named {
            code.to_owned()
        } else {
            "unnamed".to_owned()
        }));
        self
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.code, self.message)?;
        self.cause
            .as_ref()
            .map_or(Ok(()), |cause| write!(f, "; cause={cause}"))
    }
}

impl std::error::Error for Error {}