dig_node_control_interface/
error.rs1use serde::{Deserialize, Serialize};
23
24#[non_exhaustive]
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum ControlErrorCode {
31 ParseError,
33 InvalidRequest,
35 MethodNotFound,
37 InvalidParams,
39 DispatchFailed,
41 Unauthorized,
43 NotSupported,
46 ControlError,
48}
49
50impl ControlErrorCode {
51 pub const fn code(self) -> i64 {
53 match self {
54 ControlErrorCode::ParseError => -32700,
55 ControlErrorCode::InvalidRequest => -32600,
56 ControlErrorCode::MethodNotFound => -32601,
57 ControlErrorCode::InvalidParams => -32602,
58 ControlErrorCode::DispatchFailed => -32000,
59 ControlErrorCode::Unauthorized => -32030,
60 ControlErrorCode::NotSupported => -32031,
61 ControlErrorCode::ControlError => -32032,
62 }
63 }
64
65 pub const fn name(self) -> &'static str {
67 match self {
68 ControlErrorCode::ParseError => "PARSE_ERROR",
69 ControlErrorCode::InvalidRequest => "INVALID_REQUEST",
70 ControlErrorCode::MethodNotFound => "METHOD_NOT_FOUND",
71 ControlErrorCode::InvalidParams => "INVALID_PARAMS",
72 ControlErrorCode::DispatchFailed => "DISPATCH_FAILED",
73 ControlErrorCode::Unauthorized => "UNAUTHORIZED",
74 ControlErrorCode::NotSupported => "NOT_SUPPORTED",
75 ControlErrorCode::ControlError => "CONTROL_ERROR",
76 }
77 }
78
79 pub const fn origin(self) -> &'static str {
81 match self {
82 ControlErrorCode::MethodNotFound => "boundary",
83 ControlErrorCode::InvalidParams => "node",
84 _ => "shell",
85 }
86 }
87
88 pub const fn description(self) -> &'static str {
90 match self {
91 ControlErrorCode::ParseError => "Request body was not valid JSON.",
92 ControlErrorCode::InvalidRequest => {
93 "Request was not a single JSON-RPC object (batch arrays are not supported)."
94 }
95 ControlErrorCode::MethodNotFound => "The control method is not resolved by the node.",
96 ControlErrorCode::InvalidParams => "Invalid or missing method parameters.",
97 ControlErrorCode::DispatchFailed => "The node failed to dispatch the control request.",
98 ControlErrorCode::Unauthorized => {
99 "A control.* method was called without a valid local control token."
100 }
101 ControlErrorCode::NotSupported => {
102 "The requested control operation is not supported on this node build."
103 }
104 ControlErrorCode::ControlError => "A control operation failed at runtime.",
105 }
106 }
107
108 pub fn from_code(code: i64) -> Option<ControlErrorCode> {
110 ControlErrorCode::ALL
111 .iter()
112 .copied()
113 .find(|c| c.code() == code)
114 }
115
116 pub const ALL: &'static [ControlErrorCode] = &[
118 ControlErrorCode::ParseError,
119 ControlErrorCode::InvalidRequest,
120 ControlErrorCode::MethodNotFound,
121 ControlErrorCode::InvalidParams,
122 ControlErrorCode::DispatchFailed,
123 ControlErrorCode::Unauthorized,
124 ControlErrorCode::NotSupported,
125 ControlErrorCode::ControlError,
126 ];
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct ControlErrorData {
134 pub code: String,
136 pub origin: String,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct ControlError {
147 pub code: i64,
149 pub message: String,
151 pub data: ControlErrorData,
153}
154
155impl ControlError {
156 pub fn of(code: ControlErrorCode, message: impl Into<String>) -> ControlError {
158 ControlError {
159 code: code.code(),
160 message: message.into(),
161 data: ControlErrorData {
162 code: code.name().to_string(),
163 origin: code.origin().to_string(),
164 },
165 }
166 }
167
168 pub fn code_enum(&self) -> Option<ControlErrorCode> {
170 ControlErrorCode::from_code(self.code)
171 }
172}
173
174impl std::fmt::Display for ControlError {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 write!(f, "{} ({}): {}", self.data.code, self.code, self.message)
177 }
178}
179
180impl std::error::Error for ControlError {}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn codes_and_symbols_and_origins_are_stable() {
188 assert_eq!(ControlErrorCode::Unauthorized.code(), -32030);
189 assert_eq!(ControlErrorCode::NotSupported.code(), -32031);
190 assert_eq!(ControlErrorCode::ControlError.code(), -32032);
191 assert_eq!(ControlErrorCode::InvalidParams.name(), "INVALID_PARAMS");
192 assert_eq!(ControlErrorCode::InvalidParams.origin(), "node");
193 assert_eq!(ControlErrorCode::MethodNotFound.origin(), "boundary");
194 assert_eq!(ControlErrorCode::Unauthorized.origin(), "shell");
195 }
196
197 #[test]
198 fn from_code_round_trips_every_code() {
199 for &c in ControlErrorCode::ALL {
200 assert_eq!(ControlErrorCode::from_code(c.code()), Some(c));
201 }
202 assert_eq!(ControlErrorCode::from_code(0), None);
203 }
204
205 #[test]
206 fn every_code_has_a_description() {
207 for &c in ControlErrorCode::ALL {
208 assert!(!c.description().is_empty());
209 }
210 }
211
212 #[test]
213 fn error_serializes_to_the_canonical_json_shape() {
214 let e = ControlError::of(ControlErrorCode::Unauthorized, "no token");
215 let v = serde_json::to_value(&e).unwrap();
216 assert_eq!(v["code"], -32030);
217 assert_eq!(v["message"], "no token");
218 assert_eq!(v["data"]["code"], "UNAUTHORIZED");
219 assert_eq!(v["data"]["origin"], "shell");
220 assert_eq!(e.code_enum(), Some(ControlErrorCode::Unauthorized));
221 }
222
223 #[test]
224 fn display_is_symbol_code_and_message() {
225 let e = ControlError::of(ControlErrorCode::ControlError, "boom");
226 assert_eq!(e.to_string(), "CONTROL_ERROR (-32032): boom");
227 }
228}