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}
31
32impl ApiError {
33 #[must_use]
34 pub fn is_retryable(&self) -> bool {
35 match self {
36 Self::Http(error) => error.is_connect() || error.is_timeout() || error.is_request(),
37 Self::Api { retryable, .. } => *retryable,
38 Self::RetriesExhausted { last_error, .. } => last_error.is_retryable(),
39 Self::MissingApiKey
40 | Self::ExpiredOAuthToken
41 | Self::Auth(_)
42 | Self::InvalidApiKeyEnv(_)
43 | Self::Io(_)
44 | Self::Json(_)
45 | Self::InvalidSseFrame(_)
46 | Self::BackoffOverflow { .. } => false,
47 }
48 }
49}
50
51impl Display for ApiError {
52 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::MissingApiKey => {
55 write!(
56 f,
57 "TERNLANG_AUTH_TOKEN or TERNLANG_API_KEY is not set; export one before calling the Ternlang API"
58 )
59 }
60 Self::ExpiredOAuthToken => {
61 write!(
62 f,
63 "saved OAuth token is expired and no refresh token is available"
64 )
65 }
66 Self::Auth(message) => write!(f, "auth error: {message}"),
67 Self::InvalidApiKeyEnv(error) => {
68 write!(
69 f,
70 "failed to read TERNLANG_AUTH_TOKEN / TERNLANG_API_KEY: {error}"
71 )
72 }
73 Self::Http(error) => write!(f, "http error: {error}"),
74 Self::Io(error) => write!(f, "io error: {error}"),
75 Self::Json(error) => write!(f, "json error: {error}"),
76 Self::Api {
77 status,
78 error_type,
79 message,
80 body,
81 ..
82 } => match (error_type, message) {
83 (Some(error_type), Some(message)) => {
84 write!(
85 f,
86 "ternlang api returned {status} ({error_type}): {message}"
87 )
88 }
89 _ => write!(f, "ternlang api returned {status}: {body}"),
90 },
91 Self::RetriesExhausted {
92 attempts,
93 last_error,
94 } => write!(
95 f,
96 "ternlang api failed after {attempts} attempts: {last_error}"
97 ),
98 Self::InvalidSseFrame(message) => write!(f, "invalid sse frame: {message}"),
99 Self::BackoffOverflow {
100 attempt,
101 base_delay,
102 } => write!(
103 f,
104 "retry backoff overflowed on attempt {attempt} with base delay {base_delay:?}"
105 ),
106 }
107 }
108}
109
110impl std::error::Error for ApiError {}
111
112impl From<reqwest::Error> for ApiError {
113 fn from(value: reqwest::Error) -> Self {
114 Self::Http(value)
115 }
116}
117
118impl From<std::io::Error> for ApiError {
119 fn from(value: std::io::Error) -> Self {
120 Self::Io(value)
121 }
122}
123
124impl From<serde_json::Error> for ApiError {
125 fn from(value: serde_json::Error) -> Self {
126 Self::Json(value)
127 }
128}
129
130impl From<VarError> for ApiError {
131 fn from(value: VarError) -> Self {
132 Self::InvalidApiKeyEnv(value)
133 }
134}