# Понимание проекта Trails Server: Архитектурный разбор и гайд для контрибьютора ## 1. Философия архитектуры: Почему именно такая структура? Проект следует принципам **Clean Architecture** (чистая архитектура) с четким разделением ответственности. Каждый crate — это слой с собственной причиной существования: - **domain** изолирует бизнес-правила от деталей реализации (ORM, HTTP, БД) - **entity** определяет data access objects (DAO), привязанные к SeaORM - **infrastructure** содержит "грязные" детали: репозитории, внешние сервисы, шаблоны email - **api-core** реализует бизнес-логику (use cases), зависящую только от domain - **api** — чистый адаптер HTTP, преобразующий запросы/ответы, но не содержащий логики - **bin** — entrypoint, который wire'ит все компоненты вместе Эта структура позволяет: - **Тестировать бизнес-логику без HTTP/БД** (mock'ая repository) - **Менять технологии без рефакторинга** (например, заменить PostgreSQL на SQLite для тестов) - **Разным бинарникам использовать одну логику** (admin и client) --- ## 2. Обзор ключевых концепций домена ### "Trail" — центральная сущность На основе файлов миграций и структур, Trail — это контент-объект с жизненным циклом: - **Статус**: `draft` → `public` (см. миграцию `m20250129_122341`) - **Иерархия**: может быть root-элементом или child'ом (см. `m20241020_153507`) - **Thread**: цепочка связанных trails (соединение через `thread_id`) - **Content types**: поддержка множественных типов контента (`m20250618_120032`) - **Аналитика**: история посещений (`trails_visit_history`) - **Взаимодействие**: система лайков (`like.rs`) ### Abuse System Модерация с многоуровневой системой репортов (degree 1/2) и причинами (`abuse_reason.rs`) ### Два API в одном сервисе - **Client API** (`/client/*`): user-facing endpoints для создания trails, авторизации, профилей - **Admin API** (`/admin/*`): administrative endpoints для модерации, статистики, управления пользователями --- ## 3. Путешествие запроса: От TCP-сокета до БД ``` ┌─────────────────────────────────────────────────────────────────┐ │ bin::client │ │ └── tokio::main() │ │ └── axum::Server::bind() │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ api::router::client::create_router() │ │ └── routes like POST /trails │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ api::endpoints::client::trails::create_trail() │ │ ├── Extractors (JWT, FormData) │ │ └── Валидация через validator crate │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ api_core::trails::TrailsService::create() │ │ └── Бизнес-правила: проверка прав, валидация статуса │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ infrastructure::repositories::trail::TrailRepository │ │ └── sea_orm::Entity::insert() (database abstraction) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ entity::trail::ActiveModel → PostgreSQL │ └─────────────────────────────────────────────────────────────────┘ ``` **Ключевой принцип**: `api` crate **не знает** о существовании БД. Он работает с абстракциями (`api-core` traits). --- ## 4. Глубокое погружение в crates ### `entity` — Data Access Layer - **Содержит**: SeaORM entity definitions, связанные 1:1 с таблицами БД - **Важно**: Все поля `pub`, но это нормально — это DTO для ORM - **Пример**: `user.rs` содержит `#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]` - **Использование**: Никогда не используйте `entity` в бизнес-логике напрямую. Всегда маппайте в `domain` ### `domain` — Language of the Business - **request/response**: DTO для API (разделены на `client`/`admin`) - **common**: Переиспользуемые типы (пагинация, токены, abuse) - **Валидация**: Derive-метки `#[derive(Validate)]` — central source of truth - **Пример**: `domain::client::trails::CreateTrailRequest` с валидацией полей ### `infrastructure` — "Грязные детали" **Репозитории** (`repositories/`): ```rust // Паттерн: каждая сущность имеет trait + impl #[async_trait] pub trait TrailRepository: Send + Sync { async fn create(&self, trail: domain::trails::NewTrail) -> Result; // ... } ``` Реализация зависит от `sea_orm`, но домен ничего не знает об этом. **Services** (`services/`): - `auth.rs`: JWT encoding/decoding, bcrypt/argon2 - `mail.rs`: Askama templates (type-safe email HTML) - `storage/`: Абстракция над файловым хранилищем через OpenDAL с **стратегиями**: - `single`: простое хранение - `mirror`: дублирование в несколько бакетов - `backup`: асинхронный бэкап в cold storage **Forms** (`forms/`): Type-safe multipart form handling (`axum_typed_multipart`) ### `api-core` — Use Cases Orchestrator Содержит **stateless services**, которые координируют репозитории: ```rust pub struct TrailService { trail_repo: Arc, user_repo: Arc, storage: Arc, } ``` Здесь проверяются сложные инварианты: права доступа, консистентность данных, транзакционность. ### `api` — HTTP Adapter **Extractors** (`extractors/`): - `jwt.rs`: Извлечение claims из `Authorization: Bearer` - `api_key_extractor.rs`: API key auth - `multitype_form_extractor.rs`: Объединяет JSON и multipart **Middleware** (`middleware/`): - `jwt.rs`: Проверка JWT - `api_key_middleware.rs`: Rate limiting + API key validation **Router**: Разделение на `admin.rs` и `client.rs` для безопасности ### `bin` — Composition Root Инициализация зависимостей (DI container вручную): ```rust let db_pool = infrastructure::db::connect().await?; let trail_repo = Arc::new(TrailRepositoryImpl::new(db_pool.clone())); let trail_service = Arc::new(TrailService::new(trail_repo, ...)); let router = api::router::client::create_router(trail_service, ...); ``` **Важно**: Никакой логики, только сборка компонентов. ### `migrate` — Database Schema as Code SeaORM миграции с timestamp-именами (`m20240910_074808`). Каждая миграция — отдельный модуль, проверяющий `sea-orm-migration::MigrationTrait`. --- ## 5. Как начать изучение: Практический план ### Шаг 1: Запуск и отладка ```bash # 1. Понять переменные окружения (см. .env.example или docker-compose.yml) DATABASE_URL=postgres://... # 2. Запустить миграции cargo run --bin migrate # 3. Запустить клиентский API cargo run --bin client # 4. Проверить health endpoint curl http://localhost:3000/client/ping # 5. Запустить тестовый сценарий hurl examples/positive/register_positive.hurl ``` ### Шаг 2: Изучение через тесты **Hurl-тесты** (`examples/positive/`) — лучшая документация: ```hurl # examples/positive/user_login.hurl POST http://localhost:3000/client/auth/login { "username": "testuser", "password": "testpass123" } HTTP 200 [Captures] access_token: jsonpath "$.access_token" ``` Это показывает **real-world usage** без изучения кода. ### Шаг 3: Трейсинг одной фичи Выберите простой endpoint и пройдитесь по нему: 1. **GET /client/profiles/:id** → `api::endpoints::client::profiles.rs` 2. Смотрите, как используется `ProfileService` 3. Изучите `ProfileRepository` в `infrastructure` 4. Проверьте `domain::client::profiles::responses::ProfileResponse` ### Шаг 4: Изучение доменной модели Прочитайте **все миграции по порядку** (`crates/migrate/src/`). Это история эволюции бизнес-требований: - Сначала был User и Trail - Потом добавили лайки, треды, статусы - Затем abuse system - Потом множественные content types --- ## 6. Паттерны и конвенции ### Обработка ошибок Все crates используют `thiserror` для domain errors и `anyhow` для верхнего уровня: ```rust // domain ошибка #[derive(Error, Debug)] pub enum TrailError { #[error("Trail not found")] NotFound, #[error("Permission denied")] Forbidden, } // api уровень pub type ApiResult = Result; ``` ### Валидация **Один источник правды**: Все валидации в `domain` DTO: ```rust #[derive(Validate)] pub struct CreateTrailRequest { #[validate(length(min = 3, max = 200))] pub title: String, #[validate(custom = "validate_content_type")] pub content_type: String, } ``` ### Фича-флаги через миграции Статусы, фичи и права добавляются через миграции, а не конфиги. Это делает состояние БД источником правды. --- ## 7. Где что менять: GPS для контрибьютора ### Добавить новое поле в Trail 1. **Миграция**: `crates/migrate/src/m2025xxxx_add_field.rs` 2. **Entity**: Добавить поле в `crates/entity/src/trail.rs` 3. **Domain**: Обновить `domain::trails::NewTrail` и Response 4. **Repository**: Изменить маппинг в `infrastructure::repositories::trail` 5. **Form**: Обновить `infrastructure::forms::forms_trail.rs` (если поле editable) 6. **Tests**: Добавить hurl-тест в `examples/positive/` ### Добавить новый endpoint 1. **Domain**: Создать Request/Response DTO 2. **API-core**: Создать метод в Service 3. **API**: Добавить endpoint в `api::endpoints::client` или `admin` 4. **Router**: Подключить в `api::router` 5. **Hurl**: Создать тест в `examples/` 6. **Utoipa**: Добавить `#[utoipa::path]` для Swagger-документации ### Исправить баг в бизнес-логике **Ищите в `api-core`**, а не в `api`. Например, логика перехода Draft→Public в `TrailService::update_status()`. ### Поменять storage backend Измените инициализацию в `infrastructure::services::storage_service.rs` или добавьте новый driver в `storage/drivers/`. --- ## 8. Инструменты для вашего стека (NixOS + Helix) ### NixOS-specific - **Postgres**: `services.postgresql.enable = true;` в configuration.nix - **Rust toolchain**: Используйте `rust-overlay` или `fenix` для точной версии 1.92.0 - **Дев-окружение**: Создайте `shell.nix` с зависимостями: `postgresql`, `hurl`, `docker` (for tests) ### Helix editor workflow ```toml # .helix/languages.toml [language-server.rust-analyzer] config = { cargo = { features = "all" } } ``` - **Jump to definition**: `gd` перейдет от `domain` к `entity` через trait - **Global search**: `Space + /` ищите по `examples/` чтобы найти использование endpoint - **LSP go-to-impl**: Посмотреть реализацию repository ### Быстрый поиск по коду ```bash # Найти все использования TrailService rg "TrailService" --type rust # Найти все endpoints с /trails rg "trails" crates/api/src/router/ # Найти миграции за конкретную дату ls crates/migrate/src | rg "2025-02" ``` --- ## 9. Риски и особенности для контрибьютора ### Version pinning - `tokio = "1,<1.48"` — **исключительно важно**. Это ограничение может быть связано с `sea-orm` или `axum` совместимостью. Не обновляйте без тестов. - `sea-orm = "=1.1.17"` — точная версия, возможно, из-за багов в 1.2+ ### Async runtime Везде используется `tokio::main`, но `sea-orm` использует `native-tls`, а `sqlx` — `rustls`. Это может вызвать конфликты SSL-библиотек в NixOS. Следите за линковкой. ### OpenDAL features `opendal` собран без default features, только с memory и fs. Если понадобится S3, нужно расширить features в `infrastructure/Cargo.toml`. ### Migration dependencies Миграции зависят от `entity` crate. Если меняете entity, **всегда пересобирайте миграции**: ```bash cargo clean -p migrate && cargo build -p migrate ``` --- ## 10. Чеклист перед PR - [ ] Hurl-тесты проходят: `cargo test --test hurl_test` - [ ] Clippy чист: `cargo clippy --all-targets --all-features` - [ ] Форматирование: `cargo fmt --all` - [ ] Миграции откатываются: `cargo run --bin migrate down` - [ ] OpenAPI спецификация генерируется (если меняли endpoints) - [ ] Логирование добавлено: `tracing::info!/`debug!` на важных шагах - [ ] Обработка ошибок: Domain ошибки преобразованы в правильные HTTP статусы --- **Финальный совет**: Начните с **трех файлов**: 1. `crates/migrate/src/lib.rs` — поймете эволюцию домена 2. `crates/api/src/router/client.rs` — увидите все endpoint'ы 3. `crates/api-core/src/trails.rs` — поймете бизнес-логику Это даст вам mental model проекта за 30 минут. Удачи в контрибуции!