1use ironflow_types::ErrorEnvelope;
4use serde::Deserialize;
5use thiserror::Error;
6
7#[derive(Debug, Clone, Deserialize)]
9pub(crate) struct ApiErrorEnvelope {
10 pub error: ErrorEnvelope,
11}
12
13#[derive(Debug, Error)]
27pub enum Error {
28 #[error("HTTP error: {0}")]
30 Http(#[from] reqwest::Error),
31
32 #[error("API error {status}: [{code}] {message}")]
34 Api {
35 status: u16,
37 code: String,
39 message: String,
41 },
42
43 #[error("deserialization error: {0}")]
45 Deserialize(String),
46
47 #[error("SSE error: {0}")]
49 Sse(String),
50
51 #[error("retries exhausted after {attempts} attempts: {source}")]
53 Exhausted {
54 attempts: u32,
56 source: Box<Error>,
58 },
59}
60
61impl Error {
62 pub fn api(status: u16, code: impl Into<String>, message: impl Into<String>) -> Self {
64 Self::Api {
65 status,
66 code: code.into(),
67 message: message.into(),
68 }
69 }
70
71 pub fn is_api_error(&self) -> bool {
73 matches!(self, Self::Api { .. })
74 }
75
76 pub fn status(&self) -> Option<u16> {
79 match self {
80 Self::Api { status, .. } => Some(*status),
81 Self::Exhausted { source, .. } => source.status(),
82 _ => None,
83 }
84 }
85
86 pub fn code(&self) -> Option<&str> {
88 match self {
89 Self::Api { code, .. } => Some(code),
90 _ => None,
91 }
92 }
93
94 pub fn is_exhausted(&self) -> bool {
96 matches!(self, Self::Exhausted { .. })
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn api_error_accessors() {
106 let err = Error::api(404, "NOT_FOUND", "not found");
107 assert!(err.is_api_error());
108 assert!(!err.is_exhausted());
109 assert_eq!(err.status(), Some(404));
110 assert_eq!(err.code(), Some("NOT_FOUND"));
111 }
112
113 #[test]
114 fn exhausted_delegates_status() {
115 let inner = Error::api(503, "SERVICE_UNAVAILABLE", "unavailable");
116 let err = Error::Exhausted {
117 attempts: 3,
118 source: Box::new(inner),
119 };
120 assert!(err.is_exhausted());
121 assert!(!err.is_api_error());
122 assert_eq!(err.status(), Some(503));
123 assert_eq!(err.code(), None);
124 }
125
126 #[test]
127 fn exhausted_with_non_api_source() {
128 let inner = Error::Deserialize("bad json".to_string());
129 let err = Error::Exhausted {
130 attempts: 2,
131 source: Box::new(inner),
132 };
133 assert!(err.is_exhausted());
134 assert_eq!(err.status(), None);
135 }
136
137 #[test]
138 fn deserialize_error() {
139 let err = Error::Deserialize("bad json".to_string());
140 assert!(!err.is_api_error());
141 assert!(!err.is_exhausted());
142 assert_eq!(err.status(), None);
143 assert_eq!(err.code(), None);
144 }
145
146 #[test]
147 fn sse_error() {
148 let err = Error::Sse("connection reset".to_string());
149 assert!(!err.is_api_error());
150 assert!(!err.is_exhausted());
151 assert_eq!(err.status(), None);
152 }
153
154 #[test]
155 fn exhausted_display() {
156 let inner = Error::api(503, "UNAVAILABLE", "down");
157 let err = Error::Exhausted {
158 attempts: 4,
159 source: Box::new(inner),
160 };
161 let msg = format!("{err}");
162 assert!(msg.contains("4 attempts"));
163 }
164}