Подготовка проекта к публикации: структура Rust-кейса (crate, examples, features)
Как оформить Rust-проект так, чтобы его было удобно использовать другим: возможности (features), примеры, документация и стабильный public API.
Содержание
Подготовка проекта к публикации: структура Rust-кейса (crate, examples, features)
Публикация Rust-проекта — это не только cargo publish. Для пользователей важнее другое: насколько предсказуемо и удобно ваш crate подключается, как быстро они понимают, что он умеет, и можно ли использовать его безопасно в своих сборках. Хорошо оформленный проект превращается в «модуль», который не требует от читателя чтения всего исходного кода.
В этой статье разберём, как структурировать Rust-кейс (crate) для публикации и дальнейшего использования другими. Ключевые темы: стабильный public API, каталог examples, документирование, а также features как инструмент управления возможностями без форков. В конце — практический чеклист и рекомендации, куда углубиться; для этого естественно подойдет курс Rust.
Почему структура проекта важнее «красивой упаковки»
Rust в экосистеме построен на контракте между автором и пользователем. Контракт формируется тремя вещами:
- API поверхности — какие типы, функции и трейты вы объявляете
pubи как поддерживаете совместимость. - Вариативность сборки — что и как меняется при включении
features. - Контент вокруг кода — документация (
rustdoc), примеры (examples), README, подсказки по использованию.
Если проект не подготовлен, пользователь неизбежно попадает в цикл: «скачал — разобрался на уровне исходников — не уверен, что правильно — не использовал». Наша задача — разорвать этот цикл ещё до публикации.
Базовая структура crate: src, Cargo.toml и контракт
lib vs bin: когда нужен библиотечный crate
Если ваша цель — чтобы другие подключали код, вам обычно нужен библиотечный crate (lib). Даже если у вас есть CLI-утилита, часто разумно выделить ядро в lib и использовать его из bin.
Типовой случай:
src/lib.rs— публичное API библиотекиsrc/main.rs— бинарник (если он нужен)
Для публикации на crates.io ориентируйтесь в первую очередь на lib.
Выделение модулей и контроль public API
Пример структуры:
mycrate/
Cargo.toml
src/
lib.rs
api.rs
error.rs
client.rs
В lib.rs лучше собрать публичные элементы и скрыть детали реализации через mod и pub(crate):
// src/lib.rs
mod api;
mod error;
mod client;
pub use api::{Request, Response, send_request};
pub use error::MyError;
Так пользователь видит понятный набор точек входа. Внутренние модули можно менять без обязательства сохранить конкретную файловую структуру. В Rust это критически важно: сохраняйте совместимость интерфейса, а не структуры.
Стабильность и versioning: что считать «публичным API»
В Rust понятие public API обычно включает:
- экспортируемые
pubэлементы (pub fn,pub struct,pub trait,pub enum) - публичные поля и методы (
pubполя/методы уpub struct) - сигнатуры, трейт-реализации, поведение в документации (пусть и не формально)
Грубая ошибка — «считать публичным только то, что очевидно». Например, если вы экспортировали pub struct с pub полями, вы фактически закрепили структуру данных. Даже изменение формы полей — это ломающее изменение.
Рекомендация практического уровня:
- В публичных
structполя делайте приватными, а доступ — через методы. - Для изменений используйте семантическое версионирование и продумывайте deprecations.
- Не усложняйте поверхность: лучше меньше
pub, но хорошо документированных.
Каталог examples: как показывать реальные сценарии использования
Зачем examples нужны именно пользователям
Файлы в examples/ — это не «декорации» и не «демка ради демки». Это:
- способ показать правильный способ использования API
- форма тестируемой документации
- быстрый путь для тех, кто не хочет читать исходники
Кроме того, примеры можно сделать частью CI: они будут компилироваться, а вы — ловить регрессии.
Минимальный пример в examples/
examples/
basic.rs
examples/basic.rs:
use mycrate::{Request, Response, send_request};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let req = Request::new("https://example.com");
let res: Response = send_request(req)?;
println!("status: {}", res.status());
Ok(())
}
Важно: пример должен быть максимально близким к «реальному» использованию. Если ваш crate рассчитан на async, то и пример должен быть async, а не «упрощённая синхронная вставка».
Примеры как «тесты» и источник уверенности
В отличие от комментариев, примеры реально собираются как отдельные бинарники. Вы можете добавить в CI шаг:
cargo test(для unit/integration)cargo run --example basic(или простоcargo check --examples)
Даже один cargo check --examples в пайплайне снижает риск того, что пользователи наткнутся на устаревший синтаксис.
Команда для локальной проверки:
cargo check --examples
Несколько примеров под разные сценарии — но без дробления
Частая ошибка: сделать десятки примеров по мелочам. В итоге пользователь теряет ориентир.
Компромисс:
- 1–3 примера «как использовать»
- 1 пример «как настроить features» (если это существенно)
- 1 пример «как обрабатывать ошибки» (если модель ошибок сложная)
Документация: rustdoc, README и поведение API
rustdoc — основная точка контакта
Если пользователь смотрит на ваш crate, чаще всего он начинает с документации: docs.rs или встроенный cargo doc. Поэтому:
- Пишите комментарии
///для всех публичных элементов. - Указывайте контракт: что функция принимает, что возвращает, какие ошибки генерирует.
- Не ограничивайтесь «делает то-то»: объясняйте нюансы.
Пример:
/// Sends an HTTP request built from [`Request`].
///
/// # Errors
/// Returns [`MyError`] if the request fails or if the response body
/// cannot be decoded according to the selected parser.
///
/// # Examples
/// ```no_run
/// use mycrate::{Request, send_request};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let res = send_request(Request::new("https://example.com"))?;
/// # Ok(())
/// # }
/// ```
pub fn send_request(req: Request) -> Result<Response, MyError> {
// ...
unimplemented!()
}
Важно не «переполнить» документацию; лучше сделать несколько качественных точек входа.
README: что писать в первую очередь
README — это обзор, а не повтор rustdoc. Он нужен, чтобы:
- быстро показать назначение crate
- объяснить установку (
Cargo.toml) - привести 1–2 минимальных примера использования
- указать, как включать features
Структура README, которая обычно работает:
- Короткое описание (1–3 абзаца)
- Установка (
cargo add/ зависимость вCargo.toml) - Quickstart кодом (1 пример)
- Features и что они меняют
- Ссылка на документацию / примеры
- Поддержка и ограничения (например, MSRV — минимальная версия Rust)
MSRV: недооцененный аспект подготовки
Минимальная поддерживаемая версия Rust (MSRV) — часть «контракта». Если вы публикуете crate без явного MSRV, пользователи могут столкнуться с неожиданными сборками.
Практика: зафиксируйте MSRV в документации и, по возможности, в метаданных (rust-version в Cargo.toml, если вы используете это поле).
Пример:
[package]
name = "mycrate"
version = "0.3.0"
edition = "2021"
rust-version = "1.74"
Features: управление возможностями без ломания API
Как features влияют на совместимость
features в Cargo — инструмент, который управляет тем, что встраивается в сборку: включаются дополнительные зависимости, меняется реализация, добавляются методы или поведение.
Важное различие:
- Публичное API должно быть стабильным.
- Семантика и доступность некоторых возможностей могут зависеть от features, но это нужно честно описывать.
Если вы делаете так, что при выключенном feature исчезает тип или функция, пользователи будут вынуждены учитывать комбинации и читать документацию.
Рекомендации по дизайну features
-
Делайте features «оси» конфигурации, а не «переключатели ради переключателей». Примеры осей:
- поддержка конкретного backend'а (например,
tokiovsasync-std) - выбор парсера (
jsonvsyaml) - включение интеграции со сторонними системами
- поддержка конкретного backend'а (например,
-
Не превращайте features в скрытую архитектуру, где половина crate существует только при включении флагов, а другая половина — при выключении. Лучше:
- базовое ядро всегда компилируется
- расширения добавляются через features
-
Не меняйте публичные типы радикально между feature-сценариями. Если тип меняется, то вы ломаете ожидания даже при одинаковых версиях.
Пример: feature «serde» как стандартная практика
Многие Rust-crates предлагают serde feature, чтобы не заставлять всех тащить зависимость.
В Cargo.toml:
[dependencies]
serde = { version = "1", optional = true }
[features]
default = []
serde = ["dep:serde"]
В коде:
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub struct MyType {
value: String,
}
#[cfg(feature = "serde")]
#[derive(Serialize, Deserialize)]
pub struct MyTypeSerdeView {
value: String,
}
Лучше экспортировать отдельный тип/функции для сериализации, если это не является частью ядра. Если же вы действительно хотите, чтобы пользователь получил Serialize/Deserialize прямо на вашем типе, тогда аккуратно делайте derive:
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MyType {
value: String,
}
Смысл: вы контролируете контракт, но делаете его предсказуемым через документацию.
Документирование features в публичной части
Каждое feature должно быть описано. Часто это делают в README, но полезно и в Cargo.toml комментариями. Однако комментарии в Cargo.toml не индексируются как документация. Поэтому минимум:
- секция в README «Features»
- упоминание в
rustdoc(вlib.rsможно сделать краткий обзор)
Пример раздела в lib.rs:
//! # Crate mycrate
//!
//! ## Features
//! - `serde`: adds serialization support.
//! - `tokio`: enables Tokio integration for async networking.
Практика public API: как проектировать «правильный экспорт»
Минимальная поверхность: pub use вместо «раскрытия всего»
Нередко авторы открывают всю структуру через pub mod .... Это удобно на старте, но потом становится сложно менять внутренности: пользователь начинает зависеть от модулей и путей.
Вместо:
pub mod client;
pub mod error;
Лучше:
mod client;
mod error;
pub use client::Client;
pub use error::MyError;
Так вы можете в будущем переместить реализацию между модулями, не ломая mycrate::Client.
Ошибки и типы: продумайте, что будет «видно»
Модель ошибок — часть UX. Пользователь ожидает:
- понятный тип ошибки (
MyError) - реалистичное поведение (что именно считается ошибкой)
- корректные трейты (
std::error::Error,Display)
Пример каркаса:
use std::fmt;
#[derive(Debug)]
pub enum MyError {
Network(reqwest::Error),
InvalidResponse(String),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::Network(e) => write!(f, "network error: {e}"),
MyError::InvalidResponse(msg) => write!(f, "invalid response: {msg}"),
}
}
}
impl std::error::Error for MyError {}
Если вы используете thiserror, это можно сделать короче, но в любом случае важно: тип ошибки должен быть стабильным и документированным.
Сборка и проверка перед публикацией
cargo fmt, cargo clippy, cargo test как минимум
Перед релизом полезно прогнать:
- форматирование:
cargo fmt --check - линтер:
cargo clippy --all-targets --all-features - тесты:
cargo test
И отдельно — проверку примеров:
cargo check --examples
Если features существенно меняют код, проверяйте несколько наборов:
--no-default-features--all-features
Это не «идеальность», а способ обнаружить типовые проблемы: пропавшие impl’ы, некорректные cfg-гейты, отсутствие зависимостей в нужном feature.
Не забывайте про документы и примеры в rustdoc
Rustdoc-диагностический режим может ловить ошибки в code blocks.
Для проверки:
cargo test --doc
Это особенно полезно, если вы активно используете ```rust в документации. Если примеры не компилируются, пользователи увидят это на этапе чтения или сборки документации, но лучше поймать заранее.
Типичные подводные камни при подготовке к публикации
1) «Работает у меня» вместо reproducible build
Если crate полагается на внешние ресурсы (файлы, схемы, URL), убедитесь, что:
- для примеров нет скрытых предположений
- интеграции с сетью явно описаны
- тесты не зависят от состояния сети, если это не часть намерения
Иначе cargo test и cargo check --examples становятся нестабильными.
2) Features, которые меняют типы «не предупреждая»
Если поведение или наличие методов зависит от feature, пользователю нужен ориентир. Иначе он получит компиляционные ошибки при интеграции.
Минимум: описывайте в README и в документации, какие features нужны для каких API.
3) Публичные поля у структур
Открытые поля (pub field) часто делают вашу структуру «замороженной». Лучше предоставить методы или конструкторы, а внутреннюю представляемость менять.
4) Отсутствие интеграционных тестов (кроме unit)
Если crate ориентирован на внешнее использование, интеграционные тесты позволяют моделировать реальные сценарии потребителя: комбинация features, работа с типами, обработка ошибок. Даже 1–2 теста на ключевые сценарии окупаются.
Чеклист релиза: как понять, что проект готов другим
Ниже — короткая практическая выжимка, по которой можно пройтись перед публикацией.
Crate и API
- У
lib.rsесть чёткий наборpubэкспортируемых сущностей. - Публичные типы документированы (
///), включая поведение и ошибки. - Структуры не «закрепляют» внутренности через
pubполя, где это не нужно. - Присутствует последовательная модель ошибок (тип ошибки стабилен и понятен).
Examples
- Есть хотя бы один пример «quickstart» в
examples/. - Примеры компилируются:
cargo check --examples. - Если функциональность feature-специфична — пример учитывает это или вынесен отдельно.
Features
- features описаны в README и/или в документации.
- базовое ядро собирается без включения обязательных features (если это логично).
-
cargo check --all-featuresиcargo check --no-default-featuresпроходят (или есть осмысленные ограничения, которые вы документировали).
Документация
- README содержит установку и минимальный пример.
- rustdoc показывает примеры и контракты (включая ошибки).
-
cargo test --docне падает (если вы используете code blocks).
Вывод
Подготовка Rust-проекта к публикации — это дисциплина построения интерфейса. Структура crate определяет «контракт» с пользователем, каталог examples отвечает за обучение через практику, а features дают гибкость без необходимости разветвлять код на отдельные репозитории. И самое важное: всё это работает только вместе с документацией и вниманием к стабильности public API.
Если вы хотите глубже разобраться не только в том, как расположить файлы, но и в том, как проектировать интерфейсы и совместимость в Rust на уровне практик — полезным следующим шагом может стать курс Rust, где подобные темы обычно разбираются системно: от композиции модулей до нюансов features и техники документации.
Комментарии
Пока нет комментариев