Customization
Customization Guide
Section titled “Customization Guide”This guide covers how to customize the rust-template beyond the initial setup. For basic configuration (renaming the crate, updating metadata), see the main README.
Table of Contents
Section titled “Table of Contents”- Adding New Modules
- Removing Example Code
- Library-Only vs Binary Crate
- Adjusting Lint Strictness
- Adjusting Supply Chain Policy
- Modifying Release Targets
- Docker Customization
- Adding Property-Based Tests
1. Adding New Modules
Section titled “1. Adding New Modules”Create the module file
Section titled “Create the module file”Create a new file under crates/. For example, crates/parser.rs:
//! Parser module for rust_template.
use crate::{Error, Result};
/// Parses the given input string into a structured value.////// # Arguments////// * `input` - The raw input to parse.////// # Returns////// The parsed output.////// # Errors////// Returns [`Error::InvalidInput`] if the input is malformed.////// # Examples////// ```rust/// use rust_template::parser::parse;////// let result = parse("valid input")?;/// assert!(!result.is_empty());/// # Ok::<(), rust_template::Error>(())/// ```pub fn parse(input: &str) -> Result<String> { if input.is_empty() { return Err(Error::InvalidInput("input cannot be empty".to_string())); } Ok(input.to_string())}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_parse_valid() { let result = parse("hello"); assert!(result.is_ok()); assert_eq!(result.unwrap(), "hello"); }
#[test] fn test_parse_empty() { let result = parse(""); assert!(matches!(result, Err(Error::InvalidInput(_)))); }}Register the module in crates/lib.rs
Section titled “Register the module in crates/lib.rs”Add a pub mod declaration at the top of crates/lib.rs:
pub mod parser;Add integration tests
Section titled “Add integration tests”Create or extend a file in tests/. For example, tests/parser_test.rs:
use rust_template::parser::parse;
#[test]fn test_parse_integration() { let result = parse("test input"); assert!(result.is_ok());}Checklist
Section titled “Checklist”- Module file created in
crates/ -
pub modadded tocrates/lib.rs - All public items have doc comments with
# Examples - Unit tests in
#[cfg(test)]module within the file - Integration tests added to
tests/ -
cargo testpasses -
cargo clippy --all-targets --all-featurespasses -
cargo doc --no-depsbuilds without warnings
2. Removing Example Code
Section titled “2. Removing Example Code”The template ships with placeholder functions (add, divide) and a Config builder to demonstrate patterns. Once you are ready to add your own code, remove them.
Step 1: Clean up crates/lib.rs
Section titled “Step 1: Clean up crates/lib.rs”Remove the add and divide functions, the Config struct and its impl blocks, and the entire #[cfg(test)] mod tests block. Keep the Error enum and the Result type alias — you will likely need them.
Your crates/lib.rs should look like this after cleanup:
#![doc = include_str!("../README.md")]
use thiserror::Error;
/// Error type for `rust_template` operations.#[derive(Error, Debug)]pub enum Error { /// Invalid input was provided. #[error("invalid input: {0}")] InvalidInput(String),
/// An operation failed. #[error("operation '{operation}' failed: {cause}")] OperationFailed { /// The operation that failed. operation: String, /// The underlying cause. cause: String, },}
/// Result type alias for `rust_template` operations.pub type Result<T> = std::result::Result<T, Error>;
// Add your modules and public API here.Step 2: Update or remove crates/main.rs
Section titled “Step 2: Update or remove crates/main.rs”If you keep a binary target, rewrite crates/main.rs to remove references to the example functions:
//! Binary entry point for `rust_template`.
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::process::ExitCode;
fn run() -> Result<(), rust_template::Error> { // Your application logic here Ok(())}
fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("Error: {e}"); ExitCode::FAILURE }, }}If you do not need a binary, see Library-Only vs Binary Crate below.
Step 3: Clean up integration tests
Section titled “Step 3: Clean up integration tests”Remove or rewrite tests/integration_test.rs. Delete the example tests that reference add, divide, and Config, and replace them with tests for your own API.
Step 4: Update README.md
Section titled “Step 4: Update README.md”Remove the example usage snippets in README.md that reference add, divide, and Config. Replace them with documentation for your own public API.
3. Library-Only vs Binary Crate
Section titled “3. Library-Only vs Binary Crate”Removing the binary target
Section titled “Removing the binary target”If your crate is library-only, remove the binary configuration:
-
Delete
crates/main.rs. -
Remove the
[[bin]]section fromCargo.toml:# Delete these lines:[[bin]]name = "rust_template"path = "crates/main.rs" -
Remove the binary-related steps from the Docker and release workflows if you do not need them.
Adding additional binaries
Section titled “Adding additional binaries”Add extra [[bin]] sections to Cargo.toml:
[[bin]]name = "rust_template"path = "crates/main.rs"
[[bin]]name = "rust_template_cli"path = "crates/cli.rs"Each binary gets its own entry point file under crates/. Remember to add #![allow(clippy::print_stdout, clippy::print_stderr)] at the top of each binary file since the lint configuration forbids print macros in library code.
Using a workspace for multiple crates
Section titled “Using a workspace for multiple crates”For larger projects, convert to a Cargo workspace. Replace the top-level Cargo.toml with a workspace manifest:
[workspace]resolver = "2"members = [ "crates/core", "crates/cli", "crates/server",]
[workspace.package]edition = "2024"rust-version = "1.92"license = "MIT"repository = "https://github.com/zircote/rust-template"
[workspace.lints.rust]unsafe_code = "forbid"missing_docs = "warn"
[workspace.lints.clippy]all = { level = "warn", priority = -1 }pedantic = { level = "warn", priority = -1 }nursery = { level = "warn", priority = -1 }unwrap_used = "deny"expect_used = "deny"panic = "deny"Each member crate then has its own Cargo.toml that inherits workspace settings:
[package]name = "rust_template_core"version = "0.1.0"edition.workspace = truerust-version.workspace = truelicense.workspace = true
[lints]workspace = true4. Adjusting Lint Strictness
Section titled “4. Adjusting Lint Strictness”Lint groups in Cargo.toml
Section titled “Lint groups in Cargo.toml”The template enables four Clippy lint groups in [lints.clippy]:
| Group | What it covers |
|---|---|
all | Standard Clippy lints (correctness, style, complexity, performance) |
pedantic | Stricter, opinionated lints (naming conventions, API design, documentation) |
nursery | Experimental lints that may have false positives |
cargo | Cargo manifest issues (missing fields, feature misuse) |
To relax the overall strictness, remove a group or change its level:
[lints.clippy]all = { level = "warn", priority = -1 }pedantic = { level = "warn", priority = -1 }# nursery = { level = "warn", priority = -1 } # Commented out to disablecargo = { level = "warn", priority = -1 }Allowing specific lints per item
Section titled “Allowing specific lints per item”Use #[allow()] attributes to suppress a lint for a specific function, struct, or module:
#[allow(clippy::cast_possible_truncation)]fn to_u32(value: u64) -> u32 { value as u32}For an entire module:
#[allow(clippy::too_many_lines)]mod complex_module { // ...}Denied lints
Section titled “Denied lints”The template denies these lints at the crate level:
unwrap_used = "deny" # Use Result and ? insteadexpect_used = "deny" # Use Result and ? insteadpanic = "deny" # Never panic in library codetodo = "deny" # Complete all implementationsunimplemented = "deny" # Complete all implementationsdbg_macro = "deny" # Remove debug macros before committingprint_stdout = "deny" # Use tracing or log insteadprint_stderr = "deny" # Use tracing or log insteadTo relax any of these, change "deny" to "warn" or "allow":
todo = "warn" # Allow TODOs with a warningNote that print_stdout and print_stderr are already allowed in crates/main.rs via file-level attributes.
Why the binary exempts the print lints: binary entry points typically need to write output to stdout/stderr, so the file-level
#[allow]incrates/main.rsis intentional. Library code stays subject to the deny.
clippy.toml thresholds
Section titled “clippy.toml thresholds”The clippy.toml file controls numeric thresholds for various lints:
| Setting | Default | Purpose |
|---|---|---|
cognitive-complexity-threshold | 25 | Maximum cognitive complexity per function |
excessive-nesting-threshold | 4 | Maximum nesting depth |
too-many-lines-threshold | 100 | Maximum function length |
too-many-arguments-threshold | 7 | Maximum function parameters |
max-struct-bools | 3 | Maximum bool fields in a struct |
max-fn-params-bools | 3 | Maximum bool parameters in a function |
pass-by-value-size-limit | 256 | Byte threshold to warn about passing large types by value |
type-complexity-threshold | 250 | Threshold for overly complex types |
Adjust these to match your project’s needs. For example, to allow longer functions:
too-many-lines-threshold = 200Test code is exempt from several strict lints via these settings in clippy.toml:
allow-dbg-in-tests = trueallow-expect-in-tests = trueallow-unwrap-in-tests = trueallow-print-in-tests = truerustfmt.toml options
Section titled “rustfmt.toml options”Key formatting settings you may want to adjust:
| Setting | Default | Purpose |
|---|---|---|
max_width | 100 | Maximum line width |
tab_spaces | 4 | Spaces per indentation level |
imports_granularity | "Crate" | How imports are grouped (Crate, Module, Item, One) |
group_imports | "StdExternalCrate" | Import grouping order (std, external, crate) |
wrap_comments | true | Wrap long comments to fit comment_width |
trailing_comma | "Vertical" | Add trailing commas in multi-line constructs |
edition | "2024" | Rust edition for parsing |
To change line width to 120 characters:
max_width = 120comment_width = 1205. Adjusting Supply Chain Policy
Section titled “5. Adjusting Supply Chain Policy”The deny.toml file configures cargo-deny to audit your dependency tree.
Adding allowed licenses
Section titled “Adding allowed licenses”The [licenses] section lists allowed SPDX license identifiers. To add a new license:
[licenses]allow = [ "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Zlib", "MPL-2.0", "Unicode-DFS-2016", "Unicode-3.0", "CC0-1.0", "BSL-1.0", "0BSD", "LGPL-3.0", # <-- Added]For crates with non-standard license files, add a [[licenses.clarify]] entry:
[[licenses.clarify]]name = "some_crate"expression = "MIT AND BSD-2-Clause"license-files = [{ path = "LICENSE", hash = 0x12345678 }]Exempting specific advisories
Section titled “Exempting specific advisories”If a security advisory does not apply to your usage, add its ID to the ignore list:
[advisories]ignore = [ "RUSTSEC-2024-XXXX", # Reason: only affects feature X which we don't use]Always include a comment explaining why the advisory is exempted.
Banning specific crates
Section titled “Banning specific crates”The [bans] section prevents specific crates from entering your dependency tree:
[bans]deny = [ { name = "openssl", wrappers = [], reason = "Use rustls for TLS instead" }, { name = "atty", wrappers = [], reason = "Use std::io::IsTerminal instead (available in Rust 1.70+)" }, { name = "some_crate", wrappers = [], reason = "Known to be unmaintained" },]The wrappers field allows exceptions when a banned crate is only used as a transitive dependency of a specific wrapper crate:
{ name = "openssl", wrappers = ["openssl-sys"], reason = "Only allow via openssl-sys" }Configuring source restrictions
Section titled “Configuring source restrictions”By default, only crates.io is allowed as a registry source:
[sources]unknown-registry = "deny"unknown-git = "deny"allow-registry = ["https://github.com/rust-lang/crates.io-index"]allow-git = []To allow a private registry or specific git repositories:
allow-registry = [ "https://github.com/rust-lang/crates.io-index", "https://my-company.example.com/cargo/index",]
allow-git = [ "https://github.com/zircote/private-crate",]Verifying changes
Section titled “Verifying changes”After modifying deny.toml, run the checks:
cargo deny check6. Modifying Release Targets
Section titled “6. Modifying Release Targets”The release workflow (.github/workflows/release.yml) builds binaries for multiple platforms using a matrix strategy.
Default targets
Section titled “Default targets”| Target | OS Runner | Architecture |
|---|---|---|
x86_64-unknown-linux-gnu | ubuntu-latest | Linux x86_64 |
aarch64-unknown-linux-gnu | ubuntu-latest | Linux ARM64 (cross-compiled) |
x86_64-apple-darwin | macos-latest | macOS x86_64 |
aarch64-apple-darwin | macos-latest | macOS ARM64 (Apple Silicon) |
x86_64-pc-windows-msvc | windows-latest | Windows x86_64 |
Adding a target
Section titled “Adding a target”Add a new entry to the matrix.include array in release.yml:
- os: ubuntu-latest target: x86_64-unknown-linux-musl artifact_name: rust_template asset_name: rust_template-linux-musl-amd64For musl targets, you will also need to install the musl toolchain:
- name: Install musl tools if: matrix.target == 'x86_64-unknown-linux-musl' run: | sudo apt-get update sudo apt-get install -y musl-toolsRemoving a target
Section titled “Removing a target”Delete the corresponding entry from matrix.include. For example, to drop Windows support, remove:
# Delete this block:- os: windows-latest target: x86_64-pc-windows-msvc artifact_name: rust_template.exe asset_name: rust_template-windows-amd64.exeAlso remove the target from deny.toml’s [graph].targets list so supply chain checks stay aligned.
Cross-compilation requirements
Section titled “Cross-compilation requirements”Cross-compilation for aarch64-unknown-linux-gnu requires:
gcc-aarch64-linux-gnuinstalled on the runner- The
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKERenvironment variable set toaarch64-linux-gnu-gcc
Both are already configured in the workflow. For other cross-compilation targets, you will need to install the appropriate cross-compilation toolchain and set the corresponding linker environment variable.
7. Docker Customization
Section titled “7. Docker Customization”The Dockerfile uses a multi-stage build:
- Builder stage (
rust:1.92-slim) — compiles the binary with release optimizations. - Runtime stage (
gcr.io/distroless/cc-debian12) — minimal image containing only the binary.
Changing the base image
Section titled “Changing the base image”To use a different runtime base image (for example, Debian slim instead of distroless):
# Runtime stageFROM debian:bookworm-slim
RUN apt-get update && \ apt-get install -y --no-install-recommends ca-certificates && \ rm -rf /var/lib/apt/lists/*
RUN useradd --create-home appuserUSER appuser
COPY --from=builder /app/target/release/rust_template /usr/local/bin/rust_template
ENTRYPOINT ["/usr/local/bin/rust_template"]Use Debian slim if you need to debug inside the container or require additional runtime dependencies.
Why distroless by default? It contains no shell or package manager, which reduces the runtime attack surface. Switch to Debian slim only when you need to debug inside the container or link additional runtime libraries.
Adding runtime dependencies
Section titled “Adding runtime dependencies”If your binary links against shared libraries at runtime, install them in the runtime stage. With distroless, you are limited to what is available in the base image. For additional libraries, switch to a Debian-based runtime:
FROM debian:bookworm-slim
RUN apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates \ libpq5 \ && rm -rf /var/lib/apt/lists/*Modifying the builder stage
Section titled “Modifying the builder stage”The builder stage uses a dependency caching strategy: it first copies Cargo.toml and Cargo.lock, builds with dummy source files to cache dependencies, then copies the real source and rebuilds. This means dependency downloads are cached across builds as long as Cargo.toml/Cargo.lock do not change.
If you add new source directories (for example, a workspace with multiple crates), update the dummy source creation:
# Create dummy source to cache dependenciesRUN mkdir -p crates/core/src crates/cli/src && \ echo "pub fn dummy() {}" > crates/core/src/lib.rs && \ echo "fn main() {}" > crates/cli/src/main.rsAnd update the copy step:
COPY crates/ ./crates/Adding build-time dependencies
Section titled “Adding build-time dependencies”If your project needs additional system libraries at compile time, add them in the builder stage:
RUN apt-get update && \ apt-get install -y --no-install-recommends \ pkg-config \ libssl-dev \ libpq-dev \ protobuf-compiler \ && rm -rf /var/lib/apt/lists/*8. Adding Property-Based Tests
Section titled “8. Adding Property-Based Tests”The template already includes proptest as a dev-dependency and ships with example property tests in tests/integration_test.rs.
Where property tests live
Section titled “Where property tests live”Property tests can be placed in:
- Unit tests: Inside
crates/*.rsin a#[cfg(test)]module - Integration tests: Inside
tests/files, typically in a submodule
The template demonstrates the integration test approach:
mod property_tests { use super::*; use proptest::prelude::*;
proptest! { #[test] fn add_is_commutative(a in any::<i32>(), b in any::<i32>()) { let a = i64::from(a); let b = i64::from(b); prop_assert_eq!(add(a, b), add(b, a)); }
#[test] fn add_zero_is_identity(n in any::<i64>()) { prop_assert_eq!(add(n, 0), n); prop_assert_eq!(add(0, n), n); } }}Writing effective property tests
Section titled “Writing effective property tests”Focus on invariants — properties that must always hold regardless of input:
use proptest::prelude::*;
proptest! { #[test] fn roundtrip_serialize_deserialize(input in "\\PC{1,100}") { let serialized = serialize(&input)?; let deserialized = deserialize(&serialized)?; prop_assert_eq!(input, deserialized); }
#[test] fn output_is_always_valid(input in any::<u64>()) { let result = transform(input); prop_assert!(is_valid(&result)); }}Custom strategies
Section titled “Custom strategies”For domain-specific types, define custom generators:
use proptest::prelude::*;
fn valid_config() -> impl Strategy<Value = Config> { (any::<bool>(), 1..100u32, 1..3600u64).prop_map(|(verbose, retries, timeout)| { Config::new() .with_verbose(verbose) .with_max_retries(retries) .with_timeout(timeout) })}
proptest! { #[test] fn config_always_has_positive_timeout(config in valid_config()) { prop_assert!(config.timeout_secs > 0); }}Configuration
Section titled “Configuration”Proptest behavior can be tuned via a proptest.toml file or the ProptestConfig struct. Create proptest.toml in the project root to adjust the number of test cases:
# Number of successful test cases required (default: 256)cases = 512
# Maximum number of shrink iterations (default: 4096)max_shrink_iters = 8192For detailed guidance on property-based testing strategies, see docs/testing/PROPERTY-BASED-TESTING.md.