317 lines
9.4 KiB
Markdown
317 lines
9.4 KiB
Markdown
# Rust Code Rules
|
||
|
||
## 1. Назначение документа
|
||
|
||
Этот документ фиксирует Rust-specific правила кода для проекта:
|
||
|
||
- toolchain;
|
||
- linting;
|
||
- formatting;
|
||
- ошибки;
|
||
- async;
|
||
- ownership;
|
||
- visibility;
|
||
- dependency hygiene.
|
||
|
||
Цель документа - убрать плавающие договоренности по стилю и практике командной Rust-разработки.
|
||
|
||
## 2. Toolchain
|
||
|
||
### 2.1. Версия Rust
|
||
|
||
Для проекта должен быть зафиксирован `rust-toolchain.toml`.
|
||
|
||
В нем должны быть определены:
|
||
|
||
- стабильный `channel`;
|
||
- `protocol`;
|
||
- при необходимости `components`.
|
||
|
||
Рекомендуемый состав:
|
||
|
||
- `rustfmt`
|
||
- `clippy`
|
||
|
||
### 2.2. MSRV
|
||
|
||
Нужно зафиксировать `MSRV` - минимально поддерживаемую версию Rust.
|
||
|
||
Правило:
|
||
|
||
- без необходимости не использовать возможности языка новее зафиксированного `MSRV`;
|
||
- обновление `MSRV` - это отдельное осознанное решение.
|
||
|
||
## 3. Formatting и linting
|
||
|
||
### 3.1. Formatting
|
||
|
||
Обязательное правило:
|
||
|
||
- весь код форматируется через `cargo fmt`.
|
||
|
||
Ручной стиль форматирования не обсуждается и не поддерживается.
|
||
|
||
### 3.2. Clippy
|
||
|
||
Обязательное правило:
|
||
|
||
- `cargo clippy --all-targets --all-features -- -D warnings`
|
||
|
||
Предупреждения считаются ошибками, если нет явно зафиксированного исключения.
|
||
|
||
### 3.3. CI quality gates
|
||
|
||
Минимально в CI должны запускаться:
|
||
|
||
- `cargo fmt --check`
|
||
- `cargo clippy --all-targets --all-features -- -D warnings`
|
||
- `cargo test`
|
||
|
||
Опционально позже:
|
||
|
||
- `cargo deny`
|
||
- `cargo audit`
|
||
|
||
## 4. `unsafe`
|
||
|
||
Для проекта принимается правило:
|
||
|
||
- `unsafe` запрещен по умолчанию.
|
||
|
||
Если когда-либо потребуется `unsafe`, то:
|
||
|
||
- это должно быть отдельное осознанное решение;
|
||
- причина должна быть технически обоснована;
|
||
- блок должен быть минимальным;
|
||
- вокруг него должны быть тесты.
|
||
|
||
Для MVP можно считать:
|
||
|
||
- `unsafe_code = deny`
|
||
|
||
## 5. Panic policy
|
||
|
||
В production code запрещены:
|
||
|
||
- `unwrap()`
|
||
- `expect()`
|
||
- `todo!()`
|
||
- `unimplemented!()`
|
||
- `dbg!()`
|
||
- необоснованные `panic!()`
|
||
|
||
Допускается:
|
||
|
||
- в тестах;
|
||
- в очень раннем bootstrap-коде, если это действительно аварийное завершение и не часть доменной логики.
|
||
|
||
Базовое правило:
|
||
|
||
- ошибки возвращаются через `Result`, а не через panic.
|
||
|
||
## 6. Правила ошибок
|
||
|
||
### 6.1. Domain и service errors
|
||
|
||
В домене и сервисах использовать типизированные ошибки.
|
||
|
||
Рекомендуемо:
|
||
|
||
- `thiserror`
|
||
|
||
### 6.2. Application boundary
|
||
|
||
На верхних слоях приложений допускается агрегирование ошибок, если это упрощает wiring.
|
||
|
||
При необходимости:
|
||
|
||
- `anyhow` только на внешних границах приложения, не в доменной модели.
|
||
|
||
### 6.3. Error context
|
||
|
||
Ошибка должна сохранять стадию отказа:
|
||
|
||
- schema
|
||
- mapping
|
||
- adapter
|
||
- persistence
|
||
- external service
|
||
- internal runtime
|
||
|
||
## 7. Visibility rules
|
||
|
||
Правило:
|
||
|
||
- по умолчанию все приватное;
|
||
- `pub(crate)` предпочтительнее `pub`;
|
||
- публичный API должен быть минимальным.
|
||
|
||
Нельзя:
|
||
|
||
- открывать модуль наружу "на всякий случай";
|
||
- делать `pub` просто ради удобства из соседнего файла;
|
||
- реэкспортировать целые деревья модулей без причины.
|
||
|
||
## 8. Ownership и данные
|
||
|
||
### 8.1. Клонирование
|
||
|
||
Правило:
|
||
|
||
- не клонировать данные без необходимости;
|
||
- клон должен быть осознанным, а не способом обойти borrow checker без понимания причины.
|
||
|
||
### 8.2. Shared mutability
|
||
|
||
Правило:
|
||
|
||
- не использовать `Arc<Mutex<_>>` как универсальный контейнер состояния;
|
||
- shared mutability допускается только там, где она действительно нужна по архитектуре.
|
||
|
||
### 8.3. ID types
|
||
|
||
Идентификаторы должны быть отдельными типами, а не просто `String`.
|
||
|
||
Примеры:
|
||
|
||
- `OperationId`
|
||
- `DescriptorId`
|
||
- `AuthProfileId`
|
||
|
||
## 9. Async rules
|
||
|
||
### 9.1. Где допускается `async`
|
||
|
||
`async` используется только там, где есть:
|
||
|
||
- I/O;
|
||
- network;
|
||
- storage;
|
||
- async boundary приложения.
|
||
|
||
### 9.2. Где `async` не нужен
|
||
|
||
Нельзя превращать:
|
||
|
||
- schema validation;
|
||
- mapping;
|
||
- чистую доменную логику;
|
||
- небольшие derived methods
|
||
|
||
в `async fn` без причины.
|
||
|
||
### 9.3. `tokio`
|
||
|
||
`tokio` должен находиться:
|
||
|
||
- в приложениях;
|
||
- в I/O слоях;
|
||
- в адаптерах и runtime orchestration, если там есть реальный async.
|
||
|
||
Доменный слой не должен зависеть от `tokio`.
|
||
|
||
## 10. API design rules
|
||
|
||
### 10.1. Конструкторы
|
||
|
||
Использовать:
|
||
|
||
- `new()` для гарантированно валидного и простого создания;
|
||
- `try_new()` там, где есть валидация и возможна ошибка.
|
||
|
||
### 10.2. Builders
|
||
|
||
Если структура имеет много параметров и прямой конструктор становится нечитаемым, допускается builder.
|
||
|
||
Но:
|
||
|
||
- builder не должен маскировать плохую модель данных;
|
||
- builder не должен использоваться как замена нормальной декомпозиции.
|
||
|
||
### 10.3. DTO отдельно от domain
|
||
|
||
Если HTTP payload начинает расходиться с доменной моделью, нужно вводить отдельный DTO слой.
|
||
|
||
Нельзя:
|
||
|
||
- тащить `serde`-ориентированный API payload прямо в домен только ради удобства.
|
||
|
||
## 11. Dependency rules
|
||
|
||
### 11.1. Внешние crates
|
||
|
||
Правило:
|
||
|
||
- сначала использовать `std`;
|
||
- потом существующие внутренние abstractions;
|
||
- только потом тянуть новый внешний crate.
|
||
|
||
Нельзя:
|
||
|
||
- добавлять зависимость "на всякий случай";
|
||
- дублировать crates с пересекающейся функцией без причины.
|
||
|
||
### 11.2. Макросы
|
||
|
||
Правило:
|
||
|
||
- не злоупотреблять макросами там, где обычный Rust-код читается лучше;
|
||
- derive-макросы допустимы;
|
||
- сложные процедурные макросы без сильной причины не нужны.
|
||
|
||
## 12. Serialization rules
|
||
|
||
### 12.1. JSON/YAML
|
||
|
||
Правило:
|
||
|
||
- доменная модель одна;
|
||
- `JSON` и `YAML` - только два формата сериализации;
|
||
- нельзя допускать, чтобы YAML export стал отдельной несовместимой моделью.
|
||
|
||
### 12.2. Secrets
|
||
|
||
Никогда не сериализовать:
|
||
|
||
- реальные токены;
|
||
- пароли;
|
||
- API keys
|
||
|
||
в exports, logs и test snapshots.
|
||
|
||
## 13. Тестовые практики на уровне Rust-кода
|
||
|
||
Минимально:
|
||
|
||
- unit tests рядом с модулем или в `tests`;
|
||
- integration tests для crate boundaries;
|
||
- фикстуры для schema/mapping/proto/yaml roundtrip.
|
||
|
||
Полезное правило:
|
||
|
||
- баг сначала воспроизводится тестом, потом фиксится кодом.
|
||
|
||
## 14. Что часто запрещают в Rust-командах
|
||
|
||
Практически всегда под запретом:
|
||
|
||
- `unwrap()` в production code;
|
||
- `unsafe` без review;
|
||
- giant modules;
|
||
- giant enums со всем подряд;
|
||
- giant services, где смешаны orchestration и transport;
|
||
- абстракции "на будущее" без второго реального кейса.
|
||
|
||
## 15. Практический итог
|
||
|
||
Для этого проекта правильный Rust-профиль такой:
|
||
|
||
- фиксированный toolchain;
|
||
- обязательные `fmt` и `clippy`;
|
||
- `unsafe` запрещен по умолчанию;
|
||
- panics запрещены в production code;
|
||
- ошибки типизированы;
|
||
- `pub` минимизируется;
|
||
- `async` только на реальных async boundaries;
|
||
- код читается за счет имен и декомпозиции, а не за счет комментариев.
|