datalab-cli 0.1.0

A powerful CLI for converting, extracting, and processing documents using the Datalab API
Documentation
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()))
    }

    /// Returns an actionable suggestion for fixing the error
    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,
        }
    }

    /// Returns a help URL for more information about the error
    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>;