use std::collections::HashMap;
use std::time::Duration;
use serde::Deserialize;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Api(Box<ApiError>),
#[error("transport error: {0}")]
Transport(#[source] reqwest::Error),
#[error("serialization error: {0}")]
Serde(#[from] serde_json::Error),
#[error("configuration error: {0}")]
Config(String),
#[error("token error: {0}")]
Token(String),
#[error(transparent)]
TokenValidation(#[from] TokenError),
#[error("HTTP response body exceeded {limit} bytes (received at least {actual})")]
ResponseTooLarge {
limit: usize,
actual: usize,
},
#[error("illegal state: {0}")]
IllegalState(String),
#[error(transparent)]
Webhook(#[from] WebhookError),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum TokenError {
#[error("JWT exceeds {limit} bytes (received {actual})")]
TooLarge {
limit: usize,
actual: usize,
},
#[error("malformed JWT: {0}")]
Malformed(&'static str),
#[error("invalid JWT {segment} encoding")]
InvalidEncoding {
segment: &'static str,
},
#[error("invalid JWT protected header: {0}")]
InvalidHeader(String),
#[error("unsupported JWT algorithm {actual:?}; expected \"HS256\"")]
UnsupportedAlgorithm {
actual: String,
},
#[error("incompatible JWT type {actual:?}")]
IncompatibleType {
actual: String,
},
#[error("JWT signature verification failed")]
SignatureMismatch,
#[error("invalid JWT claims: {0}")]
InvalidClaims(String),
#[error("custom claims cannot replace reserved claim {claim:?}")]
ReservedClaim {
claim: String,
},
#[error("JWT timestamp arithmetic overflow")]
TimestampOverflow,
#[error("JWT expired at {exp} (current time {now})")]
Expired {
exp: i64,
now: i64,
},
#[error("JWT is not valid before {nbf} (current time {now})")]
NotYetValid {
nbf: i64,
now: i64,
},
#[error("JWT was issued in the future at {iat} (current time {now})")]
IssuedInFuture {
iat: i64,
now: i64,
},
#[error("JWT user_id {actual:?} does not match expected user {expected:?}")]
UserMismatch {
expected: String,
actual: String,
},
#[error("Stream rejected the JWT as expired")]
ExpiredByServer,
}
impl From<ApiError> for Error {
fn from(e: ApiError) -> Self {
Error::Api(Box::new(e))
}
}
impl Error {
pub fn as_api_error(&self) -> Option<&ApiError> {
match self {
Error::Api(e) => Some(e),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
match self {
Error::Transport(_) => true,
Error::Api(e) => !e.unrecoverable && e.status == 429,
_ => false,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, thiserror::Error)]
#[error("stream api error (code {code}, http {status}): {message}")]
#[non_exhaustive]
pub struct ApiError {
#[serde(default)]
pub code: i32,
#[serde(default)]
pub message: String,
#[serde(rename = "StatusCode", default)]
pub status: u16,
#[serde(default)]
pub more_info: String,
#[serde(default)]
pub duration: String,
#[serde(default)]
pub unrecoverable: bool,
#[serde(default)]
pub exception_fields: HashMap<String, String>,
#[serde(skip)]
pub retry_after: Option<Duration>,
}
impl ApiError {
pub fn is_rate_limited(&self) -> bool {
self.status == 429
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WebhookError {
#[error("webhook signature mismatch")]
SignatureMismatch,
#[error("invalid webhook payload: {0}")]
InvalidPayload(String),
#[error("webhook payload missing 'type' field")]
MissingType,
#[error("webhook payload exceeded {limit} bytes after decompression")]
PayloadTooLarge {
limit: usize,
},
}