|
4 | 4 | //! All client operations return a [`Result<T, OpClientError>`] which provides
|
5 | 5 | //! detailed error information for different failure scenarios.
|
6 | 6 | //!
|
7 |
| -//! ## Error Categories |
| 7 | +//! ## Error Structure |
8 | 8 | //!
|
9 |
| -//! - **HTTP Errors**: Network and HTTP protocol errors |
10 |
| -//! - **Parsing Errors**: Header, URL, and data parsing failures |
11 |
| -//! - **Cryptographic Errors**: Key and signature-related issues |
12 |
| -//! - **I/O Errors**: File system and network I/O problems |
| 9 | +//! - `description` - Human-readable error message |
| 10 | +//! - `validationErrors` - Optional list of validation error messages |
| 11 | +//! - `status` - HTTP status code (only for HTTP errors) |
| 12 | +//! - `code` - Error code (only for HTTP errors) |
| 13 | +//! - `details` - Additional error details as key-value pairs |
13 | 14 | //!
|
14 | 15 | //! ## Example Usage
|
15 | 16 | //!
|
|
19 | 20 | //! fn handle_client_error(result: Result<()>) {
|
20 | 21 | //! match result {
|
21 | 22 | //! Ok(()) => println!("Operation successful"),
|
22 |
| -//! Err(OpClientError::Http(msg)) => eprintln!("HTTP error: {}", msg), |
23 |
| -//! Err(OpClientError::Signature(msg)) => eprintln!("Signature error: {}", msg), |
24 |
| -//! Err(OpClientError::Other(msg)) => eprintln!("Other error: {}", msg), |
25 |
| -//! Err(e) => eprintln!("Unexpected error: {:?}", e), |
| 23 | +//! Err(e) => { |
| 24 | +//! eprintln!("Error: {}", e.description); |
| 25 | +//! if let Some(status) = e.status { |
| 26 | +//! eprintln!("Status: {}", status); |
| 27 | +//! } |
| 28 | +//! if let Some(code) = e.code { |
| 29 | +//! eprintln!("Code: {}", code); |
| 30 | +//! } |
| 31 | +//! if let Some(validation_errors) = e.validation_errors { |
| 32 | +//! for error in validation_errors { |
| 33 | +//! eprintln!("Validation error: {}", error); |
| 34 | +//! } |
| 35 | +//! } |
| 36 | +//! } |
26 | 37 | //! }
|
27 | 38 | //! }
|
28 | 39 | //! ```
|
29 | 40 |
|
| 41 | +use std::collections::HashMap; |
30 | 42 | use thiserror::Error;
|
31 | 43 |
|
32 | 44 | /// Error type for Open Payments client operations.
|
33 | 45 | ///
|
34 |
| -/// This enum provides detailed error information for different types of failures |
35 |
| -/// that can occur during client operations. Each variant includes context-specific |
36 |
| -/// error messages to help with debugging and error handling. |
| 46 | +/// ## Fields |
37 | 47 | ///
|
38 |
| -/// ## Error Variants |
39 |
| -/// |
40 |
| -/// - `Http` - Network and HTTP protocol errors |
41 |
| -/// - `HeaderParse` - HTTP header parsing failures |
42 |
| -/// - `Serde` - JSON serialization/deserialization errors |
43 |
| -/// - `Io` - File system and I/O errors |
44 |
| -/// - `Pem` - PEM format parsing errors |
45 |
| -/// - `Pkcs8` - PKCS8 key format errors |
46 |
| -/// - `Base64` - Base64 encoding/decoding errors |
47 |
| -/// - `Url` - URL parsing errors |
48 |
| -/// - `Signature` - Cryptographic signature errors |
49 |
| -/// - `Other` - Miscellaneous errors |
| 48 | +/// - `description` - Human-readable error message |
| 49 | +/// - `validation_errors` - Optional list of validation error messages |
| 50 | +/// - `status` - HTTP status code (only relevant for HTTP errors) |
| 51 | +/// - `code` - Error code (only relevant for HTTP errors) |
| 52 | +/// - `details` - Additional error details as key-value pairs |
50 | 53 | #[derive(Debug, Error)]
|
51 |
| -pub enum OpClientError { |
52 |
| - /// HTTP protocol or network-related errors. |
53 |
| - /// |
54 |
| - /// This includes connection failures, timeout errors, and HTTP status code errors. |
55 |
| - /// The error message provides details about the specific HTTP issue. |
56 |
| - #[error("HTTP error: {0}")] |
57 |
| - Http(String), |
58 |
| - |
59 |
| - /// HTTP header parsing errors. |
60 |
| - /// |
61 |
| - /// Occurs when the client cannot parse required HTTP headers such as |
62 |
| - /// `Content-Type`, `Authorization`, or custom headers. |
63 |
| - #[error("Header parse error: {0}")] |
64 |
| - HeaderParse(String), |
65 |
| - |
66 |
| - /// JSON serialization or deserialization errors. |
67 |
| - /// |
68 |
| - /// This error is automatically converted from `serde_json::Error` and occurs |
69 |
| - /// when the client cannot serialize request data or deserialize response data. |
70 |
| - #[error("Serde error: {0}")] |
71 |
| - Serde(#[from] serde_json::Error), |
72 |
| - |
73 |
| - /// File system and I/O errors. |
74 |
| - /// |
75 |
| - /// This error is automatically converted from `std::io::Error` and occurs |
76 |
| - /// when the client cannot read key files or perform other I/O operations. |
77 |
| - #[error("IO error: {0}")] |
78 |
| - Io(#[from] std::io::Error), |
79 |
| - |
80 |
| - /// PEM format parsing errors. |
81 |
| - /// |
82 |
| - /// Occurs when the client cannot parse PEM-encoded private keys or certificates. |
83 |
| - /// This includes malformed PEM files or unsupported PEM types. |
84 |
| - #[error("Invalid PEM: {0}")] |
85 |
| - Pem(String), |
86 |
| - |
87 |
| - /// PKCS8 key format errors. |
88 |
| - /// |
89 |
| - /// Occurs when the client cannot parse PKCS8-encoded private keys. |
90 |
| - /// This includes unsupported key algorithms or malformed key data. |
91 |
| - #[error("PKCS8 error: {0}")] |
92 |
| - Pkcs8(String), |
93 |
| - |
94 |
| - /// Base64 encoding or decoding errors. |
95 |
| - /// |
96 |
| - /// This error is automatically converted from `base64::DecodeError` and occurs |
97 |
| - /// when the client cannot decode Base64-encoded data such as signatures or keys. |
98 |
| - #[error("Base64 error: {0}")] |
99 |
| - Base64(#[from] base64::DecodeError), |
100 |
| - |
101 |
| - /// URL parsing errors. |
102 |
| - /// |
103 |
| - /// This error is automatically converted from `url::ParseError` and occurs |
104 |
| - /// when the client cannot parse URLs for wallet addresses or API endpoints. |
105 |
| - #[error("URL parse error: {0}")] |
106 |
| - Url(#[from] url::ParseError), |
107 |
| - |
108 |
| - /// Cryptographic signature errors. |
109 |
| - /// |
110 |
| - /// Occurs when there are issues with HTTP message signature creation or validation. |
111 |
| - /// This includes key loading failures, signature generation errors, and validation failures. |
112 |
| - #[error("Signature error: {0}")] |
113 |
| - Signature(String), |
114 |
| - |
115 |
| - /// Miscellaneous errors that don't fit into other categories. |
116 |
| - /// |
117 |
| - /// This variant is used for unexpected errors that don't fall into the standard error categories. |
118 |
| - #[error("Other error: {0}")] |
119 |
| - Other(String), |
| 54 | +pub struct OpClientError { |
| 55 | + /// Human-readable error description. |
| 56 | + pub description: String, |
| 57 | + |
| 58 | + /// Optional list of validation error messages. |
| 59 | + pub validation_errors: Option<Vec<String>>, |
| 60 | + |
| 61 | + /// HTTP status (only relevant for HTTP errors). |
| 62 | + pub status: Option<String>, |
| 63 | + |
| 64 | + /// Error code (only relevant for HTTP errors). |
| 65 | + pub code: Option<u16>, |
| 66 | + |
| 67 | + /// Additional error details as key-value pairs. |
| 68 | + pub details: Option<HashMap<String, serde_json::Value>>, |
| 69 | +} |
| 70 | + |
| 71 | +impl OpClientError { |
| 72 | + /// Creates a new HTTP error with status code and optional error code. |
| 73 | + pub fn http(description: impl Into<String>, status: Option<String>, code: Option<u16>) -> Self { |
| 74 | + Self { |
| 75 | + description: description.into(), |
| 76 | + validation_errors: None, |
| 77 | + status, |
| 78 | + code, |
| 79 | + details: None, |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + /// Creates a new validation error with a list of validation messages. |
| 84 | + pub fn validation(description: impl Into<String>, validation_errors: Vec<String>) -> Self { |
| 85 | + Self { |
| 86 | + description: description.into(), |
| 87 | + validation_errors: Some(validation_errors), |
| 88 | + status: None, |
| 89 | + code: None, |
| 90 | + details: None, |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + /// Creates a new general error without HTTP-specific fields. |
| 95 | + pub fn other(description: impl Into<String>) -> Self { |
| 96 | + Self { |
| 97 | + description: description.into(), |
| 98 | + validation_errors: None, |
| 99 | + status: None, |
| 100 | + code: None, |
| 101 | + details: None, |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + /// Adds additional details to the error. |
| 106 | + pub fn with_details(mut self, details: HashMap<String, serde_json::Value>) -> Self { |
| 107 | + self.details = Some(details); |
| 108 | + self |
| 109 | + } |
| 110 | + |
| 111 | + /// Adds a single detail key-value pair to the error. |
| 112 | + pub fn with_detail(mut self, key: impl Into<String>, value: serde_json::Value) -> Self { |
| 113 | + if self.details.is_none() { |
| 114 | + self.details = Some(HashMap::new()); |
| 115 | + } |
| 116 | + if let Some(ref mut details) = self.details { |
| 117 | + details.insert(key.into(), value); |
| 118 | + } |
| 119 | + self |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +impl std::fmt::Display for OpClientError { |
| 124 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 125 | + write!(f, "{}", self.description)?; |
| 126 | + |
| 127 | + if let Some(status) = &self.status { |
| 128 | + write!(f, " (Status: {status})")?; |
| 129 | + } |
| 130 | + |
| 131 | + if let Some(code) = &self.code { |
| 132 | + write!(f, " (Code: {code})")?; |
| 133 | + } |
| 134 | + |
| 135 | + if let Some(validation_errors) = &self.validation_errors { |
| 136 | + write!(f, " [Validation errors: {}]", validation_errors.join(", "))?; |
| 137 | + } |
| 138 | + |
| 139 | + Ok(()) |
| 140 | + } |
120 | 141 | }
|
121 | 142 |
|
122 | 143 | impl From<reqwest::Error> for OpClientError {
|
123 |
| - /// Converts reqwest HTTP errors to `OpClientError::Http`. |
124 |
| - /// |
125 |
| - /// This implementation allows the client to automatically convert reqwest errors |
126 |
| - /// into the unified error type, providing consistent error handling across the library. |
127 | 144 | fn from(err: reqwest::Error) -> Self {
|
128 |
| - OpClientError::Http(format!("{err}")) |
| 145 | + let status = err |
| 146 | + .status() |
| 147 | + .map(|s| s.canonical_reason().unwrap_or("Unknown").to_string()); |
| 148 | + let code = err.status().map(|s| s.as_u16()); |
| 149 | + let description = format!("HTTP error: {err}"); |
| 150 | + |
| 151 | + Self { |
| 152 | + description, |
| 153 | + validation_errors: None, |
| 154 | + status, |
| 155 | + code, |
| 156 | + details: None, |
| 157 | + } |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +impl From<serde_json::Error> for OpClientError { |
| 162 | + fn from(err: serde_json::Error) -> Self { |
| 163 | + Self::other(format!("JSON serialization/deserialization error: {err}")) |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +impl From<std::io::Error> for OpClientError { |
| 168 | + fn from(err: std::io::Error) -> Self { |
| 169 | + Self::other(format!("I/O error: {err}")) |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +impl From<base64::DecodeError> for OpClientError { |
| 174 | + fn from(err: base64::DecodeError) -> Self { |
| 175 | + Self::other(format!("Base64 decoding error: {err}")) |
| 176 | + } |
| 177 | +} |
| 178 | + |
| 179 | +impl From<url::ParseError> for OpClientError { |
| 180 | + fn from(err: url::ParseError) -> Self { |
| 181 | + Self::other(format!("URL parsing error: {err}")) |
| 182 | + } |
| 183 | +} |
| 184 | + |
| 185 | +impl OpClientError { |
| 186 | + pub fn header_parse(description: impl Into<String>) -> Self { |
| 187 | + Self::other(format!("Header parse error: {}", description.into())) |
| 188 | + } |
| 189 | + |
| 190 | + pub fn pem(description: impl Into<String>) -> Self { |
| 191 | + Self::other(format!("Invalid PEM: {}", description.into())) |
| 192 | + } |
| 193 | + |
| 194 | + pub fn pkcs8(description: impl Into<String>) -> Self { |
| 195 | + Self::other(format!("PKCS8 error: {}", description.into())) |
| 196 | + } |
| 197 | + |
| 198 | + pub fn signature(description: impl Into<String>) -> Self { |
| 199 | + Self::other(format!("Signature error: {}", description.into())) |
| 200 | + } |
| 201 | +} |
| 202 | + |
| 203 | +impl From<url::ParseError> for Box<OpClientError> { |
| 204 | + fn from(err: url::ParseError) -> Self { |
| 205 | + Box::new(OpClientError::from(err)) |
| 206 | + } |
| 207 | +} |
| 208 | + |
| 209 | +impl From<reqwest::Error> for Box<OpClientError> { |
| 210 | + fn from(err: reqwest::Error) -> Self { |
| 211 | + Box::new(OpClientError::from(err)) |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +impl From<serde_json::Error> for Box<OpClientError> { |
| 216 | + fn from(err: serde_json::Error) -> Self { |
| 217 | + Box::new(OpClientError::from(err)) |
| 218 | + } |
| 219 | +} |
| 220 | + |
| 221 | +impl From<std::io::Error> for Box<OpClientError> { |
| 222 | + fn from(err: std::io::Error) -> Self { |
| 223 | + Box::new(OpClientError::from(err)) |
| 224 | + } |
| 225 | +} |
| 226 | + |
| 227 | +impl From<base64::DecodeError> for Box<OpClientError> { |
| 228 | + fn from(err: base64::DecodeError) -> Self { |
| 229 | + Box::new(OpClientError::from(err)) |
129 | 230 | }
|
130 | 231 | }
|
131 | 232 |
|
132 | 233 | /// Result type for Open Payments client operations.
|
133 | 234 | ///
|
134 |
| -/// This is a type alias for `Result<T, OpClientError>` that provides a convenient |
| 235 | +/// This is a type alias for `Result<T, Box<OpClientError>>` that provides a convenient |
135 | 236 | /// way to handle client operation results with detailed error information.
|
136 |
| -pub type Result<T> = std::result::Result<T, OpClientError>; |
| 237 | +pub type Result<T> = std::result::Result<T, Box<OpClientError>>; |
0 commit comments