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

341 lines
No EOL
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Понимание проекта 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<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**, которые координируют репозитории:
```rust
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 вручную):
```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<T> = Result<T, (StatusCode, String)>;
```
### Валидация
**Один источник правды**: Все валидации в `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 минут. Удачи в контрибуции!