- Implement complete Rust API server with axum framework - Add database abstraction supporting PostgreSQL and SQLite - Implement comprehensive gacha system with probability calculations - Add JWT authentication with atproto DID integration - Create card master data system with rarities (Normal, Rare, SuperRare, Kira, Unique) - Implement draw history tracking and collection management - Add API endpoints for authentication, card drawing, and collection viewing - Include database migrations for both PostgreSQL and SQLite - Maintain full compatibility with Python API implementation - Add comprehensive documentation and development guide 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
142 lines
4.5 KiB
Rust
142 lines
4.5 KiB
Rust
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use serde_json::json;
|
|
use thiserror::Error;
|
|
|
|
#[derive(Error, Debug)]
|
|
pub enum AppError {
|
|
#[error("Database error: {0}")]
|
|
Database(#[from] sqlx::Error),
|
|
|
|
#[error("Migration error: {0}")]
|
|
Migration(#[from] sqlx::migrate::MigrateError),
|
|
|
|
#[error("Validation error: {0}")]
|
|
Validation(String),
|
|
|
|
#[error("Authentication error: {0}")]
|
|
Authentication(String),
|
|
|
|
#[error("Authorization error: {0}")]
|
|
Authorization(String),
|
|
|
|
#[error("Not found: {0}")]
|
|
NotFound(String),
|
|
|
|
#[error("Conflict: {0}")]
|
|
Conflict(String),
|
|
|
|
#[error("External service error: {0}")]
|
|
ExternalService(String),
|
|
|
|
#[error("Configuration error: {0}")]
|
|
Configuration(String),
|
|
|
|
#[error("JSON serialization error: {0}")]
|
|
Json(#[from] serde_json::Error),
|
|
|
|
#[error("HTTP client error: {0}")]
|
|
HttpClient(#[from] reqwest::Error),
|
|
|
|
#[error("JWT error: {0}")]
|
|
Jwt(#[from] jsonwebtoken::errors::Error),
|
|
|
|
#[error("Internal server error: {0}")]
|
|
Internal(String),
|
|
}
|
|
|
|
impl IntoResponse for AppError {
|
|
fn into_response(self) -> Response {
|
|
let (status, error_message, error_code) = match &self {
|
|
AppError::Database(e) => {
|
|
tracing::error!("Database error: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Database error", "DATABASE_ERROR")
|
|
}
|
|
AppError::Migration(e) => {
|
|
tracing::error!("Migration error: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Migration error", "MIGRATION_ERROR")
|
|
}
|
|
AppError::Validation(msg) => {
|
|
(StatusCode::BAD_REQUEST, msg.as_str(), "VALIDATION_ERROR")
|
|
}
|
|
AppError::Authentication(msg) => {
|
|
(StatusCode::UNAUTHORIZED, msg.as_str(), "AUTHENTICATION_ERROR")
|
|
}
|
|
AppError::Authorization(msg) => {
|
|
(StatusCode::FORBIDDEN, msg.as_str(), "AUTHORIZATION_ERROR")
|
|
}
|
|
AppError::NotFound(msg) => {
|
|
(StatusCode::NOT_FOUND, msg.as_str(), "NOT_FOUND")
|
|
}
|
|
AppError::Conflict(msg) => {
|
|
(StatusCode::CONFLICT, msg.as_str(), "CONFLICT")
|
|
}
|
|
AppError::ExternalService(msg) => {
|
|
tracing::error!("External service error: {}", msg);
|
|
(StatusCode::BAD_GATEWAY, "External service unavailable", "EXTERNAL_SERVICE_ERROR")
|
|
}
|
|
AppError::Configuration(msg) => {
|
|
tracing::error!("Configuration error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Configuration error", "CONFIGURATION_ERROR")
|
|
}
|
|
AppError::Json(e) => {
|
|
tracing::error!("JSON error: {}", e);
|
|
(StatusCode::BAD_REQUEST, "Invalid JSON", "JSON_ERROR")
|
|
}
|
|
AppError::HttpClient(e) => {
|
|
tracing::error!("HTTP client error: {}", e);
|
|
(StatusCode::BAD_GATEWAY, "External service error", "HTTP_CLIENT_ERROR")
|
|
}
|
|
AppError::Jwt(e) => {
|
|
tracing::error!("JWT error: {}", e);
|
|
(StatusCode::UNAUTHORIZED, "Invalid token", "JWT_ERROR")
|
|
}
|
|
AppError::Internal(msg) => {
|
|
tracing::error!("Internal error: {}", msg);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error", "INTERNAL_ERROR")
|
|
}
|
|
};
|
|
|
|
let body = Json(json!({
|
|
"error": {
|
|
"code": error_code,
|
|
"message": error_message,
|
|
"timestamp": chrono::Utc::now().to_rfc3339()
|
|
}
|
|
}));
|
|
|
|
(status, body).into_response()
|
|
}
|
|
}
|
|
|
|
// Convenience methods for common errors
|
|
impl AppError {
|
|
pub fn validation<T: Into<String>>(msg: T) -> Self {
|
|
Self::Validation(msg.into())
|
|
}
|
|
|
|
pub fn authentication<T: Into<String>>(msg: T) -> Self {
|
|
Self::Authentication(msg.into())
|
|
}
|
|
|
|
pub fn authorization<T: Into<String>>(msg: T) -> Self {
|
|
Self::Authorization(msg.into())
|
|
}
|
|
|
|
pub fn not_found<T: Into<String>>(msg: T) -> Self {
|
|
Self::NotFound(msg.into())
|
|
}
|
|
|
|
pub fn conflict<T: Into<String>>(msg: T) -> Self {
|
|
Self::Conflict(msg.into())
|
|
}
|
|
|
|
pub fn internal<T: Into<String>>(msg: T) -> Self {
|
|
Self::Internal(msg.into())
|
|
}
|
|
}
|
|
|
|
pub type AppResult<T> = Result<T, AppError>; |