use thiserror::Error;
#[derive(Error, Debug)]
pub enum FilesError {
#[error("HTTP request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("Bad Request (400): {message}")]
BadRequest {
message: String,
field: Option<String>,
},
#[error("Authentication failed (401): {message}")]
AuthenticationFailed {
message: String,
auth_type: Option<String>,
},
#[error("Forbidden (403): {message}")]
Forbidden {
message: String,
resource: Option<String>,
},
#[error("Not Found (404): {message}")]
NotFound {
message: String,
resource_type: Option<String>,
path: Option<String>,
},
#[error("Conflict (409): {message}")]
Conflict {
message: String,
resource: Option<String>,
},
#[error("Precondition Failed (412): {message}")]
PreconditionFailed {
message: String,
condition: Option<String>,
},
#[error("Unprocessable Entity (422): {message}")]
UnprocessableEntity {
message: String,
field: Option<String>,
value: Option<String>,
},
#[error("Locked (423): {message}")]
Locked {
message: String,
resource: Option<String>,
},
#[error("Rate Limited (429): {message}")]
RateLimited {
message: String,
retry_after: Option<u64>,
},
#[error("Internal Server Error (500): {message}")]
InternalServerError {
message: String,
request_id: Option<String>,
},
#[error("Service Unavailable (503): {message}")]
ServiceUnavailable {
message: String,
retry_after: Option<u64>,
},
#[error("API error ({code}): {message}")]
ApiError {
code: u16,
message: String,
endpoint: Option<String>,
},
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("JSON deserialization error at '{path}': {source}")]
JsonPathError {
path: String,
source: serde_json::Error,
},
#[error("I/O error: {0}")]
IoError(String),
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
}
impl FilesError {
pub fn not_found(message: impl Into<String>) -> Self {
FilesError::NotFound {
message: message.into(),
resource_type: None,
path: None,
}
}
pub fn not_found_resource(
message: impl Into<String>,
resource_type: impl Into<String>,
path: impl Into<String>,
) -> Self {
FilesError::NotFound {
message: message.into(),
resource_type: Some(resource_type.into()),
path: Some(path.into()),
}
}
pub fn bad_request(message: impl Into<String>) -> Self {
FilesError::BadRequest {
message: message.into(),
field: None,
}
}
pub fn bad_request_field(message: impl Into<String>, field: impl Into<String>) -> Self {
FilesError::BadRequest {
message: message.into(),
field: Some(field.into()),
}
}
pub fn validation_failed(
message: impl Into<String>,
field: impl Into<String>,
value: impl Into<String>,
) -> Self {
FilesError::UnprocessableEntity {
message: message.into(),
field: Some(field.into()),
value: Some(value.into()),
}
}
pub fn rate_limited(message: impl Into<String>, retry_after: Option<u64>) -> Self {
FilesError::RateLimited {
message: message.into(),
retry_after,
}
}
pub fn with_resource_type(mut self, resource_type: impl Into<String>) -> Self {
if let FilesError::NotFound {
resource_type: rt, ..
} = &mut self
{
*rt = Some(resource_type.into());
}
self
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
if let FilesError::NotFound { path: p, .. } = &mut self {
*p = Some(path.into());
}
self
}
pub fn with_field(mut self, field: impl Into<String>) -> Self {
if let FilesError::BadRequest { field: f, .. } = &mut self {
*f = Some(field.into());
}
self
}
pub fn status_code(&self) -> Option<u16> {
match self {
FilesError::BadRequest { .. } => Some(400),
FilesError::AuthenticationFailed { .. } => Some(401),
FilesError::Forbidden { .. } => Some(403),
FilesError::NotFound { .. } => Some(404),
FilesError::Conflict { .. } => Some(409),
FilesError::PreconditionFailed { .. } => Some(412),
FilesError::UnprocessableEntity { .. } => Some(422),
FilesError::Locked { .. } => Some(423),
FilesError::RateLimited { .. } => Some(429),
FilesError::InternalServerError { .. } => Some(500),
FilesError::ServiceUnavailable { .. } => Some(503),
FilesError::ApiError { code, .. } => Some(*code),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
FilesError::RateLimited { .. }
| FilesError::ServiceUnavailable { .. }
| FilesError::InternalServerError { .. }
)
}
pub fn retry_after(&self) -> Option<u64> {
match self {
FilesError::RateLimited { retry_after, .. } => *retry_after,
FilesError::ServiceUnavailable { retry_after, .. } => *retry_after,
_ => None,
}
}
}
pub type Result<T> = std::result::Result<T, FilesError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_not_found_with_context() {
let error = FilesError::not_found_resource("File not found", "file", "/path/to/file.txt");
assert!(matches!(error, FilesError::NotFound { .. }));
assert!(error.to_string().contains("Not Found"));
}
#[test]
fn test_bad_request_with_field() {
let error = FilesError::bad_request_field("Invalid value", "username");
if let FilesError::BadRequest { field, .. } = error {
assert_eq!(field, Some("username".to_string()));
} else {
panic!("Expected BadRequest error");
}
}
#[test]
fn test_validation_failed() {
let error = FilesError::validation_failed("Invalid email format", "email", "not-an-email");
if let FilesError::UnprocessableEntity { field, value, .. } = error {
assert_eq!(field, Some("email".to_string()));
assert_eq!(value, Some("not-an-email".to_string()));
} else {
panic!("Expected UnprocessableEntity error");
}
}
#[test]
fn test_rate_limited_with_retry() {
let error = FilesError::rate_limited("Too many requests", Some(60));
assert_eq!(error.retry_after(), Some(60));
assert!(error.is_retryable());
}
#[test]
fn test_status_code_extraction() {
assert_eq!(FilesError::not_found("test").status_code(), Some(404));
assert_eq!(FilesError::bad_request("test").status_code(), Some(400));
assert_eq!(
FilesError::rate_limited("test", None).status_code(),
Some(429)
);
}
#[test]
fn test_is_retryable() {
assert!(FilesError::rate_limited("test", None).is_retryable());
assert!(
FilesError::InternalServerError {
message: "test".to_string(),
request_id: None
}
.is_retryable()
);
assert!(!FilesError::not_found("test").is_retryable());
}
#[test]
fn test_builder_pattern() {
let error = FilesError::not_found("File not found")
.with_resource_type("file")
.with_path("/test.txt");
if let FilesError::NotFound {
resource_type,
path,
..
} = error
{
assert_eq!(resource_type, Some("file".to_string()));
assert_eq!(path, Some("/test.txt".to_string()));
} else {
panic!("Expected NotFound error");
}
}
}