use ironflow_types::ErrorEnvelope;
use serde::Deserialize;
use thiserror::Error;
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ApiErrorEnvelope {
pub error: ErrorEnvelope,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("API error {status}: [{code}] {message}")]
Api {
status: u16,
code: String,
message: String,
},
#[error("deserialization error: {0}")]
Deserialize(String),
#[error("SSE error: {0}")]
Sse(String),
#[error("retries exhausted after {attempts} attempts: {source}")]
Exhausted {
attempts: u32,
source: Box<Error>,
},
}
impl Error {
pub fn api(status: u16, code: impl Into<String>, message: impl Into<String>) -> Self {
Self::Api {
status,
code: code.into(),
message: message.into(),
}
}
pub fn is_api_error(&self) -> bool {
matches!(self, Self::Api { .. })
}
pub fn status(&self) -> Option<u16> {
match self {
Self::Api { status, .. } => Some(*status),
Self::Exhausted { source, .. } => source.status(),
_ => None,
}
}
pub fn code(&self) -> Option<&str> {
match self {
Self::Api { code, .. } => Some(code),
_ => None,
}
}
pub fn is_exhausted(&self) -> bool {
matches!(self, Self::Exhausted { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_error_accessors() {
let err = Error::api(404, "NOT_FOUND", "not found");
assert!(err.is_api_error());
assert!(!err.is_exhausted());
assert_eq!(err.status(), Some(404));
assert_eq!(err.code(), Some("NOT_FOUND"));
}
#[test]
fn exhausted_delegates_status() {
let inner = Error::api(503, "SERVICE_UNAVAILABLE", "unavailable");
let err = Error::Exhausted {
attempts: 3,
source: Box::new(inner),
};
assert!(err.is_exhausted());
assert!(!err.is_api_error());
assert_eq!(err.status(), Some(503));
assert_eq!(err.code(), None);
}
#[test]
fn exhausted_with_non_api_source() {
let inner = Error::Deserialize("bad json".to_string());
let err = Error::Exhausted {
attempts: 2,
source: Box::new(inner),
};
assert!(err.is_exhausted());
assert_eq!(err.status(), None);
}
#[test]
fn deserialize_error() {
let err = Error::Deserialize("bad json".to_string());
assert!(!err.is_api_error());
assert!(!err.is_exhausted());
assert_eq!(err.status(), None);
assert_eq!(err.code(), None);
}
#[test]
fn sse_error() {
let err = Error::Sse("connection reset".to_string());
assert!(!err.is_api_error());
assert!(!err.is_exhausted());
assert_eq!(err.status(), None);
}
#[test]
fn exhausted_display() {
let inner = Error::api(503, "UNAVAILABLE", "down");
let err = Error::Exhausted {
attempts: 4,
source: Box::new(inner),
};
let msg = format!("{err}");
assert!(msg.contains("4 attempts"));
}
}