1use serde::Serialize;
2use std::path::PathBuf;
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum DatalabError {
7 #[error("API error ({status}): {message}")]
8 ApiError { status: u16, message: String },
9
10 #[error("Rate limited. Retry after {retry_after:?} seconds")]
11 RateLimited { retry_after: Option<u64> },
12
13 #[error("Request timed out after {seconds} seconds")]
14 Timeout { seconds: u64 },
15
16 #[error("Network error: {0}")]
17 NetworkError(#[from] reqwest::Error),
18
19 #[error("Cache error: {0}")]
20 CacheError(#[from] std::io::Error),
21
22 #[error("JSON error: {0}")]
23 JsonError(#[from] serde_json::Error),
24
25 #[error("Invalid input: {0}")]
26 InvalidInput(String),
27
28 #[error("Missing API key. Set DATALAB_API_KEY environment variable")]
29 MissingApiKey,
30
31 #[error("File not found: {0}")]
32 FileNotFound(PathBuf),
33
34 #[error("Processing failed: {0}")]
35 ProcessingFailed(String),
36}
37
38#[derive(Serialize)]
39pub struct ErrorResponse {
40 pub error: String,
41 pub code: String,
42}
43
44impl DatalabError {
45 pub fn code(&self) -> &'static str {
46 match self {
47 DatalabError::ApiError { .. } => "API_ERROR",
48 DatalabError::RateLimited { .. } => "RATE_LIMITED",
49 DatalabError::Timeout { .. } => "TIMEOUT",
50 DatalabError::NetworkError(_) => "NETWORK_ERROR",
51 DatalabError::CacheError(_) => "CACHE_ERROR",
52 DatalabError::JsonError(_) => "JSON_ERROR",
53 DatalabError::InvalidInput(_) => "INVALID_INPUT",
54 DatalabError::MissingApiKey => "MISSING_API_KEY",
55 DatalabError::FileNotFound(_) => "FILE_NOT_FOUND",
56 DatalabError::ProcessingFailed(_) => "PROCESSING_FAILED",
57 }
58 }
59
60 pub fn to_json(&self) -> String {
61 let response = ErrorResponse {
62 error: self.to_string(),
63 code: self.code().to_string(),
64 };
65 serde_json::to_string(&response)
66 .unwrap_or_else(|_| format!(r#"{{"error":"{}","code":"{}"}}"#, self, self.code()))
67 }
68
69 pub fn suggestion(&self) -> Option<String> {
71 match self {
72 DatalabError::MissingApiKey => Some(
73 "Set your API key:\n export DATALAB_API_KEY=\"your-api-key\"".to_string()
74 ),
75 DatalabError::ApiError { status, .. } => match status {
76 401 => Some(
77 "Your API key appears to be invalid. Check that DATALAB_API_KEY is set correctly.".to_string()
78 ),
79 403 => Some(
80 "Access forbidden. Your API key may not have permission for this operation.".to_string()
81 ),
82 413 => Some(
83 "File too large. Maximum file size is 200MB.".to_string()
84 ),
85 429 => Some(
86 "Rate limit exceeded. Wait a moment before retrying.".to_string()
87 ),
88 500..=599 => Some(
89 "The API server encountered an error. Try again later.".to_string()
90 ),
91 _ => None,
92 },
93 DatalabError::RateLimited { retry_after } => {
94 if let Some(secs) = retry_after {
95 Some(format!(
96 "You've exceeded the rate limit. Wait {} seconds before retrying.",
97 secs
98 ))
99 } else {
100 Some(
101 "You've exceeded the rate limit. Wait a moment before retrying.".to_string()
102 )
103 }
104 }
105 DatalabError::Timeout { seconds } => Some(format!(
106 "The request timed out after {} seconds. Try:\n \
107 - Using --timeout with a higher value\n \
108 - Processing fewer pages with --max-pages\n \
109 - Checking your network connection",
110 seconds
111 )),
112 DatalabError::NetworkError(_) => Some(
113 "Could not connect to the API. Check your internet connection.".to_string()
114 ),
115 DatalabError::FileNotFound(path) => Some(format!(
116 "The file '{}' does not exist or is not readable.\n \
117 Check that the path is correct and you have read permissions.",
118 path.display()
119 )),
120 DatalabError::InvalidInput(msg) => {
121 if msg.contains("schema") || msg.contains("JSON") {
122 Some(
123 "Ensure your JSON is valid. You can validate it with: jq . your-file.json".to_string()
124 )
125 } else {
126 None
127 }
128 }
129 DatalabError::ProcessingFailed(msg) => {
130 if msg.contains("unsupported") {
131 Some(
132 "This file format may not be supported. Supported formats: PDF, PNG, JPG, DOCX.".to_string()
133 )
134 } else {
135 None
136 }
137 }
138 _ => None,
139 }
140 }
141
142 pub fn help_url(&self) -> Option<&'static str> {
144 match self {
145 DatalabError::MissingApiKey => Some("https://www.datalab.to/app/keys"),
146 DatalabError::ApiError { status, .. } if *status == 429 => {
147 Some("https://documentation.datalab.to/docs/common/limits")
148 }
149 DatalabError::RateLimited { .. } => {
150 Some("https://documentation.datalab.to/docs/common/limits")
151 }
152 _ => None,
153 }
154 }
155}
156
157pub type Result<T> = std::result::Result<T, DatalabError>;