1use std::env::VarError;
2use std::fmt::{Display, Formatter};
3use std::time::Duration;
4
5#[derive(Debug)]
6pub enum ApiError {
7 MissingApiKey,
8 ExpiredOAuthToken,
9 Auth(String),
10 InvalidApiKeyEnv(VarError),
11 Http(reqwest::Error),
12 Io(std::io::Error),
13 Json(serde_json::Error),
14 Api {
15 status: reqwest::StatusCode,
16 error_type: Option<String>,
17 message: Option<String>,
18 body: String,
19 retryable: bool,
20 },
21 RetriesExhausted {
22 attempts: u32,
23 last_error: Box<ApiError>,
24 },
25 InvalidSseFrame(&'static str),
26 BackoffOverflow {
27 attempt: u32,
28 base_delay: Duration,
29 },
30 Config(String),
31 ProviderError {
32 status: reqwest::StatusCode,
33 body: String,
34 },
35}
36
37impl ApiError {
38 #[must_use]
39 pub fn is_retryable(&self) -> bool {
40 match self {
41 Self::Http(error) => error.is_connect() || error.is_timeout() || error.is_request(),
42 Self::Api { retryable, .. } => *retryable,
43 Self::RetriesExhausted { last_error, .. } => last_error.is_retryable(),
44 Self::ProviderError { status, .. } => status.is_server_error(),
45 Self::MissingApiKey
46 | Self::ExpiredOAuthToken
47 | Self::Auth(_)
48 | Self::InvalidApiKeyEnv(_)
49 | Self::Io(_)
50 | Self::Json(_)
51 | Self::InvalidSseFrame(_)
52 | Self::Config(_)
53 | Self::BackoffOverflow { .. } => false,
54 }
55 }
56}
57
58impl Display for ApiError {
59 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
60 match self {
61 Self::Config(message) => write!(f, "configuration error: {message}"),
62 Self::ProviderError { status, body } => write!(f, "provider returned {status}: {body}"),
63 Self::MissingApiKey => {
64 write!(
65 f,
66 "TERNLANG_AUTH_TOKEN or TERNLANG_API_KEY is not set; export one before calling the Ternlang API"
67 )
68 }
69 Self::ExpiredOAuthToken => {
70 write!(
71 f,
72 "saved OAuth token is expired and no refresh token is available"
73 )
74 }
75 Self::Auth(message) => write!(f, "auth error: {message}"),
76 Self::InvalidApiKeyEnv(error) => {
77 write!(
78 f,
79 "failed to read TERNLANG_AUTH_TOKEN / TERNLANG_API_KEY: {error}"
80 )
81 }
82 Self::Http(error) => write!(f, "http error: {error}"),
83 Self::Io(error) => write!(f, "io error: {error}"),
84 Self::Json(error) => write!(f, "json error: {error}"),
85 Self::Api {
86 status,
87 error_type,
88 message,
89 body,
90 ..
91 } => match (error_type, message) {
92 (Some(error_type), Some(message)) => {
93 write!(
94 f,
95 "ternlang api returned {status} ({error_type}): {message}"
96 )
97 }
98 _ => write!(f, "ternlang api returned {status}: {body}"),
99 },
100 Self::RetriesExhausted {
101 attempts,
102 last_error,
103 } => write!(
104 f,
105 "ternlang api failed after {attempts} attempts: {last_error}"
106 ),
107 Self::InvalidSseFrame(message) => write!(f, "invalid sse frame: {message}"),
108 Self::BackoffOverflow {
109 attempt,
110 base_delay,
111 } => write!(
112 f,
113 "retry backoff overflowed on attempt {attempt} with base delay {base_delay:?}"
114 ),
115 }
116 }
117}
118
119impl std::error::Error for ApiError {}
120
121impl From<reqwest::Error> for ApiError {
122 fn from(value: reqwest::Error) -> Self {
123 Self::Http(value)
124 }
125}
126
127impl From<std::io::Error> for ApiError {
128 fn from(value: std::io::Error) -> Self {
129 Self::Io(value)
130 }
131}
132
133impl From<serde_json::Error> for ApiError {
134 fn from(value: serde_json::Error) -> Self {
135 Self::Json(value)
136 }
137}
138
139impl From<VarError> for ApiError {
140 fn from(value: VarError) -> Self {
141 Self::InvalidApiKeyEnv(value)
142 }
143}