"""Доменные исключения и единый конверт ошибок API. Формат ответа: {"error": {"code": "...", "message": "...", "details": {...}}} """ from __future__ import annotations from typing import Any from fastapi import Request from fastapi.responses import JSONResponse class AppError(Exception): """Базовая ошибка приложения со стабильным машинным кодом.""" status_code: int = 400 code: str = "BAD_REQUEST" def __init__( self, message: str, *, code: str | None = None, status_code: int | None = None, details: dict[str, Any] | None = None, ) -> None: super().__init__(message) self.message = message if code is not None: self.code = code if status_code is not None: self.status_code = status_code self.details = details def to_response(self) -> JSONResponse: return JSONResponse( status_code=self.status_code, content={ "error": { "code": self.code, "message": self.message, "details": self.details, } }, ) class NotFoundError(AppError): status_code = 404 code = "NOT_FOUND" class ForbiddenError(AppError): status_code = 403 code = "FORBIDDEN" class ValidationError(AppError): status_code = 422 code = "VALIDATION_ERROR" class ConflictError(AppError): status_code = 409 code = "CONFLICT" class AuthError(AppError): status_code = 401 code = "UNAUTHORIZED" # ─── Конкретные ошибки со стабильными кодами для фронта ─────────────────────── class NoGroupError(AppError): status_code = 409 code = "NO_GROUP" def __init__(self) -> None: super().__init__("Вы не состоите ни в одной группе.") class NotGroupMemberError(ForbiddenError): code = "NOT_GROUP_MEMBER" def __init__(self) -> None: super().__init__("Вы не являетесь участником этой группы.") class NicknameTakenError(ConflictError): code = "NICKNAME_TAKEN" def __init__(self) -> None: super().__init__("Никнейм уже занят.") class FactionNotAvailableError(ValidationError): code = "FACTION_NOT_AVAILABLE" def __init__(self) -> None: super().__init__("Фракция недоступна выбранной группе.") class DuplicateParticipantError(ValidationError): code = "DUPLICATE_PARTICIPANT" def __init__(self) -> None: super().__init__("Игрок или фракция повторяются в партии.") class InvalidRankingError(ValidationError): code = "INVALID_RANKING" def __init__(self, message: str = "Некорректная расстановка мест.") -> None: super().__init__(message) class InvalidCredentialsError(AuthError): code = "INVALID_CREDENTIALS" def __init__(self) -> None: super().__init__("Неверный логин или пароль.") async def app_error_handler(_request: Request, exc: AppError) -> JSONResponse: return exc.to_response()