skill_web/api/
error.rs

1//! API error types
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6/// API error types
7#[derive(Error, Debug, Clone, PartialEq)]
8pub enum ApiError {
9    /// Network error (failed to send request)
10    #[error("Network error: {0}")]
11    Network(String),
12
13    /// HTTP error with status code
14    #[error("HTTP {status}: {message}")]
15    Http { status: u16, message: String },
16
17    /// Resource not found (404)
18    #[error("Not found: {0}")]
19    NotFound(String),
20
21    /// Bad request (400)
22    #[error("Bad request: {0}")]
23    BadRequest(String),
24
25    /// Unauthorized (401)
26    #[error("Unauthorized")]
27    Unauthorized,
28
29    /// Forbidden (403)
30    #[error("Forbidden")]
31    Forbidden,
32
33    /// Server error (5xx)
34    #[error("Server error: {0}")]
35    Server(String),
36
37    /// Timeout
38    #[error("Request timed out")]
39    Timeout,
40
41    /// Serialization error
42    #[error("Serialization error: {0}")]
43    Serialization(String),
44
45    /// Deserialization error
46    #[error("Deserialization error: {0}")]
47    Deserialization(String),
48
49    /// Validation error
50    #[error("Validation error: {0}")]
51    Validation(String),
52}
53
54impl ApiError {
55    /// Create an error from HTTP status code and message
56    pub fn from_status(status: u16, message: String) -> Self {
57        match status {
58            400 => Self::BadRequest(message),
59            401 => Self::Unauthorized,
60            403 => Self::Forbidden,
61            404 => Self::NotFound(message),
62            408 => Self::Timeout,
63            500..=599 => Self::Server(message),
64            _ => Self::Http { status, message },
65        }
66    }
67
68    /// Check if the error is recoverable (can retry)
69    pub fn is_recoverable(&self) -> bool {
70        matches!(
71            self,
72            Self::Network(_) | Self::Timeout | Self::Server(_)
73        )
74    }
75
76    /// Check if this is a client error (4xx)
77    pub fn is_client_error(&self) -> bool {
78        matches!(
79            self,
80            Self::NotFound(_)
81                | Self::BadRequest(_)
82                | Self::Unauthorized
83                | Self::Forbidden
84                | Self::Validation(_)
85        )
86    }
87
88    /// Get the HTTP status code if applicable
89    pub fn status_code(&self) -> Option<u16> {
90        match self {
91            Self::Http { status, .. } => Some(*status),
92            Self::NotFound(_) => Some(404),
93            Self::BadRequest(_) => Some(400),
94            Self::Unauthorized => Some(401),
95            Self::Forbidden => Some(403),
96            Self::Server(_) => Some(500),
97            Self::Timeout => Some(408),
98            _ => None,
99        }
100    }
101}
102
103/// API error response from server
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ApiErrorResponse {
106    /// Error code
107    pub code: String,
108    /// Human-readable message
109    pub message: String,
110    /// Additional details
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub details: Option<serde_json::Value>,
113}
114
115impl From<ApiErrorResponse> for ApiError {
116    fn from(resp: ApiErrorResponse) -> Self {
117        match resp.code.as_str() {
118            "NOT_FOUND" => Self::NotFound(resp.message),
119            "BAD_REQUEST" => Self::BadRequest(resp.message),
120            "VALIDATION_ERROR" => Self::Validation(resp.message),
121            "UNAUTHORIZED" => Self::Unauthorized,
122            "FORBIDDEN" => Self::Forbidden,
123            "INTERNAL_ERROR" => Self::Server(resp.message),
124            _ => Self::Http {
125                status: 0,
126                message: resp.message,
127            },
128        }
129    }
130}
131
132/// Result type alias for API operations
133pub type ApiResult<T> = Result<T, ApiError>;