Loading...
Loading...
Rust project implementation guide for multi-crate workspace projects. Covers workspace config, toolchain (nightly + rustfmt + clippy + cranky + cargo-deny), strict lint rules (no unsafe/unwrap/expect/panic), error handling (thiserror + anyhow), async runtime (Tokio), TLS (rustls + aws-lc-rs), CI/CD (GitHub Actions with test/build/docker/SBOM), and coding conventions. Use when scaffolding, developing, or reviewing Rust applications.
npx skill4agent add zzci/skills pma-rust| Category | Technology | Notes |
|---|---|---|
| Core | ||
| Language | Rust | edition 2024, nightly toolchain |
| Build | Cargo workspaces | multi-crate monorepo |
| Task runner | just | command runner |
| Async & HTTP | ||
| Runtime | Tokio | full features |
| HTTP server | Axum 0.8 | multipart, middleware, graceful shutdown |
| HTTP client | reqwest 0.12 | rustls-tls, no openssl |
| Data | ||
| ORM | Diesel 2 | r2d2 pool, feature-gated backend |
| Concurrent cache | DashMap 6 | lock-free concurrent HashMap |
| CLI | ||
| Argument parsing | clap 4 | derive macros, subcommands |
| Error Handling | ||
| Typed errors | thiserror 2 | per-crate error enums |
| Error propagation | anyhow 1.0 | boundary crossing |
| Serialization | ||
| Serde | serde 1.0 + serde_json + toml | derive |
| Linting | ||
| Format | rustfmt | edition 2024 |
| Lint | clippy + cargo-cranky | strict deny rules |
| Dependency audit | cargo-deny | license, ban, advisory |
| Security | ||
| TLS | rustls 0.23 | aws-lc-rs provider, no openssl/ring |
| Token comparison | subtle 2 | constant-time to prevent timing attacks |
Cargo.toml # [workspace] root
Cargo.lock
rust-toolchain # nightly-YYYY-MM-DD
rustfmt.toml
clippy.toml
Cranky.toml # cargo-cranky lint config
deny.toml # cargo-deny config
justfile # task runner
.cargo/
config.toml # rustflags
.github/
workflows/
ci.yml
docs/
architecture.md
changelog.md
task/
plan/
crates/
app/ # main binary crate
src/
main.rs
commands/
core/ # runtime services, state, DB
src/
services.rs # DI container
db/
protocols/
common/ # shared types, config, errors
src/
config/
error.rs
types/
helpers/
protocol-xxx/ # per-protocol crate
src/
lib.rs
db/ # Diesel ORM + migrations
src/
lib.rs
schema.rs
models.rs
migrations/
diesel.toml
tests/ # integration tests (TypeScript/Bun)[workspace]
resolver = "2"
members = ["crates/*"]
default-members = ["crates/app"]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
[workspace.dependencies]
# Pin shared dependencies here
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "time", "local-time", "ansi"] }
clap = { version = "4", features = ["derive"] }
axum = { version = "0.8", features = ["multipart"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
diesel = { version = "2", features = ["r2d2"] }
diesel_migrations = "2"
dashmap = "6"
subtle = "2"
[profile.release]
lto = true
panic = "abort"
strip = "debuginfo"nightly-YYYY-MM-DD[target.'cfg(all())']
rustflags = [
"--cfg", "tokio_unstable",
"-Zremap-cwd-prefix=/reproducible-cwd",
"--remap-path-prefix=$HOME=/reproducible-home",
"--remap-path-prefix=$PWD=/reproducible-pwd",
]tokio_unstableimports_granularity = "Module"
group_imports = "StdExternalCrate"avoid-breaking-exported-api = false
allow-unwrap-in-tests = true[cranky]
deny = [
"unsafe_code",
"clippy::unwrap_used",
"clippy::expect_used",
"clippy::panic",
"clippy::indexing_slicing",
"clippy::dbg_macro",
]
allow = [
"clippy::result_large_err",
]unsafeunwrapexpectpanic#[allow(...)][bans]
deny = [
{ crate = "openssl-sys", use-instead = "rustls" },
]
[licenses]
allow = [
"MIT", "Apache-2.0", "ISC",
"BSD-2-Clause", "BSD-3-Clause",
"Zlib", "CC0-1.0",
]thiserror#[from]#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sea_orm::DbErr),
#[error("config error: {0}")]
Config(#[from] ConfigError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}anyhow::Result<T>Secret<T>Debugpub struct Secret<T>(T);
impl<T> std::fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<secret>")
}
}pub struct Services {
pub db: Arc<Mutex<DatabaseConnection>>,
pub config: Arc<Mutex<AppConfig>>,
pub state: Arc<Mutex<State>>,
}Arc<Mutex<T>>ServicesArcpub trait ProtocolServer {
fn name(&self) -> &'static str;
fn run(self, address: ListenEndpoint) -> impl Future<Output = Result<()>> + Send;
}enum_dispatchBox<dyn Trait>tokio::sync::Mutex#[cfg(debug_assertions)]
pub async fn lock(&self) -> MutexGuard<'_, T> {
match tokio::time::timeout(Duration::from_secs(5), self.inner.lock()).await {
Ok(guard) => guard,
Err(_) => panic!("deadlock detected on mutex: {}", self.name),
}
}crates/
db/ # @project/db
src/
lib.rs
schema.rs # diesel print-schema output
models.rs # Queryable/Insertable structs
migrations/
00000000000000_create_xxx/
up.sql
down.sql
diesel.toml[print_schema]
file = "src/schema.rs"
[migrations_directory]
dir = "src/migrations"[features]
default = ["sqlite"]
postgres = ["diesel/postgres"]
mysql = ["diesel/mysql"]
sqlite = ["diesel/sqlite"]diesel::r2d2use diesel::r2d2::{self, ConnectionManager};
pub type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
pub fn establish_pool(database_url: &str) -> DbPool {
let manager = ConnectionManager::<SqliteConnection>::new(database_url);
r2d2::Pool::builder()
.max_size(10)
.build(manager)
.expect("failed to create pool")
}use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/migrations");
pub fn run_migrations(conn: &mut impl MigrationHarness<diesel::sqlite::Sqlite>) {
conn.run_pending_migrations(MIGRATIONS)
.expect("failed to run migrations");
}# Install diesel CLI
cargo install diesel_cli --no-default-features --features sqlite
# Setup (creates diesel.toml + migrations dir)
diesel setup
# Generate migration
diesel migration generate create_users
# Run migrations
diesel migration run
# Print schema
diesel print-schema > src/schema.rsserde::Deserialize#[serde(default)]use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub database: DatabaseConfig,
#[serde(default)]
pub logging: LoggingConfig,
}
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
fn default_host() -> String { "0.0.0.0".into() }
fn default_port() -> u16 { 3000 }
impl Default for ServerConfig {
fn default() -> Self {
Self { host: default_host(), port: default_port() }
}
}impl AppConfig {
pub fn load(path: &str) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: AppConfig = toml::from_str(&content)?;
Ok(config)
}
/// Auto-generate default config if file does not exist, then exit.
pub fn ensure_exists(path: &str) -> anyhow::Result<()> {
if !std::path::Path::new(path).exists() {
let default = toml::to_string_pretty(&AppConfig::default())?;
std::fs::write(path, default)?;
eprintln!("generated default config at {path}, please edit and restart");
std::process::exit(0);
}
Ok(())
}
}let config_path = std::env::var("APP_CONFIG").unwrap_or_else(|_| "config.toml".into());
AppConfig::ensure_exists(&config_path)?;
let config = AppConfig::load(&config_path)?;# config.toml
[server]
host = "0.0.0.0"
port = 3000
[database]
url = "sqlite://data.db"
max_connections = 10
[logging]
level = "info"
format = "text" # "text" or "json"#[serde(default)]#[serde(default = "fn_name")]Default#[serde(default)]APP_CONFIGconfig.tomlclapuse clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "myapp", about = "Application description")]
pub struct Cli {
/// Config file path
#[arg(short, long, default_value = "config.toml")]
pub config: String,
/// Enable verbose logging
#[arg(short, long)]
pub verbose: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Start the server
Run,
/// Check configuration validity
Check,
/// Print version info
Version,
/// Generate default config file
Init,
}use clap::Parser;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Run => {
let config = AppConfig::load(&cli.config)?;
start_server(&config).await?;
}
Command::Check => {
let _config = AppConfig::load(&cli.config)?;
println!("config OK");
}
Command::Version => {
println!("{}", env!("CARGO_PKG_VERSION"));
}
Command::Init => {
AppConfig::ensure_exists(&cli.config)?;
}
}
Ok(())
}#[derive(Parser)]#[derive(Subcommand)]///--help#[arg(short, long)]#[arg(default_value = "...")]crates/app/src/cli.rsmain.rsuse tokio::signal;
pub async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => tracing::info!("received Ctrl+C, shutting down"),
_ = terminate => tracing::info!("received SIGTERM, shutting down"),
}
}#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = AppConfig::load()?;
// Start server with graceful shutdown
let server = start_server(&config);
tokio::select! {
result = server => result?,
_ = shutdown_signal() => {
tracing::info!("graceful shutdown complete");
}
}
Ok(())
}use axum::{Router, routing::{get, post}, middleware};
pub fn create_router(state: AppState) -> Router {
Router::new()
// Public routes
.route("/health", get(health))
// Protected API routes
.nest("/api/v1", api_routes()
.layer(middleware::from_fn(verify_token)))
.with_state(Arc::new(state))
}pub struct AppState {
pub db: DbPool,
pub config: AppConfig,
}State(state): State<Arc<AppState>>async fn get_item(
State(state): State<Arc<AppState>>,
Path(id): Path<i32>,
) -> Result<Json<Item>, AppError> {
let conn = &mut state.db.get()?;
let item = items::table.find(id).first(conn)?;
Ok(Json(item))
}subtle::ConstantTimeEquse subtle::ConstantTimeEq;
async fn verify_token(
headers: HeaderMap,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let token = headers.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "));
match token {
Some(t) if t.as_bytes().ct_eq(expected.as_bytes()).into() => {
Ok(next.run(request).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_signal())
.await?;subtle::ConstantTimeEq==use std::net::IpAddr;
fn is_private_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
v4.is_private() || v4.is_loopback() || v4.is_link_local()
|| v4.is_broadcast() || v4.is_unspecified()
}
IpAddr::V6(v6) => v6.is_loopback() || v6.is_unspecified(),
}
}
fn validate_webhook_url(url: &str) -> anyhow::Result<()> {
let host = url::Url::parse(url)?.host_str()
.ok_or_else(|| anyhow::anyhow!("no host"))?
.to_string();
let addrs = std::net::ToSocketAddrs::to_socket_addrs(
&(host.as_str(), 443)
)?;
for addr in addrs {
anyhow::ensure!(!is_private_ip(addr.ip()), "private IP not allowed");
}
Ok(())
}DashMapuse dashmap::DashMap;
pub struct Cache {
inner: DashMap<String, CachedItem>,
}
impl Cache {
pub fn get(&self, key: &str) -> Option<CachedItem> {
self.inner.get(key).map(|v| v.clone())
}
pub fn insert(&self, key: String, value: CachedItem) {
self.inner.insert(key, value);
}
}MutexDashMaptracing-subscriber--log-format jsonLayer{crate_prefix}jobs:
# Gate 1: format must pass before anything else runs
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo fmt --check
# Gate 2: parallel checks after format passes
clippy:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo cranky --all-targets -- -D warnings
test:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: dtolnay/rust-toolchain@<sha>
- run: cargo test --all-features
deny:
needs: [fmt]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- uses: EmbarkStudios/cargo-deny-action@<sha>fmt| Job | Command | Gate |
|---|---|---|
| Format | | Must pass |
| Lint | | Must pass, zero warnings |
| Deny | | No banned deps, no license violations |
| Test | | Must pass |
| Build | | Must succeed |
# Common tasks
check:
cargo cranky --all-features
cargo test --all-features
cargo fmt --check
cargo deny check
fmt:
cargo fmt
build:
cargo build --all-features --release
config-schema:
cargo run -p app -- config-schema > config-schema.json
openapi:
cargo run -p admin -- openapi > openapi.json| Area | Convention |
|---|---|
| Crate naming | |
| Imports | Module granularity, grouped std/external/local |
| Error types | Per-crate |
| No unsafe | Enforced by cranky deny rule |
| No unwrap/expect | Enforced by cranky; use |
| Shared state | |
| Secrets | |
| Token auth | |
| TLS | rustls + aws-lc-rs only; openssl banned |
| HTTP server | Axum 0.8 with middleware + graceful shutdown |
| Concurrent cache | DashMap — no Mutex needed for shared caches |
| Database | Diesel 2 + r2d2 pool; feature-gated backend (sqlite default) |
| CLI | clap 4 derive — |
| Config | TOML + |
| Shutdown | Tokio signal handler; SIGINT + SIGTERM graceful shutdown |
| Reproducible builds | Path remapping via rustflags |
| Release profile | |