1use std::collections::HashMap;
10use std::io;
11use thiserror::Error;
12
13pub type Result<T> = std::result::Result<T, Error>;
15
16#[derive(Debug, Error)]
18pub enum Error {
19 #[error("Device not found: {device_id}")]
21 DeviceNotFound { device_id: String },
22
23 #[error("Device already exists: {device_id}")]
25 DeviceAlreadyExists { device_id: String },
26
27 #[error("Data point not found: {device_id}/{point_id}")]
29 DataPointNotFound { device_id: String, point_id: String },
30
31 #[error("Invalid address: {address} (expected range: {min}-{max})")]
33 InvalidAddress { address: u32, min: u32, max: u32 },
34
35 #[error("Invalid value for data point {point_id}: {reason}")]
37 InvalidValue { point_id: String, reason: String },
38
39 #[error("Type mismatch: expected {expected}, got {actual}")]
41 TypeMismatch { expected: String, actual: String },
42
43 #[error("Protocol error: {0}")]
45 Protocol(String),
46
47 #[error("Configuration error: {0}")]
49 Config(String),
50
51 #[error("Validation failed: {message}")]
53 Validation {
54 message: String,
55 errors: ValidationErrors,
56 },
57
58 #[error("I/O error: {0}")]
60 Io(#[from] io::Error),
61
62 #[error("Engine error: {0}")]
64 Engine(String),
65
66 #[error("Lifecycle error: invalid transition from {from:?} to {to:?}")]
68 Lifecycle {
69 from: crate::device::DeviceState,
70 to: crate::device::DeviceState,
71 },
72
73 #[error("Capacity exceeded: {current}/{max} {resource}")]
75 CapacityExceeded {
76 current: usize,
77 max: usize,
78 resource: String,
79 },
80
81 #[error("Operation timed out after {duration_ms}ms")]
83 Timeout { duration_ms: u64 },
84
85 #[error("Operation not supported: {0}")]
87 NotSupported(String),
88
89 #[error("Internal error: {0}")]
91 Internal(String),
92
93 #[error("Serialization error: {0}")]
95 Serialization(String),
96
97 #[error("Channel error: {0}")]
99 Channel(String),
100
101 #[error("Access denied: {operation} not allowed on {point_id} (mode: {mode})")]
103 AccessDenied {
104 point_id: String,
105 operation: String,
106 mode: String,
107 },
108
109 #[error("Value {value} out of range [{min}, {max}] for {point_id}")]
111 OutOfRange {
112 point_id: String,
113 value: f64,
114 min: f64,
115 max: f64,
116 },
117}
118
119impl Error {
120 pub fn device_not_found(device_id: impl Into<String>) -> Self {
122 Self::DeviceNotFound {
123 device_id: device_id.into(),
124 }
125 }
126
127 pub fn point_not_found(device_id: impl Into<String>, point_id: impl Into<String>) -> Self {
129 Self::DataPointNotFound {
130 device_id: device_id.into(),
131 point_id: point_id.into(),
132 }
133 }
134
135 pub fn capacity_exceeded(current: usize, max: usize, resource: impl Into<String>) -> Self {
137 Self::CapacityExceeded {
138 current,
139 max,
140 resource: resource.into(),
141 }
142 }
143
144 pub fn access_denied(
146 point_id: impl Into<String>,
147 operation: impl Into<String>,
148 mode: impl Into<String>,
149 ) -> Self {
150 Self::AccessDenied {
151 point_id: point_id.into(),
152 operation: operation.into(),
153 mode: mode.into(),
154 }
155 }
156
157 pub fn out_of_range(point_id: impl Into<String>, value: f64, min: f64, max: f64) -> Self {
159 Self::OutOfRange {
160 point_id: point_id.into(),
161 value,
162 min,
163 max,
164 }
165 }
166
167 pub fn validation(errors: ValidationErrors) -> Self {
169 let count = errors.len();
170 Self::Validation {
171 message: format!("{} validation error(s)", count),
172 errors,
173 }
174 }
175
176 pub fn is_recoverable(&self) -> bool {
178 matches!(self, Self::Timeout { .. } | Self::Io(_) | Self::Channel(_))
179 }
180
181 pub fn is_validation(&self) -> bool {
183 matches!(self, Self::Validation { .. })
184 }
185
186 pub fn is_not_found(&self) -> bool {
188 matches!(
189 self,
190 Self::DeviceNotFound { .. } | Self::DataPointNotFound { .. }
191 )
192 }
193
194 pub fn severity(&self) -> ErrorSeverity {
196 match self {
197 Self::Internal(_) | Self::Engine(_) => ErrorSeverity::Critical,
198 Self::Protocol(_) | Self::Lifecycle { .. } => ErrorSeverity::High,
199 Self::Timeout { .. } | Self::Io(_) | Self::Channel(_) => ErrorSeverity::Medium,
200 Self::Validation { .. }
201 | Self::InvalidValue { .. }
202 | Self::TypeMismatch { .. }
203 | Self::OutOfRange { .. }
204 | Self::AccessDenied { .. } => ErrorSeverity::Low,
205 _ => ErrorSeverity::Medium,
206 }
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
212pub enum ErrorSeverity {
213 Low,
215 Medium,
217 High,
219 Critical,
221}
222
223#[derive(Debug, Clone, Default)]
225pub struct ValidationErrors {
226 errors: HashMap<String, Vec<String>>,
227}
228
229impl ValidationErrors {
230 pub fn new() -> Self {
232 Self::default()
233 }
234
235 pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
237 self.errors
238 .entry(field.into())
239 .or_default()
240 .push(message.into());
241 }
242
243 pub fn add_if(
245 &mut self,
246 condition: bool,
247 field: impl Into<String>,
248 message: impl Into<String>,
249 ) {
250 if condition {
251 self.add(field, message);
252 }
253 }
254
255 pub fn is_empty(&self) -> bool {
257 self.errors.is_empty()
258 }
259
260 pub fn len(&self) -> usize {
262 self.errors.len()
263 }
264
265 pub fn total_errors(&self) -> usize {
267 self.errors.values().map(|v| v.len()).sum()
268 }
269
270 pub fn get(&self, field: &str) -> Option<&Vec<String>> {
272 self.errors.get(field)
273 }
274
275 pub fn fields(&self) -> impl Iterator<Item = &String> {
277 self.errors.keys()
278 }
279
280 pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<String>)> {
282 self.errors.iter()
283 }
284
285 pub fn into_result<T>(self, ok: T) -> Result<T> {
287 if self.is_empty() {
288 Ok(ok)
289 } else {
290 Err(Error::validation(self))
291 }
292 }
293
294 pub fn merge(&mut self, other: ValidationErrors) {
296 for (field, messages) in other.errors {
297 self.errors.entry(field).or_default().extend(messages);
298 }
299 }
300}
301
302impl std::fmt::Display for ValidationErrors {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 let mut first = true;
305 for (field, messages) in &self.errors {
306 for message in messages {
307 if !first {
308 write!(f, "; ")?;
309 }
310 write!(f, "{}: {}", field, message)?;
311 first = false;
312 }
313 }
314 Ok(())
315 }
316}
317
318#[derive(Debug, Default)]
320pub struct ValidationErrorsBuilder {
321 errors: ValidationErrors,
322}
323
324impl ValidationErrorsBuilder {
325 pub fn new() -> Self {
327 Self::default()
328 }
329
330 pub fn add(mut self, field: impl Into<String>, message: impl Into<String>) -> Self {
332 self.errors.add(field, message);
333 self
334 }
335
336 pub fn add_if(
338 mut self,
339 condition: bool,
340 field: impl Into<String>,
341 message: impl Into<String>,
342 ) -> Self {
343 self.errors.add_if(condition, field, message);
344 self
345 }
346
347 pub fn build(self) -> ValidationErrors {
349 self.errors
350 }
351
352 pub fn into_result<T>(self, ok: T) -> Result<T> {
354 self.errors.into_result(ok)
355 }
356}
357
358impl From<serde_json::Error> for Error {
359 fn from(err: serde_json::Error) -> Self {
360 Self::Serialization(err.to_string())
361 }
362}
363
364impl From<serde_yaml::Error> for Error {
365 fn from(err: serde_yaml::Error) -> Self {
366 Self::Serialization(err.to_string())
367 }
368}
369
370pub trait ResultExt<T> {
372 fn with_context<F, S>(self, f: F) -> Result<T>
374 where
375 F: FnOnce() -> S,
376 S: Into<String>;
377}
378
379impl<T> ResultExt<T> for Result<T> {
380 fn with_context<F, S>(self, f: F) -> Result<T>
381 where
382 F: FnOnce() -> S,
383 S: Into<String>,
384 {
385 self.map_err(|e| {
386 let context = f().into();
387 Error::Internal(format!("{}: {}", context, e))
388 })
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_error_display() {
398 let err = Error::device_not_found("device-001");
399 assert_eq!(err.to_string(), "Device not found: device-001");
400 }
401
402 #[test]
403 fn test_error_recoverable() {
404 assert!(Error::Timeout { duration_ms: 1000 }.is_recoverable());
405 assert!(!Error::DeviceNotFound {
406 device_id: "x".into()
407 }
408 .is_recoverable());
409 }
410
411 #[test]
412 fn test_validation_errors() {
413 let mut errors = ValidationErrors::new();
414 errors.add("name", "cannot be empty");
415 errors.add("name", "must be at least 3 characters");
416 errors.add("id", "must be unique");
417
418 assert_eq!(errors.len(), 2);
419 assert_eq!(errors.total_errors(), 3);
420 assert_eq!(errors.get("name").unwrap().len(), 2);
421 }
422
423 #[test]
424 fn test_validation_errors_builder() {
425 let errors = ValidationErrorsBuilder::new()
426 .add("field1", "error1")
427 .add_if(true, "field2", "error2")
428 .add_if(false, "field3", "error3") .build();
430
431 assert_eq!(errors.len(), 2);
432 assert!(errors.get("field3").is_none());
433 }
434
435 #[test]
436 fn test_validation_into_result() {
437 let errors = ValidationErrors::new();
438 let result: Result<i32> = errors.into_result(42);
439 assert!(result.is_ok());
440
441 let mut errors = ValidationErrors::new();
442 errors.add("field", "error");
443 let result: Result<i32> = errors.into_result(42);
444 assert!(result.is_err());
445 }
446
447 #[test]
448 fn test_error_severity() {
449 assert_eq!(
450 Error::Internal("test".into()).severity(),
451 ErrorSeverity::Critical
452 );
453 assert_eq!(
454 Error::Timeout { duration_ms: 1000 }.severity(),
455 ErrorSeverity::Medium
456 );
457 assert_eq!(
458 Error::InvalidValue {
459 point_id: "x".into(),
460 reason: "y".into()
461 }
462 .severity(),
463 ErrorSeverity::Low
464 );
465 }
466
467 #[test]
468 fn test_access_denied_error() {
469 let err = Error::access_denied("temp", "write", "readonly");
470 assert_eq!(
471 err.to_string(),
472 "Access denied: write not allowed on temp (mode: readonly)"
473 );
474 }
475
476 #[test]
477 fn test_out_of_range_error() {
478 let err = Error::out_of_range("temp", 150.0, 0.0, 100.0);
479 assert_eq!(
480 err.to_string(),
481 "Value 150 out of range [0, 100] for temp"
482 );
483 }
484}