hyperion/00-work/doc.md
2025-12-28 19:00:03 +03:00

18 KiB
Raw Permalink Blame History

Понимание проекта 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 — это контент-объект с жизненным циклом:

  • Статус: draftpublic (см. миграцию 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/):

// Паттерн: каждая сущность имеет trait + impl
#[async_trait]
pub trait TrailRepository: Send + Sync {
    async fn create(&self, trail: domain::trails::NewTrail) -> Result<TrailId>;
    // ...
}

Реализация зависит от 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, которые координируют репозитории:

pub struct TrailService {
    trail_repo: Arc<dyn TrailRepository>,
    user_repo: Arc<dyn UserRepository>,
    storage: Arc<dyn StorageService>,
}

Здесь проверяются сложные инварианты: права доступа, консистентность данных, транзакционность.

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 вручную):

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: Запуск и отладка

# 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/) — лучшая документация:

# 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/:idapi::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 для верхнего уровня:

// domain ошибка
#[derive(Error, Debug)]
pub enum TrailError {
    #[error("Trail not found")]
    NotFound,
    #[error("Permission denied")]
    Forbidden,
}

// api уровень
pub type ApiResult<T> = Result<T, (StatusCode, String)>;

Валидация

Один источник правды: Все валидации в domain DTO:

#[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

# .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

Быстрый поиск по коду

# Найти все использования 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, а sqlxrustls. Это может вызвать конфликты SSL-библиотек в NixOS. Следите за линковкой.

OpenDAL features

opendal собран без default features, только с memory и fs. Если понадобится S3, нужно расширить features в infrastructure/Cargo.toml.

Migration dependencies

Миграции зависят от entity crate. Если меняете entity, всегда пересобирайте миграции:

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 минут. Удачи в контрибуции!