use thiserror::Error;
#[derive(Debug, Error)]
pub enum FirebaseError {
#[error("Auth error: {0}")]
Auth(#[from] AuthError),
#[error("Firestore error: {0}")]
Firestore(#[from] FirestoreError),
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Internal error: {0}")]
Internal(String),
#[error("API key not configured")]
ApiKeyNotConfigured,
#[error("Invalid API key: {0}")]
InvalidApiKey(String),
#[error("Operation cancelled")]
Cancelled,
#[error("Unknown error: {0}")]
Unknown(String),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum AuthError {
#[error("Invalid email address")]
InvalidEmail,
#[error("Invalid password")]
InvalidPassword,
#[error("Email already in use")]
EmailAlreadyInUse,
#[error("User not found")]
UserNotFound,
#[error("Wrong password")]
WrongPassword,
#[error("User account disabled")]
UserDisabled,
#[error("Too many requests, try again later")]
TooManyRequests,
#[error("Operation not allowed")]
OperationNotAllowed,
#[error("Invalid credential: {0}")]
InvalidCredential(String),
#[error("User token expired")]
UserTokenExpired,
#[error("Invalid user token")]
InvalidUserToken,
#[error("Network error: {0}")]
NetworkRequestFailed(String),
#[error("Not authenticated")]
NotAuthenticated,
#[error("No user is currently signed in")]
NoSignedInUser,
#[error("This operation requires recent authentication")]
RequiresRecentLogin,
#[error("Invalid API key")]
InvalidApiKey,
#[error("Account exists with different credential")]
AccountExistsWithDifferentCredential,
#[error("Invalid action code")]
InvalidActionCode,
#[error("Action code expired")]
ExpiredActionCode,
#[error("Unknown auth error: code {0}")]
Unknown(i32),
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum FirestoreError {
#[error("Document not found")]
NotFound,
#[error("Permission denied")]
PermissionDenied,
#[error("Resource already exists")]
AlreadyExists,
#[error("Resource exhausted")]
ResourceExhausted,
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error("Deadline exceeded")]
DeadlineExceeded,
#[error("Operation aborted")]
Aborted,
#[error("Out of range: {0}")]
OutOfRange(String),
#[error("Feature not implemented")]
Unimplemented,
#[error("Internal error: {0}")]
Internal(String),
#[error("Service unavailable")]
Unavailable,
#[error("Data loss")]
DataLoss,
#[error("Unauthenticated")]
Unauthenticated,
#[error("Connection error: {0}")]
Connection(String),
#[error("Unknown Firestore error: code {0}")]
Unknown(i32),
}
impl FirebaseError {
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
pub fn unknown(msg: impl Into<String>) -> Self {
Self::Unknown(msg.into())
}
pub fn is_retryable(&self) -> bool {
match self {
Self::Network(_)
| Self::Auth(AuthError::NetworkRequestFailed(_))
| Self::Auth(AuthError::TooManyRequests)
| Self::Firestore(FirestoreError::Unavailable)
| Self::Firestore(FirestoreError::DeadlineExceeded)
| Self::Firestore(FirestoreError::ResourceExhausted) => true,
_ => false,
}
}
pub fn requires_auth(&self) -> bool {
matches!(
self,
Self::Auth(AuthError::NoSignedInUser)
| Self::Auth(AuthError::RequiresRecentLogin)
| Self::Auth(AuthError::UserTokenExpired)
| Self::Auth(AuthError::InvalidUserToken)
| Self::Firestore(FirestoreError::Unauthenticated)
)
}
}
impl AuthError {
pub fn from_error_code(code: &str) -> Self {
match code {
"EMAIL_NOT_FOUND" => Self::UserNotFound,
"INVALID_PASSWORD" => Self::WrongPassword,
"USER_DISABLED" => Self::UserDisabled,
"TOO_MANY_ATTEMPTS_TRY_LATER" => Self::TooManyRequests,
"EMAIL_EXISTS" => Self::EmailAlreadyInUse,
"OPERATION_NOT_ALLOWED" => Self::OperationNotAllowed,
"INVALID_EMAIL" => Self::InvalidEmail,
"WEAK_PASSWORD" => Self::InvalidPassword,
"INVALID_ID_TOKEN" => Self::InvalidUserToken,
"TOKEN_EXPIRED" => Self::UserTokenExpired,
"INVALID_API_KEY" => Self::InvalidApiKey,
"CREDENTIAL_TOO_OLD_LOGIN_AGAIN" => Self::RequiresRecentLogin,
_ => Self::Unknown(0),
}
}
}
impl FirestoreError {
pub fn from_grpc_code(code: i32) -> Self {
match code {
1 => Self::Aborted,
2 => Self::Unknown(code),
3 => Self::InvalidArgument(String::new()),
4 => Self::DeadlineExceeded,
5 => Self::NotFound,
6 => Self::AlreadyExists,
7 => Self::PermissionDenied,
8 => Self::ResourceExhausted,
9 => Self::Aborted,
10 => Self::OutOfRange(String::new()),
11 => Self::Unimplemented,
12 => Self::Internal(String::new()),
13 => Self::Unavailable,
14 => Self::DataLoss,
15 => Self::Unauthenticated,
_ => Self::Unknown(code),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_error_into_firebase_error() {
let auth_err = AuthError::InvalidEmail;
let firebase_err: FirebaseError = auth_err.into();
assert!(matches!(firebase_err, FirebaseError::Auth(AuthError::InvalidEmail)));
}
#[test]
fn test_firestore_error_into_firebase_error() {
let fs_err = FirestoreError::NotFound;
let firebase_err: FirebaseError = fs_err.into();
assert!(matches!(firebase_err, FirebaseError::Firestore(FirestoreError::NotFound)));
}
#[test]
fn test_is_retryable() {
assert!(FirebaseError::Auth(AuthError::NetworkRequestFailed("test".to_string())).is_retryable());
assert!(FirebaseError::Auth(AuthError::TooManyRequests).is_retryable());
assert!(!FirebaseError::Auth(AuthError::InvalidEmail).is_retryable());
assert!(FirebaseError::Firestore(FirestoreError::Unavailable).is_retryable());
assert!(!FirebaseError::Firestore(FirestoreError::NotFound).is_retryable());
}
#[test]
fn test_requires_auth() {
assert!(FirebaseError::Auth(AuthError::NoSignedInUser).requires_auth());
assert!(FirebaseError::Auth(AuthError::RequiresRecentLogin).requires_auth());
assert!(FirebaseError::Firestore(FirestoreError::Unauthenticated).requires_auth());
assert!(!FirebaseError::Auth(AuthError::InvalidEmail).requires_auth());
}
#[test]
fn test_auth_error_from_code() {
assert_eq!(AuthError::from_error_code("EMAIL_NOT_FOUND"), AuthError::UserNotFound);
assert_eq!(AuthError::from_error_code("INVALID_EMAIL"), AuthError::InvalidEmail);
assert_eq!(AuthError::from_error_code("WEAK_PASSWORD"), AuthError::InvalidPassword);
}
#[test]
fn test_firestore_error_from_grpc() {
assert_eq!(FirestoreError::from_grpc_code(5), FirestoreError::NotFound);
assert_eq!(FirestoreError::from_grpc_code(7), FirestoreError::PermissionDenied);
assert_eq!(FirestoreError::from_grpc_code(13), FirestoreError::Unavailable);
}
#[test]
fn test_error_display() {
let err = FirebaseError::Auth(AuthError::InvalidEmail);
let display = format!("{}", err);
assert!(display.contains("Auth error"));
assert!(display.contains("Invalid email"));
}
#[test]
fn test_auth_error_equality() {
assert_eq!(AuthError::InvalidEmail, AuthError::InvalidEmail);
assert_ne!(AuthError::InvalidEmail, AuthError::WrongPassword);
}
}