use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum BoteError {
#[error("tool not found: {0}")]
ToolNotFound(String),
#[error("invalid params for tool '{tool}': {reason}")]
InvalidParams { tool: String, reason: String },
#[error("tool execution failed: {tool} — {reason}")]
ExecFailed { tool: String, reason: String },
#[error("protocol error: {0}")]
Protocol(String),
#[error("parse error: {0}")]
Parse(String),
#[error("transport closed")]
TransportClosed,
#[error("transport bind failed: {0}")]
BindFailed(String),
#[error("request cancelled: {0}")]
RequestCancelled(String),
#[error("schema violation for tool '{tool}': {}", violations.join("; "))]
SchemaViolation {
tool: String,
violations: Vec<String>,
},
#[error("sandbox error for tool '{tool}': {reason}")]
SandboxError { tool: String, reason: String },
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl BoteError {
#[inline]
#[must_use]
pub fn rpc_code(&self) -> i32 {
match self {
Self::Parse(_) => -32700,
Self::Protocol(_) => -32600,
Self::ToolNotFound(_) => -32601,
Self::InvalidParams { .. } => -32602,
Self::ExecFailed { .. } => -32000,
Self::TransportClosed => -32003,
Self::BindFailed(_) => -32003,
Self::RequestCancelled(_) => -32800,
Self::SchemaViolation { .. } => -32602,
Self::SandboxError { .. } => -32000,
Self::Json(_) => -32700,
Self::Io(_) => -32603,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rpc_codes_all_variants() {
assert_eq!(BoteError::Parse("bad".into()).rpc_code(), -32700);
assert_eq!(BoteError::Protocol("bad".into()).rpc_code(), -32600);
assert_eq!(BoteError::ToolNotFound("x".into()).rpc_code(), -32601);
assert_eq!(
BoteError::InvalidParams {
tool: "x".into(),
reason: "y".into()
}
.rpc_code(),
-32602
);
assert_eq!(
BoteError::ExecFailed {
tool: "x".into(),
reason: "y".into()
}
.rpc_code(),
-32000
);
assert_eq!(BoteError::TransportClosed.rpc_code(), -32003);
assert_eq!(
BoteError::BindFailed("port in use".into()).rpc_code(),
-32003
);
assert_eq!(
BoteError::RequestCancelled("req-1".into()).rpc_code(),
-32800
);
let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken");
assert_eq!(BoteError::Io(io_err).rpc_code(), -32603);
}
#[test]
fn display_messages() {
assert_eq!(
BoteError::ToolNotFound("foo".into()).to_string(),
"tool not found: foo"
);
assert_eq!(
BoteError::InvalidParams {
tool: "t".into(),
reason: "r".into()
}
.to_string(),
"invalid params for tool 't': r"
);
assert_eq!(
BoteError::ExecFailed {
tool: "t".into(),
reason: "r".into()
}
.to_string(),
"tool execution failed: t — r"
);
assert_eq!(
BoteError::Protocol("bad".into()).to_string(),
"protocol error: bad"
);
assert_eq!(
BoteError::Parse("bad".into()).to_string(),
"parse error: bad"
);
assert_eq!(BoteError::TransportClosed.to_string(), "transport closed");
assert_eq!(
BoteError::BindFailed("port in use".into()).to_string(),
"transport bind failed: port in use"
);
assert_eq!(
BoteError::RequestCancelled("req-1".into()).to_string(),
"request cancelled: req-1"
);
}
#[test]
fn json_error_from_serde() {
let err: BoteError = serde_json::from_str::<serde_json::Value>("not json")
.unwrap_err()
.into();
assert_eq!(err.rpc_code(), -32700);
assert!(!err.to_string().is_empty());
}
#[test]
fn io_error_from_std() {
let err: BoteError = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
assert_eq!(err.rpc_code(), -32603);
assert!(err.to_string().contains("gone"));
}
}