use crate::client::RateLimitInfo;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("configuration error: {0}")]
Config(String),
#[error("transport error")]
Transport(#[source] reqwest::Error),
#[error("HTTP {status}")]
Http {
status: u16,
body: String,
},
#[error("GraphQL error in {operation}: {}", errors.first().map(|e| e.message.as_str()).unwrap_or("unknown"))]
Api {
operation: &'static str,
errors: Vec<GraphQlError>,
},
#[error("rate limited")]
RateLimited {
retry_after: Option<std::time::Duration>,
info: Option<RateLimitInfo>,
},
#[error("could not decode {operation} response (schema drift?)")]
Decode {
operation: &'static str,
#[source]
source: serde_json::Error,
},
#[error("{operation} returned no data")]
MissingData {
operation: &'static str,
},
#[error("{operation} reported success=false")]
MutationFailed {
operation: &'static str,
},
}
impl Error {
pub fn is_rate_limited(&self) -> bool {
matches!(self, Error::RateLimited { .. })
|| self.error_types().contains(&LinearErrorType::Ratelimited)
}
pub fn is_authentication(&self) -> bool {
self.error_types()
.contains(&LinearErrorType::AuthenticationError)
}
pub fn error_types(&self) -> Vec<LinearErrorType> {
match self {
Error::Api { errors, .. } => errors
.iter()
.filter_map(|e| e.extensions.as_ref())
.map(ErrorExtensions::typed)
.collect(),
_ => Vec::new(),
}
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[non_exhaustive]
pub struct GraphQlError {
pub message: String,
#[serde(default)]
pub path: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub extensions: Option<ErrorExtensions>,
}
#[derive(Debug, Clone, serde::Deserialize)]
#[non_exhaustive]
pub struct ErrorExtensions {
#[serde(default, rename = "type")]
pub error_type: Option<String>,
#[serde(default)]
pub code: Option<String>,
#[serde(default, rename = "userError")]
pub user_error: Option<bool>,
#[serde(default, rename = "userPresentableMessage")]
pub user_presentable_message: Option<String>,
}
impl ErrorExtensions {
pub fn typed(&self) -> LinearErrorType {
self.error_type
.as_deref()
.or(self.code.as_deref())
.map(parse_error_type)
.unwrap_or(LinearErrorType::Unknown)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LinearErrorType {
FeatureNotAccessible,
InvalidInput,
Ratelimited,
NetworkError,
AuthenticationError,
Forbidden,
BootstrapError,
Unknown,
InternalError,
Other,
UserError,
GraphqlError,
LockTimeout,
UsageLimitExceeded,
Unrecognized(String),
}
fn parse_error_type(raw: &str) -> LinearErrorType {
let normalized: String = raw
.to_ascii_lowercase()
.chars()
.filter(|c| !matches!(c, ' ' | '_' | '-'))
.collect();
match normalized.as_str() {
"featurenotaccessible" => LinearErrorType::FeatureNotAccessible,
"invalidinput" => LinearErrorType::InvalidInput,
"ratelimited" => LinearErrorType::Ratelimited,
"networkerror" => LinearErrorType::NetworkError,
"authenticationerror" => LinearErrorType::AuthenticationError,
"forbidden" => LinearErrorType::Forbidden,
"bootstraperror" => LinearErrorType::BootstrapError,
"unknown" => LinearErrorType::Unknown,
"internalerror" => LinearErrorType::InternalError,
"other" => LinearErrorType::Other,
"usererror" => LinearErrorType::UserError,
"graphqlerror" => LinearErrorType::GraphqlError,
"locktimeout" => LinearErrorType::LockTimeout,
"usagelimitexceeded" => LinearErrorType::UsageLimitExceeded,
_ => LinearErrorType::Unrecognized(raw.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_wire_variants_insensitively() {
assert_eq!(
parse_error_type("authentication error"),
LinearErrorType::AuthenticationError
);
assert_eq!(
parse_error_type("AUTHENTICATION_ERROR"),
LinearErrorType::AuthenticationError
);
assert_eq!(
parse_error_type("Ratelimited"),
LinearErrorType::Ratelimited
);
assert_eq!(
parse_error_type("RATELIMITED"),
LinearErrorType::Ratelimited
);
assert_eq!(
parse_error_type("usage-limit-exceeded"),
LinearErrorType::UsageLimitExceeded
);
assert_eq!(
parse_error_type("brand new thing"),
LinearErrorType::Unrecognized("brand new thing".to_string())
);
}
#[test]
fn typed_falls_back_to_code_then_unknown() {
let ext: ErrorExtensions =
serde_json::from_value(serde_json::json!({ "code": "RATELIMITED" })).unwrap();
assert_eq!(ext.typed(), LinearErrorType::Ratelimited);
let ext: ErrorExtensions = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(ext.typed(), LinearErrorType::Unknown);
}
}