use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ControlErrorCode {
ParseError,
InvalidRequest,
MethodNotFound,
InvalidParams,
DispatchFailed,
Unauthorized,
NotSupported,
ControlError,
}
impl ControlErrorCode {
pub const fn code(self) -> i64 {
match self {
ControlErrorCode::ParseError => -32700,
ControlErrorCode::InvalidRequest => -32600,
ControlErrorCode::MethodNotFound => -32601,
ControlErrorCode::InvalidParams => -32602,
ControlErrorCode::DispatchFailed => -32000,
ControlErrorCode::Unauthorized => -32030,
ControlErrorCode::NotSupported => -32031,
ControlErrorCode::ControlError => -32032,
}
}
pub const fn name(self) -> &'static str {
match self {
ControlErrorCode::ParseError => "PARSE_ERROR",
ControlErrorCode::InvalidRequest => "INVALID_REQUEST",
ControlErrorCode::MethodNotFound => "METHOD_NOT_FOUND",
ControlErrorCode::InvalidParams => "INVALID_PARAMS",
ControlErrorCode::DispatchFailed => "DISPATCH_FAILED",
ControlErrorCode::Unauthorized => "UNAUTHORIZED",
ControlErrorCode::NotSupported => "NOT_SUPPORTED",
ControlErrorCode::ControlError => "CONTROL_ERROR",
}
}
pub const fn origin(self) -> &'static str {
match self {
ControlErrorCode::MethodNotFound => "boundary",
ControlErrorCode::InvalidParams => "node",
_ => "shell",
}
}
pub const fn description(self) -> &'static str {
match self {
ControlErrorCode::ParseError => "Request body was not valid JSON.",
ControlErrorCode::InvalidRequest => {
"Request was not a single JSON-RPC object (batch arrays are not supported)."
}
ControlErrorCode::MethodNotFound => "The control method is not resolved by the node.",
ControlErrorCode::InvalidParams => "Invalid or missing method parameters.",
ControlErrorCode::DispatchFailed => "The node failed to dispatch the control request.",
ControlErrorCode::Unauthorized => {
"A control.* method was called without a valid local control token."
}
ControlErrorCode::NotSupported => {
"The requested control operation is not supported on this node build."
}
ControlErrorCode::ControlError => "A control operation failed at runtime.",
}
}
pub fn from_code(code: i64) -> Option<ControlErrorCode> {
ControlErrorCode::ALL
.iter()
.copied()
.find(|c| c.code() == code)
}
pub const ALL: &'static [ControlErrorCode] = &[
ControlErrorCode::ParseError,
ControlErrorCode::InvalidRequest,
ControlErrorCode::MethodNotFound,
ControlErrorCode::InvalidParams,
ControlErrorCode::DispatchFailed,
ControlErrorCode::Unauthorized,
ControlErrorCode::NotSupported,
ControlErrorCode::ControlError,
];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlErrorData {
pub code: String,
pub origin: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlError {
pub code: i64,
pub message: String,
pub data: ControlErrorData,
}
impl ControlError {
pub fn of(code: ControlErrorCode, message: impl Into<String>) -> ControlError {
ControlError {
code: code.code(),
message: message.into(),
data: ControlErrorData {
code: code.name().to_string(),
origin: code.origin().to_string(),
},
}
}
pub fn code_enum(&self) -> Option<ControlErrorCode> {
ControlErrorCode::from_code(self.code)
}
}
impl std::fmt::Display for ControlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({}): {}", self.data.code, self.code, self.message)
}
}
impl std::error::Error for ControlError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codes_and_symbols_and_origins_are_stable() {
assert_eq!(ControlErrorCode::Unauthorized.code(), -32030);
assert_eq!(ControlErrorCode::NotSupported.code(), -32031);
assert_eq!(ControlErrorCode::ControlError.code(), -32032);
assert_eq!(ControlErrorCode::InvalidParams.name(), "INVALID_PARAMS");
assert_eq!(ControlErrorCode::InvalidParams.origin(), "node");
assert_eq!(ControlErrorCode::MethodNotFound.origin(), "boundary");
assert_eq!(ControlErrorCode::Unauthorized.origin(), "shell");
}
#[test]
fn from_code_round_trips_every_code() {
for &c in ControlErrorCode::ALL {
assert_eq!(ControlErrorCode::from_code(c.code()), Some(c));
}
assert_eq!(ControlErrorCode::from_code(0), None);
}
#[test]
fn every_code_has_a_description() {
for &c in ControlErrorCode::ALL {
assert!(!c.description().is_empty());
}
}
#[test]
fn error_serializes_to_the_canonical_json_shape() {
let e = ControlError::of(ControlErrorCode::Unauthorized, "no token");
let v = serde_json::to_value(&e).unwrap();
assert_eq!(v["code"], -32030);
assert_eq!(v["message"], "no token");
assert_eq!(v["data"]["code"], "UNAUTHORIZED");
assert_eq!(v["data"]["origin"], "shell");
assert_eq!(e.code_enum(), Some(ControlErrorCode::Unauthorized));
}
#[test]
fn display_is_symbol_code_and_message() {
let e = ControlError::of(ControlErrorCode::ControlError, "boom");
assert_eq!(e.to_string(), "CONTROL_ERROR (-32032): boom");
}
}