use std::fmt;
use std::time::Duration;
use serde::Deserialize;
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("could not reach Linear: {0}")]
Transport(#[source] reqwest::Error),
#[error("Linear rejected the credentials. Run `linear-tui auth login` to sign in again.")]
Unauthorized,
#[error("Linear is rate limiting requests{}", retry_hint(*.retry_after))]
RateLimited { retry_after: Option<Duration> },
#[error("API error ({status}): {body}")]
Http {
status: reqwest::StatusCode,
body: String,
},
#[error("{}", join(.0))]
GraphQL(Vec<GraphQLError>),
#[error("unexpected response from Linear: {0}")]
Decode(String),
#[error("{0}")]
Rejected(&'static str),
#[error("could not refresh the session: {0}")]
Refresh(String),
}
#[derive(Debug, Clone, Deserialize)]
pub struct GraphQLError {
pub message: String,
#[serde(default)]
pub extensions: Option<Extensions>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Extensions {
#[serde(default)]
pub code: Option<String>,
}
impl GraphQLError {
pub fn code(&self) -> Option<&str> {
self.extensions.as_ref()?.code.as_deref()
}
}
impl fmt::Display for GraphQLError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl ApiError {
pub fn from_graphql(errors: Vec<GraphQLError>, retry_after: Option<Duration>) -> Self {
if errors
.iter()
.any(|e| e.code() == Some("AUTHENTICATION_ERROR"))
{
Self::Unauthorized
} else if errors.iter().any(|e| e.code() == Some("RATELIMITED")) {
Self::RateLimited { retry_after }
} else {
Self::GraphQL(errors)
}
}
}
fn join(errors: &[GraphQLError]) -> String {
let messages: Vec<_> = errors.iter().map(|e| e.message.as_str()).collect();
format!("GraphQL errors: {}", messages.join(", "))
}
fn retry_hint(retry_after: Option<Duration>) -> String {
match retry_after {
Some(wait) => format!("; try again in {}s", wait.as_secs().max(1)),
None => "; try again shortly".to_string(),
}
}