use serde::Serialize;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DatalabError {
#[error("API error ({status}): {message}")]
ApiError { status: u16, message: String },
#[error("Rate limited. Retry after {retry_after:?} seconds")]
RateLimited { retry_after: Option<u64> },
#[error("Request timed out after {seconds} seconds")]
Timeout { seconds: u64 },
#[error("Network error: {0}")]
NetworkError(#[from] reqwest::Error),
#[error("Cache error: {0}")]
CacheError(#[from] std::io::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Missing API key. Set DATALAB_API_KEY environment variable")]
MissingApiKey,
#[error("File not found: {0}")]
FileNotFound(PathBuf),
#[error("Processing failed: {0}")]
ProcessingFailed(String),
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub error: String,
pub code: String,
}
impl DatalabError {
pub fn code(&self) -> &'static str {
match self {
DatalabError::ApiError { .. } => "API_ERROR",
DatalabError::RateLimited { .. } => "RATE_LIMITED",
DatalabError::Timeout { .. } => "TIMEOUT",
DatalabError::NetworkError(_) => "NETWORK_ERROR",
DatalabError::CacheError(_) => "CACHE_ERROR",
DatalabError::JsonError(_) => "JSON_ERROR",
DatalabError::InvalidInput(_) => "INVALID_INPUT",
DatalabError::MissingApiKey => "MISSING_API_KEY",
DatalabError::FileNotFound(_) => "FILE_NOT_FOUND",
DatalabError::ProcessingFailed(_) => "PROCESSING_FAILED",
}
}
pub fn to_json(&self) -> String {
let response = ErrorResponse {
error: self.to_string(),
code: self.code().to_string(),
};
serde_json::to_string(&response)
.unwrap_or_else(|_| format!(r#"{{"error":"{}","code":"{}"}}"#, self, self.code()))
}
pub fn suggestion(&self) -> Option<String> {
match self {
DatalabError::MissingApiKey => Some(
"Set your API key:\n export DATALAB_API_KEY=\"your-api-key\"".to_string()
),
DatalabError::ApiError { status, .. } => match status {
401 => Some(
"Your API key appears to be invalid. Check that DATALAB_API_KEY is set correctly.".to_string()
),
403 => Some(
"Access forbidden. Your API key may not have permission for this operation.".to_string()
),
413 => Some(
"File too large. Maximum file size is 200MB.".to_string()
),
429 => Some(
"Rate limit exceeded. Wait a moment before retrying.".to_string()
),
500..=599 => Some(
"The API server encountered an error. Try again later.".to_string()
),
_ => None,
},
DatalabError::RateLimited { retry_after } => {
if let Some(secs) = retry_after {
Some(format!(
"You've exceeded the rate limit. Wait {} seconds before retrying.",
secs
))
} else {
Some(
"You've exceeded the rate limit. Wait a moment before retrying.".to_string()
)
}
}
DatalabError::Timeout { seconds } => Some(format!(
"The request timed out after {} seconds. Try:\n \
- Using --timeout with a higher value\n \
- Processing fewer pages with --max-pages\n \
- Checking your network connection",
seconds
)),
DatalabError::NetworkError(_) => Some(
"Could not connect to the API. Check your internet connection.".to_string()
),
DatalabError::FileNotFound(path) => Some(format!(
"The file '{}' does not exist or is not readable.\n \
Check that the path is correct and you have read permissions.",
path.display()
)),
DatalabError::InvalidInput(msg) => {
if msg.contains("schema") || msg.contains("JSON") {
Some(
"Ensure your JSON is valid. You can validate it with: jq . your-file.json".to_string()
)
} else {
None
}
}
DatalabError::ProcessingFailed(msg) => {
if msg.contains("unsupported") {
Some(
"This file format may not be supported. Supported formats: PDF, PNG, JPG, DOCX.".to_string()
)
} else {
None
}
}
_ => None,
}
}
pub fn help_url(&self) -> Option<&'static str> {
match self {
DatalabError::MissingApiKey => Some("https://www.datalab.to/app/keys"),
DatalabError::ApiError { status, .. } if *status == 429 => {
Some("https://documentation.datalab.to/docs/common/limits")
}
DatalabError::RateLimited { .. } => {
Some("https://documentation.datalab.to/docs/common/limits")
}
_ => None,
}
}
}
pub type Result<T> = std::result::Result<T, DatalabError>;