use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
ParseError,
InvalidRequest,
MethodNotFound,
InvalidParams,
InternalError,
Unauthorized,
NotFound,
AgentDisconnected,
RateLimit,
StaleProtocol,
PayloadTooLarge,
}
impl ErrorKind {
pub fn code(self) -> i32 {
match self {
Self::ParseError => -32700,
Self::InvalidRequest => -32600,
Self::MethodNotFound => -32601,
Self::InvalidParams => -32602,
Self::InternalError => -32603,
Self::Unauthorized => -32000,
Self::NotFound => -32001,
Self::AgentDisconnected => -32002,
Self::RateLimit => -32003,
Self::StaleProtocol => -32004,
Self::PayloadTooLarge => -32005,
}
}
pub fn default_message(self) -> &'static str {
match self {
Self::ParseError => "Parse error",
Self::InvalidRequest => "Invalid Request",
Self::MethodNotFound => "Method not found",
Self::InvalidParams => "Invalid params",
Self::InternalError => "Internal error",
Self::Unauthorized => "Unauthorized",
Self::NotFound => "Not found",
Self::AgentDisconnected => "Agent disconnected from broker",
Self::RateLimit => "Rate limit exceeded",
Self::StaleProtocol => "Stale protocol version",
Self::PayloadTooLarge => "Payload too large",
}
}
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct RpcError {
pub code: i32,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<RpcErrorData>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
pub struct RpcErrorData {
pub kind: ErrorKind,
pub detail: String,
}
impl RpcError {
pub fn new(kind: ErrorKind, detail: impl Into<String>) -> Self {
Self {
code: kind.code(),
message: kind.default_message().to_string(),
data: Some(RpcErrorData {
kind,
detail: detail.into(),
}),
}
}
pub fn bare(kind: ErrorKind) -> Self {
Self {
code: kind.code(),
message: kind.default_message().to_string(),
data: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_kind_codes_match_spec_table() {
assert_eq!(ErrorKind::ParseError.code(), -32700);
assert_eq!(ErrorKind::InvalidRequest.code(), -32600);
assert_eq!(ErrorKind::MethodNotFound.code(), -32601);
assert_eq!(ErrorKind::InvalidParams.code(), -32602);
assert_eq!(ErrorKind::InternalError.code(), -32603);
assert_eq!(ErrorKind::Unauthorized.code(), -32000);
assert_eq!(ErrorKind::NotFound.code(), -32001);
assert_eq!(ErrorKind::AgentDisconnected.code(), -32002);
assert_eq!(ErrorKind::RateLimit.code(), -32003);
assert_eq!(ErrorKind::StaleProtocol.code(), -32004);
assert_eq!(ErrorKind::PayloadTooLarge.code(), -32005);
}
#[test]
fn error_kind_serialises_pascal_case_per_spec() {
let json = serde_json::to_string(&ErrorKind::StaleProtocol).unwrap();
assert_eq!(json, "\"StaleProtocol\"");
let json = serde_json::to_string(&ErrorKind::RateLimit).unwrap();
assert_eq!(json, "\"RateLimit\"");
let json = serde_json::to_string(&ErrorKind::Unauthorized).unwrap();
assert_eq!(json, "\"Unauthorized\"");
}
#[test]
fn rpc_error_new_round_trips_through_json() {
let e = RpcError::new(
ErrorKind::Unauthorized,
"manifest 'reboot' has user_invokable=false",
);
let json = serde_json::to_string(&e).unwrap();
let back: RpcError = serde_json::from_str(&json).unwrap();
assert_eq!(back.code, -32000);
assert_eq!(back.message, "Unauthorized");
let data = back.data.expect("data populated");
assert_eq!(data.kind, ErrorKind::Unauthorized);
assert_eq!(data.detail, "manifest 'reboot' has user_invokable=false");
}
#[test]
fn rpc_error_bare_round_trips_without_data_field() {
let e = RpcError::bare(ErrorKind::ParseError);
let v = serde_json::to_value(&e).unwrap();
assert!(
v.get("data").is_none(),
"data field must be absent on the wire, got {v:?}",
);
assert_eq!(v["code"], -32700);
}
#[test]
fn rpc_error_spec_example_decodes() {
let wire = r#"{
"code": -32000,
"message": "Job not user-invokable",
"data": {"kind":"Unauthorized","detail":"manifest 'reboot' has user_invokable=false"}
}"#;
let e: RpcError = serde_json::from_str(wire).expect("decode");
assert_eq!(e.code, -32000);
let data = e.data.expect("data populated");
assert_eq!(data.kind, ErrorKind::Unauthorized);
assert!(data.detail.contains("user_invokable=false"));
}
}