/* * Copyright (C) 2026 Mateusz GruszczyƄski @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Free for non-commercial and evaluation use under terms of BSL/GPLv3. * Commercial or production use requires a valid paid license. * See LICENSE file in repository root for details. */ use axum::{ Json, http::StatusCode, response::{IntoResponse, Response}, }; pub struct ApiError { status: StatusCode, message: String, } impl ApiError { pub(crate) fn bad_request(message: &str) -> Self { Self { status: StatusCode::BAD_REQUEST, message: message.into(), } } pub(crate) fn rate_limited(message: &str) -> Self { Self { status: StatusCode::TOO_MANY_REQUESTS, message: message.into(), } } pub(crate) fn payload_too_large(max_bytes: usize) -> Self { let max_mb = max_bytes / (1024 * 1024); Self { status: StatusCode::PAYLOAD_TOO_LARGE, message: format!("The file may be at most {max_mb} MB"), } } pub(crate) fn not_found_file() -> Self { Self { status: StatusCode::NOT_FOUND, message: "File not found".into(), } } pub(crate) fn unauthorized() -> Self { Self { status: StatusCode::UNAUTHORIZED, message: "Invalid password".into(), } } pub(crate) fn forbidden(message: &str) -> Self { Self { status: StatusCode::FORBIDDEN, message: message.into(), } } pub(crate) fn not_found_workspace() -> Self { Self { status: StatusCode::NOT_FOUND, message: "Workspace not found".into(), } } pub(crate) fn not_found_note() -> Self { Self { status: StatusCode::NOT_FOUND, message: "Note not found".into(), } } pub(crate) fn not_found_revision() -> Self { Self { status: StatusCode::NOT_FOUND, message: "Revision not found".into(), } } pub(crate) fn internal(message: &str) -> Self { Self { status: StatusCode::INTERNAL_SERVER_ERROR, message: message.into(), } } } impl From for ApiError { fn from(error: sqlx::Error) -> Self { tracing::error!(%error, "database error"); Self::internal("Database error") } } impl IntoResponse for ApiError { fn into_response(self) -> Response { ( self.status, Json(serde_json::json!({"error": self.message})), ) .into_response() } }