use std::fmt;
use std::time::Duration;
use serde_json::Value;
use crate::response::{RunStatus, SmartBrowseRun};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Api(#[from] ApiError),
#[error("transport error: {0}")]
Transport(#[source] reqwest::Error),
#[error("request timed out after {0:?}")]
Timeout(Duration),
#[error("smartbrowse run {} ended in state {}", .run.id, .run.run_status)]
RunFailed {
run: SmartBrowseRun,
},
#[error("timed out waiting for smartbrowse run {} (last state: {})", .last.id, .last.run_status)]
WaitTimeout {
last: SmartBrowseRun,
},
#[error("configuration error: {0}")]
Config(String),
#[error("decode error: {0}")]
Decode(String),
}
impl Error {
pub fn as_api(&self) -> Option<&ApiError> {
match self {
Error::Api(e) => Some(e),
_ => None,
}
}
pub fn is_code(&self, code: &ErrorCode) -> bool {
self.as_api().map(|e| e.code() == code).unwrap_or(false)
}
pub fn is_unauthorized(&self) -> bool {
self.as_api()
.map(ApiError::is_unauthorized)
.unwrap_or(false)
}
pub fn is_insufficient_credits(&self) -> bool {
self.as_api()
.map(ApiError::is_insufficient_credits)
.unwrap_or(false)
}
pub fn is_rate_limited(&self) -> bool {
self.as_api()
.map(ApiError::is_rate_limited)
.unwrap_or(false)
}
pub fn is_not_found(&self) -> bool {
self.as_api().map(ApiError::is_not_found).unwrap_or(false)
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("api error [{status}] {code}: {message}")]
pub struct ApiError {
status: u16,
code: ErrorCode,
message: String,
details: Option<Value>,
request_id: Option<String>,
retry_after: Option<Duration>,
}
impl ApiError {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
status: u16,
code: ErrorCode,
message: String,
details: Option<Value>,
request_id: Option<String>,
retry_after: Option<Duration>,
) -> Self {
Self {
status,
code,
message,
details,
request_id,
retry_after,
}
}
pub fn status(&self) -> u16 {
self.status
}
pub fn code(&self) -> &ErrorCode {
&self.code
}
pub fn kind(&self) -> &ErrorCode {
&self.code
}
pub fn message(&self) -> &str {
&self.message
}
pub fn details(&self) -> Option<&Value> {
self.details.as_ref()
}
pub fn request_id(&self) -> Option<&str> {
self.request_id.as_deref()
}
pub fn retry_after(&self) -> Option<Duration> {
self.retry_after
}
pub fn balance(&self) -> Option<i64> {
self.detail_i64("balance")
}
pub fn required(&self) -> Option<i64> {
self.detail_i64("required")
}
pub fn reason(&self) -> Option<&str> {
self.detail_str("reason")
}
pub fn deletion_scheduled_for(&self) -> Option<&str> {
self.detail_str("deletion_scheduled_for")
}
pub fn is_unauthorized(&self) -> bool {
matches!(self.code, ErrorCode::Unauthorized)
}
pub fn is_insufficient_credits(&self) -> bool {
matches!(self.code, ErrorCode::InsufficientCredits)
}
pub fn is_rate_limited(&self) -> bool {
matches!(self.code, ErrorCode::RateLimited)
}
pub fn is_not_found(&self) -> bool {
matches!(self.code, ErrorCode::NotFound)
}
pub fn is_validation_failed(&self) -> bool {
matches!(self.code, ErrorCode::ValidationFailed)
}
fn detail_i64(&self, key: &str) -> Option<i64> {
self.details.as_ref()?.get(key)?.as_i64()
}
fn detail_str(&self, key: &str) -> Option<&str> {
self.details.as_ref()?.get(key)?.as_str()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorCode {
InvalidRequest,
Unauthorized,
InsufficientCredits,
EmailVerificationRequired,
Forbidden,
NotFound,
Conflict,
AccountDeletionPending,
ValidationFailed,
RateLimited,
InternalError,
ServiceUnavailable,
Unknown(String),
}
impl ErrorCode {
pub fn as_str(&self) -> &str {
match self {
ErrorCode::InvalidRequest => "invalid_request",
ErrorCode::Unauthorized => "unauthorized",
ErrorCode::InsufficientCredits => "insufficient_credits",
ErrorCode::EmailVerificationRequired => "email_verification_required",
ErrorCode::Forbidden => "forbidden",
ErrorCode::NotFound => "not_found",
ErrorCode::Conflict => "conflict",
ErrorCode::AccountDeletionPending => "account_deletion_pending",
ErrorCode::ValidationFailed => "validation_failed",
ErrorCode::RateLimited => "rate_limited",
ErrorCode::InternalError => "internal_error",
ErrorCode::ServiceUnavailable => "service_unavailable",
ErrorCode::Unknown(s) => s.as_str(),
}
}
pub(crate) fn parse(s: &str) -> Self {
match s {
"invalid_request" => ErrorCode::InvalidRequest,
"unauthorized" => ErrorCode::Unauthorized,
"insufficient_credits" => ErrorCode::InsufficientCredits,
"email_verification_required" => ErrorCode::EmailVerificationRequired,
"forbidden" => ErrorCode::Forbidden,
"not_found" => ErrorCode::NotFound,
"conflict" => ErrorCode::Conflict,
"account_deletion_pending" => ErrorCode::AccountDeletionPending,
"validation_failed" => ErrorCode::ValidationFailed,
"rate_limited" => ErrorCode::RateLimited,
"internal_error" => ErrorCode::InternalError,
"service_unavailable" => ErrorCode::ServiceUnavailable,
other => ErrorCode::Unknown(other.to_string()),
}
}
pub(crate) fn from_status(status: u16) -> Self {
match status {
400 => ErrorCode::InvalidRequest,
401 => ErrorCode::Unauthorized,
403 => ErrorCode::Forbidden,
404 => ErrorCode::NotFound,
409 => ErrorCode::Conflict,
422 => ErrorCode::ValidationFailed,
429 => ErrorCode::RateLimited,
500 => ErrorCode::InternalError,
502 | 503 => ErrorCode::ServiceUnavailable,
other => ErrorCode::Unknown(format!("http_{other}")),
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Display for RunStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
RunStatus::Queued => "queued",
RunStatus::Running => "running",
RunStatus::Completed => "completed",
RunStatus::Failed => "failed",
RunStatus::Cancelled => "cancelled",
RunStatus::Unknown => "unknown",
};
f.write_str(s)
}
}