1use crate::client::RateLimitInfo;
5
6pub type Result<T, E = Error> = std::result::Result<T, E>;
8
9#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum Error {
13 #[error("configuration error: {0}")]
16 Config(String),
17
18 #[error("transport error")]
22 Transport(#[source] reqwest::Error),
23
24 #[error("HTTP {status}")]
26 Http {
27 status: u16,
29 body: String,
31 },
32
33 #[error("GraphQL error in {operation}: {}", errors.first().map(|e| e.message.as_str()).unwrap_or("unknown"))]
38 Api {
39 operation: &'static str,
41 errors: Vec<GraphQlError>,
43 },
44
45 #[error("rate limited")]
49 RateLimited {
50 retry_after: Option<std::time::Duration>,
52 info: Option<RateLimitInfo>,
54 },
55
56 #[error("could not decode {operation} response (schema drift?)")]
59 Decode {
60 operation: &'static str,
62 #[source]
64 source: serde_json::Error,
65 },
66
67 #[error("{operation} returned no data")]
69 MissingData {
70 operation: &'static str,
72 },
73
74 #[error("{operation} reported success=false")]
76 MutationFailed {
77 operation: &'static str,
79 },
80}
81
82impl Error {
83 pub fn is_rate_limited(&self) -> bool {
86 matches!(self, Error::RateLimited { .. })
87 || self.error_types().contains(&LinearErrorType::Ratelimited)
88 }
89
90 pub fn is_authentication(&self) -> bool {
92 self.error_types()
93 .contains(&LinearErrorType::AuthenticationError)
94 }
95
96 pub fn error_types(&self) -> Vec<LinearErrorType> {
99 match self {
100 Error::Api { errors, .. } => errors
101 .iter()
102 .filter_map(|e| e.extensions.as_ref())
103 .map(ErrorExtensions::typed)
104 .collect(),
105 _ => Vec::new(),
106 }
107 }
108}
109
110#[derive(Debug, Clone, serde::Deserialize)]
112#[non_exhaustive]
113pub struct GraphQlError {
114 pub message: String,
116 #[serde(default)]
118 pub path: Option<Vec<serde_json::Value>>,
119 #[serde(default)]
121 pub extensions: Option<ErrorExtensions>,
122}
123
124#[derive(Debug, Clone, serde::Deserialize)]
126#[non_exhaustive]
127pub struct ErrorExtensions {
128 #[serde(default, rename = "type")]
131 pub error_type: Option<String>,
132 #[serde(default)]
134 pub code: Option<String>,
135 #[serde(default, rename = "userError")]
137 pub user_error: Option<bool>,
138 #[serde(default, rename = "userPresentableMessage")]
140 pub user_presentable_message: Option<String>,
141}
142
143impl ErrorExtensions {
144 pub fn typed(&self) -> LinearErrorType {
148 self.error_type
149 .as_deref()
150 .or(self.code.as_deref())
151 .map(parse_error_type)
152 .unwrap_or(LinearErrorType::Unknown)
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
162#[non_exhaustive]
163pub enum LinearErrorType {
164 FeatureNotAccessible,
166 InvalidInput,
168 Ratelimited,
170 NetworkError,
172 AuthenticationError,
174 Forbidden,
176 BootstrapError,
178 Unknown,
180 InternalError,
182 Other,
184 UserError,
186 GraphqlError,
188 LockTimeout,
190 UsageLimitExceeded,
192 Unrecognized(String),
194}
195
196fn parse_error_type(raw: &str) -> LinearErrorType {
197 let normalized: String = raw
198 .to_ascii_lowercase()
199 .chars()
200 .filter(|c| !matches!(c, ' ' | '_' | '-'))
201 .collect();
202 match normalized.as_str() {
203 "featurenotaccessible" => LinearErrorType::FeatureNotAccessible,
204 "invalidinput" => LinearErrorType::InvalidInput,
205 "ratelimited" => LinearErrorType::Ratelimited,
206 "networkerror" => LinearErrorType::NetworkError,
207 "authenticationerror" => LinearErrorType::AuthenticationError,
208 "forbidden" => LinearErrorType::Forbidden,
209 "bootstraperror" => LinearErrorType::BootstrapError,
210 "unknown" => LinearErrorType::Unknown,
211 "internalerror" => LinearErrorType::InternalError,
212 "other" => LinearErrorType::Other,
213 "usererror" => LinearErrorType::UserError,
214 "graphqlerror" => LinearErrorType::GraphqlError,
215 "locktimeout" => LinearErrorType::LockTimeout,
216 "usagelimitexceeded" => LinearErrorType::UsageLimitExceeded,
217 _ => LinearErrorType::Unrecognized(raw.to_string()),
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn parses_wire_variants_insensitively() {
227 assert_eq!(
228 parse_error_type("authentication error"),
229 LinearErrorType::AuthenticationError
230 );
231 assert_eq!(
232 parse_error_type("AUTHENTICATION_ERROR"),
233 LinearErrorType::AuthenticationError
234 );
235 assert_eq!(
236 parse_error_type("Ratelimited"),
237 LinearErrorType::Ratelimited
238 );
239 assert_eq!(
240 parse_error_type("RATELIMITED"),
241 LinearErrorType::Ratelimited
242 );
243 assert_eq!(
244 parse_error_type("usage-limit-exceeded"),
245 LinearErrorType::UsageLimitExceeded
246 );
247 assert_eq!(
248 parse_error_type("brand new thing"),
249 LinearErrorType::Unrecognized("brand new thing".to_string())
250 );
251 }
252
253 #[test]
254 fn typed_falls_back_to_code_then_unknown() {
255 let ext: ErrorExtensions =
256 serde_json::from_value(serde_json::json!({ "code": "RATELIMITED" })).unwrap();
257 assert_eq!(ext.typed(), LinearErrorType::Ratelimited);
258
259 let ext: ErrorExtensions = serde_json::from_value(serde_json::json!({})).unwrap();
260 assert_eq!(ext.typed(), LinearErrorType::Unknown);
261 }
262}