pub mod capability_errors;
pub mod channel_errors;
pub mod common_errors;
pub mod crypto_errors;
pub mod device_errors;
pub mod group_errors;
pub mod protocol_errors;
pub mod storage_errors;
pub mod stream_errors;
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorCode(pub u16);
impl ErrorCode {
pub fn module(&self) -> u16 {
self.0 / 100
}
pub fn sequence(&self) -> u16 {
self.0 % 100
}
}
impl std::str::FromStr for ErrorCode {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let value = s.parse::<u16>().map_err(|e| e.to_string())?;
if value < 10000 {
Ok(Self(value))
} else {
Err("Error code must be less than 10000".to_string())
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "XP-{:04}", self.0)
}
}
impl fmt::LowerHex for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{:04x}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorCategory {
System,
Channel,
Crypto,
Group,
Device,
Stream,
Storage,
Protocol,
Capability,
}
impl ErrorCategory {
pub fn name(&self) -> &'static str {
match self {
Self::System => "System",
Self::Channel => "Channel",
Self::Crypto => "Crypto",
Self::Group => "Group",
Self::Device => "Device",
Self::Stream => "Stream",
Self::Storage => "Storage",
Self::Protocol => "Protocol",
Self::Capability => "Capability",
}
}
pub fn code_range(&self) -> (u16, u16) {
match self {
Self::System => (100, 199),
Self::Channel => (200, 299),
Self::Crypto => (300, 399),
Self::Group => (400, 499),
Self::Device => (500, 599),
Self::Stream => (600, 699),
Self::Storage => (700, 799),
Self::Protocol => (800, 899),
Self::Capability => (900, 999),
}
}
}
impl fmt::Display for ErrorCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum RetrySuggestion {
#[default]
NoRetry,
Retryable {
max_attempts: u32,
base_delay_ms: u64,
},
ManualIntervention,
Fatal,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ErrorContext {
pub location: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub group_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
pub original_message: Box<str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debug_info: Option<Box<serde_json::Value>>,
pub timestamp: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_suggestion: Option<RetrySuggestion>,
#[serde(skip_serializing_if = "Option::is_none")]
pub impact_scope: Option<ImpactScope>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImpactScope {
Operation,
Session,
Device,
Group,
System,
}
impl ErrorContext {
#[inline]
pub fn new(location: &'static str, original_message: String) -> Self {
Self {
location,
original_message: original_message.into_boxed_str(),
timestamp: chrono::Utc::now(),
..Default::default()
}
}
#[inline]
pub fn with_device_id(mut self, device_id: impl Into<String>) -> Self {
self.device_id = Some(device_id.into());
self
}
#[inline]
pub fn with_group_id(mut self, group_id: impl Into<String>) -> Self {
self.group_id = Some(group_id.into());
self
}
#[inline]
pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
#[inline]
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
self.request_id = Some(request_id.into());
self
}
#[inline]
pub fn with_debug_info(mut self, info: serde_json::Value) -> Self {
self.debug_info = Some(Box::new(info));
self
}
#[inline]
pub fn with_retry_suggestion(mut self, suggestion: RetrySuggestion) -> Self {
self.retry_suggestion = Some(suggestion);
self
}
#[inline]
pub fn with_impact_scope(mut self, scope: ImpactScope) -> Self {
self.impact_scope = Some(scope);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetailedError {
pub code: ErrorCode,
pub category: ErrorCategory,
pub message: String,
pub technical_details: String,
pub context: ErrorContext,
#[serde(skip_serializing_if = "Option::is_none")]
pub root_cause: Option<Box<DetailedError>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<&'static str>,
}
impl DetailedError {
#[inline]
pub fn new(
code: ErrorCode,
category: ErrorCategory,
message: String,
technical_details: String,
location: &'static str,
) -> Self {
let context = ErrorContext::new(location, technical_details.clone());
Self {
code,
category,
message,
technical_details,
context,
root_cause: None,
documentation_url: None,
}
}
#[inline]
pub fn with_device_id(mut self, device_id: impl Into<String>) -> Self {
self.context.device_id = Some(device_id.into());
self
}
#[inline]
pub fn with_group_id(mut self, group_id: impl Into<String>) -> Self {
self.context.group_id = Some(group_id.into());
self
}
#[inline]
pub fn with_debug_info(mut self, info: serde_json::Value) -> Self {
self.context.debug_info = Some(Box::new(info));
self
}
#[inline]
pub fn with_retry_suggestion(mut self, suggestion: RetrySuggestion) -> Self {
self.context.retry_suggestion = Some(suggestion);
self
}
#[inline]
pub fn with_root_cause(mut self, cause: DetailedError) -> Self {
self.root_cause = Some(Box::new(cause));
self
}
#[inline]
pub fn with_docs(mut self, url: &'static str) -> Self {
self.documentation_url = Some(url);
self
}
#[inline]
pub fn to_json(&self) -> std::result::Result<String, serde_json::Error> {
serde_json::to_string(self)
}
#[inline]
pub fn from_json(s: &'static str) -> std::result::Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
}
impl fmt::Display for DetailedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {} - {} (Location: {})",
self.code, self.message, self.technical_details, self.context.location
)
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
#[error("{message}")]
pub struct XLinkError {
pub code: ErrorCode,
pub category: ErrorCategory,
pub message: String,
#[serde(default)]
pub context: Box<ErrorContext>,
#[serde(default)]
pub source: Option<Box<XLinkError>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<&'static str>,
}
impl XLinkError {
fn new_internal(
code: ErrorCode,
category: ErrorCategory,
message: String,
technical_details: &str,
location: &'static str,
) -> Self {
Self {
code,
category,
message,
context: Box::new(ErrorContext::new(location, technical_details.to_string())),
source: None,
documentation_url: None,
}
}
#[inline]
pub fn with_device_id<S: Into<String>>(mut self, device_id: S) -> Self {
self.context.device_id = Some(device_id.into());
self
}
#[inline]
pub fn with_group_id<S: Into<String>>(mut self, group_id: S) -> Self {
self.context.group_id = Some(group_id.into());
self
}
#[inline]
pub fn with_session_id<S: Into<String>>(mut self, session_id: S) -> Self {
self.context.session_id = Some(session_id.into());
self
}
#[inline]
pub fn with_request_id<S: Into<String>>(mut self, request_id: S) -> Self {
self.context.request_id = Some(request_id.into());
self
}
#[inline]
pub fn with_debug_info(mut self, info: serde_json::Value) -> Self {
self.context.debug_info = Some(Box::new(info));
self
}
#[inline]
pub fn with_retry_suggestion(mut self, suggestion: RetrySuggestion) -> Self {
self.context.retry_suggestion = Some(suggestion);
self
}
#[inline]
pub fn with_docs(mut self, url: &'static str) -> Self {
self.documentation_url = Some(url);
self
}
#[inline]
pub fn with_source(mut self, source: XLinkError) -> Self {
self.source = Some(Box::new(source));
self
}
#[inline]
pub fn code(&self) -> ErrorCode {
self.code
}
#[inline]
pub fn category(&self) -> ErrorCategory {
self.category
}
#[inline]
pub fn message(&self) -> &str {
&self.message
}
#[inline]
pub fn original_message(&self) -> &str {
&self.context.original_message
}
#[inline]
pub fn location(&self) -> &'static str {
self.context.location
}
#[inline]
pub fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
self.context.timestamp
}
#[inline]
pub fn is_retryable(&self) -> bool {
matches!(
self.context.retry_suggestion,
Some(RetrySuggestion::Retryable { .. })
)
}
#[inline]
pub fn retry_suggestion(&self) -> Option<RetrySuggestion> {
self.context.retry_suggestion
}
#[inline]
pub fn to_json(&self) -> std::result::Result<String, serde_json::Error> {
serde_json::to_string(self)
}
#[inline]
pub fn to_detailed(self) -> DetailedError {
let code = self.code;
let category = self.category;
let message = self.message.clone();
let _location = self.context.location;
let technical_details = self.context.original_message.to_string();
let context = *self.context;
let source = self.source.map(|s| {
let xlink_error = *s;
Box::new(xlink_error.to_detailed())
});
DetailedError {
code,
category,
message,
technical_details,
context,
root_cause: source,
documentation_url: None,
}
}
#[inline]
pub fn source_iter(&self) -> SourceIter<'_> {
SourceIter(Some(self))
}
#[inline]
pub fn to_log_string(&self) -> String {
let mut result = format!(
"[{}] {} | Category: {} | Location: {} | Time: {}",
self.code, self.message, self.category, self.context.location, self.context.timestamp
);
if let Some(ref device_id) = self.context.device_id {
result.push_str(&format!(" | Device: {}", device_id));
}
if let Some(ref group_id) = self.context.group_id {
result.push_str(&format!(" | Group: {}", group_id));
}
if let Some(ref suggestion) = self.context.retry_suggestion {
result.push_str(&format!(" | Retry: {:?}", suggestion));
}
result
}
}
pub struct SourceIter<'a>(Option<&'a XLinkError>);
impl<'a> Iterator for SourceIter<'a> {
type Item = &'a XLinkError;
fn next(&mut self) -> Option<Self::Item> {
let current = self.0.take()?;
self.0 = current.source.as_deref();
Some(current)
}
}
impl From<std::io::Error> for XLinkError {
#[inline]
fn from(error: std::io::Error) -> Self {
Self::new_internal(
ErrorCode(101),
ErrorCategory::System,
"IO操作失败".to_string(),
&format!("IO error: {}", error),
file!(),
)
.with_retry_suggestion(RetrySuggestion::Retryable {
max_attempts: 3,
base_delay_ms: 100,
})
}
}
impl From<serde_json::Error> for XLinkError {
#[inline]
fn from(error: serde_json::Error) -> Self {
Self::new_internal(
ErrorCode(103),
ErrorCategory::System,
"JSON序列化失败".to_string(),
&format!("JSON error: {}", error),
file!(),
)
}
}
impl From<DetailedError> for XLinkError {
#[inline]
fn from(detailed: DetailedError) -> Self {
Self {
code: detailed.code,
category: detailed.category,
message: detailed.message,
context: Box::new(detailed.context),
source: detailed
.root_cause
.map(|rc| Box::new(XLinkError::from(*rc))),
documentation_url: None,
}
}
}
pub type Result<T> = std::result::Result<T, XLinkError>;
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ErrorStatistics {
counts: std::collections::HashMap<u16, u64>,
category_counts: std::collections::HashMap<String, u64>,
last_error_time: Option<chrono::DateTime<chrono::Utc>>,
error_timeline: Vec<(chrono::DateTime<chrono::Utc>, u16)>,
}
impl ErrorStatistics {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, error: &XLinkError) {
let code = error.code.0;
*self.counts.entry(code).or_insert(0) += 1;
let category_name = error.category.name().to_string();
*self.category_counts.entry(category_name).or_insert(0) += 1;
self.last_error_time = Some(chrono::Utc::now());
self.error_timeline.push((chrono::Utc::now(), code));
}
#[inline]
pub fn total_count(&self) -> u64 {
self.counts.values().sum()
}
pub fn get_most_common(&self, n: usize) -> Vec<(u16, u64)> {
let mut errors: Vec<_> = self.counts.iter().collect();
errors.sort_by(|a, b| b.1.cmp(a.1));
errors
.into_iter()
.take(n)
.map(|(&code, &count)| (code, count))
.collect()
}
#[inline]
pub fn get_by_category(&self) -> &std::collections::HashMap<String, u64> {
&self.category_counts
}
#[inline]
pub fn get_recent(&self, n: usize) -> Vec<(chrono::DateTime<chrono::Utc>, u16)> {
self.error_timeline.iter().rev().take(n).cloned().collect()
}
#[inline]
pub fn get_count(&self, code: u16) -> u64 {
*self.counts.get(&code).unwrap_or(&0)
}
#[inline]
pub fn get_category_count(&self, category: ErrorCategory) -> u64 {
*self.category_counts.get(category.name()).unwrap_or(&0)
}
#[inline]
pub fn last_error(&self) -> Option<chrono::DateTime<chrono::Utc>> {
self.last_error_time
}
#[inline]
pub fn has_errors(&self) -> bool {
!self.counts.is_empty()
}
#[inline]
pub fn clear(&mut self) {
self.counts.clear();
self.category_counts.clear();
self.last_error_time = None;
self.error_timeline.clear();
}
}
#[inline]
pub fn format_error_for_log(error: &XLinkError) -> String {
error.to_log_string()
}
#[inline]
pub fn to_user_message(error: &XLinkError) -> String {
error.message.clone()
}
#[macro_export]
macro_rules! xlink_error {
($code:expr, $category:ident, $message:expr, $details:expr) => {
XLinkError::new_internal(
ErrorCode($code),
ErrorCategory::$category,
$message.to_string(),
$details,
file!(),
)
};
($code:expr, $category:ident, $message:expr, $details:expr, $($method:ident($value:expr)),+) => {
{
let mut error = XLinkError::new_internal(
ErrorCode($code),
ErrorCategory::$category,
$message.to_string(),
$details,
file!(),
);
$(
error = error.$method($value);
)+
error
}
};
}
#[macro_export]
macro_rules! with_context {
($error:expr, $key:expr, $device_id:expr) => {
$crate::core::error::XLinkError::storage_read_failed($key, format!("{}", $error), file!())
.with_device_id($device_id)
};
}