341 lines
No EOL
18 KiB
Markdown
341 lines
No EOL
18 KiB
Markdown
# Понимание проекта 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 минут. Удачи в контрибуции! |