18 KiB
Понимание проекта 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/):
// Паттерн: каждая сущность имеет 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/argon2mail.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: Bearerapi_key_extractor.rs: API key authmultitype_form_extractor.rs: Объединяет JSON и multipart
Middleware (middleware/):
jwt.rs: Проверка JWTapi_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 и пройдитесь по нему:
- GET /client/profiles/:id →
api::endpoints::client::profiles.rs - Смотрите, как используется
ProfileService - Изучите
ProfileRepositoryвinfrastructure - Проверьте
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
- Миграция:
crates/migrate/src/m2025xxxx_add_field.rs - Entity: Добавить поле в
crates/entity/src/trail.rs - Domain: Обновить
domain::trails::NewTrailи Response - Repository: Изменить маппинг в
infrastructure::repositories::trail - Form: Обновить
infrastructure::forms::forms_trail.rs(если поле editable) - Tests: Добавить hurl-тест в
examples/positive/
Добавить новый endpoint
- Domain: Создать Request/Response DTO
- API-core: Создать метод в Service
- API: Добавить endpoint в
api::endpoints::clientилиadmin - Router: Подключить в
api::router - Hurl: Создать тест в
examples/ - 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, а sqlx — rustls. Это может вызвать конфликты 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 статусы
Финальный совет: Начните с трех файлов:
crates/migrate/src/lib.rs— поймете эволюцию доменаcrates/api/src/router/client.rs— увидите все endpoint'ыcrates/api-core/src/trails.rs— поймете бизнес-логику
Это даст вам mental model проекта за 30 минут. Удачи в контрибуции!