Skip to main content

open_payments/client/
error.rs

1//! # Open Payments Client Error Types
2//!
3//! This module defines the error types used throughout the Open Payments client.
4//! All client operations return a [`Result<T, OpClientError>`] which provides
5//! detailed error information for different failure scenarios.
6//!
7//! ## Error Structure
8//!
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
14//!
15//! ## Example Usage
16//!
17//! ```rust
18//! use open_payments::client::{OpClientError, Result};
19//!
20//! fn handle_client_error(result: Result<()>) {
21//!     match result {
22//!         Ok(()) => println!("Operation successful"),
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//!         }
37//!     }
38//! }
39//! ```
40
41use std::collections::HashMap;
42use thiserror::Error;
43
44/// Error type for Open Payments client operations.
45///
46/// ## Fields
47///
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
53#[derive(Debug, Error)]
54pub 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
71impl 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
123impl 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    }
141}
142
143impl From<reqwest::Error> for OpClientError {
144    fn from(err: reqwest::Error) -> Self {
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
161impl 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
167impl 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
173impl From<base64::DecodeError> for OpClientError {
174    fn from(err: base64::DecodeError) -> Self {
175        Self::other(format!("Base64 decoding error: {err}"))
176    }
177}
178
179impl From<url::ParseError> for OpClientError {
180    fn from(err: url::ParseError) -> Self {
181        Self::other(format!("URL parsing error: {err}"))
182    }
183}
184
185impl 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
203impl From<url::ParseError> for Box<OpClientError> {
204    fn from(err: url::ParseError) -> Self {
205        Box::new(OpClientError::from(err))
206    }
207}
208
209impl From<reqwest::Error> for Box<OpClientError> {
210    fn from(err: reqwest::Error) -> Self {
211        Box::new(OpClientError::from(err))
212    }
213}
214
215impl From<serde_json::Error> for Box<OpClientError> {
216    fn from(err: serde_json::Error) -> Self {
217        Box::new(OpClientError::from(err))
218    }
219}
220
221impl From<std::io::Error> for Box<OpClientError> {
222    fn from(err: std::io::Error) -> Self {
223        Box::new(OpClientError::from(err))
224    }
225}
226
227impl From<base64::DecodeError> for Box<OpClientError> {
228    fn from(err: base64::DecodeError) -> Self {
229        Box::new(OpClientError::from(err))
230    }
231}
232
233/// Result type for Open Payments client operations.
234///
235/// This is a type alias for `Result<T, Box<OpClientError>>` that provides a convenient
236/// way to handle client operation results with detailed error information.
237pub type Result<T> = std::result::Result<T, Box<OpClientError>>;