7.3 KiB
Rust Library Development Guidelines
Document Version: v3
Note on Versioning:
- This document version is independent — reused across projects
- Project version source of truth:
Cargo.tomlunder[package]CHANGELOG.mduses project version fromCargo.toml
Related Documents
- README.md — Project overview, public API description, installation and usage examples
- AGENTS.md — Rules for AI assistants
- PROJECT.md — Project goals and current state
- CHANGELOG.md — Version history
Documentation Organization
All detailed documentation of features and systems belongs in the docs/ folder, not in the project root.
The API reference is generated by cargo doc from doc comments — docs/ holds the prose documentation that does not fit in doc comments.
The root directory contains only the core documents: README.md, CLAUDE.md, AGENTS.md, DESIGN_DOCUMENT_LIB.md, PROJECT.md, CHANGELOG.md.
1. Code Style
- Rust edition: 2024 (requires Rust 1.85 or newer)
- Declare the minimum toolchain in
Cargo.toml(rust-version = "1.85") so consumers get a clear error instead of a compile failure - 100-character lines (rustfmt default
max_width) - Format with rustfmt — run
cargo fmtbefore every commit - Lint with clippy — run
cargo clippy -- -D warningsbefore every commit - snake_case functions/variables/modules, PascalCase types/traits, SCREAMING_SNAKE_CASE constants
2. Cargo
cargo add <crate> # Add runtime dependency
cargo add --dev <crate> # Add dev dependency
cargo remove <crate> # Remove dependency
cargo test # Run all tests
cargo doc --open # Build and open documentation
cargo fmt # Format code
cargo clippy -- -D warnings # Lint (treat warnings as errors)
cargo build # Build to verify compilation
cargo publish # Publish to crates.io
Never edit Cargo.toml dependency versions by hand — use cargo add.
3. Project Structure
project/
├── src/
│ ├── lib.rs # Public API — re-export everything the caller needs
│ ├── error.rs # All public error types
│ └── <module>/
│ └── mod.rs
├── tests/ # Integration tests (test the public API only)
├── examples/ # Usage examples
├── docs/ # Detailed documentation
├── Cargo.toml
└── Cargo.lock # Do NOT commit — add to .gitignore
No main.rs — libraries have no entry point.
4. Public API
- Everything intended for external use must be
puband re-exported fromlib.rs - Use
pub(crate)for internal items that cross module boundaries - Internal modules should be private (
mod foo;) unless they are part of the public API - Public API must be stable across patch versions; breaking changes require a major version bump
- Use
#[non_exhaustive]on public enums and structs to allow adding fields without breaking changes
// lib.rs — public surface
pub use error::MyError;
pub use client::Client;
pub use types::{Config, Response};
5. Error Handling
- Define all public error types in
src/error.rsusing thiserror - Export all errors from
lib.rs - Never use
.unwrap()— use?or explicit handling .expect("reason")only when the invariant is guaranteed and documented
// error.rs
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MyError {
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
6. Logging
This is a library. Libraries must never configure logging sinks — that is the responsibility of the consuming application.
Use tracing for instrumentation:
[dependencies]
tracing = "0.1"
use tracing::{debug, info, warn, error};
pub fn do_something() {
debug!("internal detail");
info!("milestone reached");
}
Never call tracing_subscriber::fmt().init() or any sink setup inside library code. The consuming application configures the subscriber.
Log levels
| Level | When to use |
|---|---|
debug |
Per-item detail: internal state, cache hits |
info |
Significant milestones visible to the consuming application |
warn |
Recoverable issues: fallback used, unexpected but non-fatal |
error |
Failures the caller must know about |
7. Environment and Secrets
Libraries do not read environment variables or .env files. Configuration is passed by the caller via arguments or constructor parameters.
8. Testing
- Use Rust's built-in test framework —
#[test]and#[cfg(test)] - Unit tests live in the same file as the code, in a
mod testsblock - Integration tests in
tests/test only the public API (as a real consumer would) - Test naming:
test_<action>_<context>
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_input() {
assert_eq!(parse("42"), Ok(42));
}
}
9. Documentation
All public items must have doc comments. Use cargo doc --open to verify locally.
/// Parses a value from a string.
///
/// # Errors
///
/// Returns [`MyError::InvalidInput`] if the string is not a valid value.
///
/// # Examples
///
/// ```
/// use my_crate::parse;
/// assert_eq!(parse("42"), Ok(42));
/// ```
pub fn parse(s: &str) -> Result<u32, MyError> { ... }
README.md must contain installation instructions and usage examples for the public API.
10. Tooling
| Tool | Purpose |
|---|---|
| rustfmt | Code formatting |
| clippy | Linting |
| cargo test | Testing |
| cargo doc | Documentation |
Run before every commit:
cargo fmt
cargo clippy -- -D warnings
cargo test
11. Distribution
Build and publish with Cargo:
cargo package # Verify what will be published
cargo publish # Publish to crates.io (requires login)
Cargo.lock is not committed — add to .gitignore.
12. Versioning
- Follow semantic versioning:
MAJOR.MINOR.PATCH - Version is defined in
Cargo.tomlunder[package] - Always ask before bumping the version — never increment automatically
- Update
CHANGELOG.mdbefore bumping the version - Breaking changes to the public API require a major version bump
13. Documentation and Task Management
- Keep
PROJECT.mdandCHANGELOG.mdup to date when making changes - Document architectural changes in this file or in
docs/ README.mdmust contain installation instructions and usage examples for the public API
Task notation
Tasks are written as single-line comments directly in code, using the Todo Tree tags defined in AGENTS.md (TODO, FIXME, BUG, HACK, NOTE). PROJECT.md carries only cross-cutting tasks that have no single place in the code.
// TODO: extract this into a separate module
// FIXME: panics on an empty slice
// BUG: off-by-one when the buffer is exactly full
// HACK: temporary workaround until the crate adds paging
// NOTE: order matters here, the parser is stateful
No other task format is used — no checkboxes, no numbered lists in documentation.
If a tag already exists at a specific location in code, do not repeat it in PROJECT.md.