Skip to content

Customization

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.


  1. Adding New Modules
  2. Removing Example Code
  3. Library-Only vs Binary Crate
  4. Adjusting Lint Strictness
  5. Adjusting Supply Chain Policy
  6. Modifying Release Targets
  7. Docker Customization
  8. Adding Property-Based Tests

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(_))));
}
}

Add a pub mod declaration at the top of crates/lib.rs:

pub mod parser;

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());
}
  • Module file created in crates/
  • pub mod added to crates/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 test passes
  • cargo clippy --all-targets --all-features passes
  • cargo doc --no-deps builds without warnings

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.

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.

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.

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.

Remove the example usage snippets in README.md that reference add, divide, and Config. Replace them with documentation for your own public API.


If your crate is library-only, remove the binary configuration:

  1. Delete crates/main.rs.

  2. Remove the [[bin]] section from Cargo.toml:

    # Delete these lines:
    [[bin]]
    name = "rust_template"
    path = "crates/main.rs"
  3. Remove the binary-related steps from the Docker and release workflows if you do not need them.

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.

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 = true
rust-version.workspace = true
license.workspace = true
[lints]
workspace = true

The template enables four Clippy lint groups in [lints.clippy]:

GroupWhat it covers
allStandard Clippy lints (correctness, style, complexity, performance)
pedanticStricter, opinionated lints (naming conventions, API design, documentation)
nurseryExperimental lints that may have false positives
cargoCargo 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 disable
cargo = { level = "warn", priority = -1 }

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 {
// ...
}

The template denies these lints at the crate level:

unwrap_used = "deny" # Use Result and ? instead
expect_used = "deny" # Use Result and ? instead
panic = "deny" # Never panic in library code
todo = "deny" # Complete all implementations
unimplemented = "deny" # Complete all implementations
dbg_macro = "deny" # Remove debug macros before committing
print_stdout = "deny" # Use tracing or log instead
print_stderr = "deny" # Use tracing or log instead

To relax any of these, change "deny" to "warn" or "allow":

todo = "warn" # Allow TODOs with a warning

Note 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] in crates/main.rs is intentional. Library code stays subject to the deny.

The clippy.toml file controls numeric thresholds for various lints:

SettingDefaultPurpose
cognitive-complexity-threshold25Maximum cognitive complexity per function
excessive-nesting-threshold4Maximum nesting depth
too-many-lines-threshold100Maximum function length
too-many-arguments-threshold7Maximum function parameters
max-struct-bools3Maximum bool fields in a struct
max-fn-params-bools3Maximum bool parameters in a function
pass-by-value-size-limit256Byte threshold to warn about passing large types by value
type-complexity-threshold250Threshold for overly complex types

Adjust these to match your project’s needs. For example, to allow longer functions:

too-many-lines-threshold = 200

Test code is exempt from several strict lints via these settings in clippy.toml:

allow-dbg-in-tests = true
allow-expect-in-tests = true
allow-unwrap-in-tests = true
allow-print-in-tests = true

Key formatting settings you may want to adjust:

SettingDefaultPurpose
max_width100Maximum line width
tab_spaces4Spaces per indentation level
imports_granularity"Crate"How imports are grouped (Crate, Module, Item, One)
group_imports"StdExternalCrate"Import grouping order (std, external, crate)
wrap_commentstrueWrap 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 = 120
comment_width = 120

The deny.toml file configures cargo-deny to audit your dependency tree.

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 }]

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.

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" }

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",
]

After modifying deny.toml, run the checks:

Terminal window
cargo deny check

The release workflow (.github/workflows/release.yml) builds binaries for multiple platforms using a matrix strategy.

TargetOS RunnerArchitecture
x86_64-unknown-linux-gnuubuntu-latestLinux x86_64
aarch64-unknown-linux-gnuubuntu-latestLinux ARM64 (cross-compiled)
x86_64-apple-darwinmacos-latestmacOS x86_64
aarch64-apple-darwinmacos-latestmacOS ARM64 (Apple Silicon)
x86_64-pc-windows-msvcwindows-latestWindows x86_64

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-amd64

For 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-tools

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.exe

Also remove the target from deny.toml’s [graph].targets list so supply chain checks stay aligned.

Cross-compilation for aarch64-unknown-linux-gnu requires:

  • gcc-aarch64-linux-gnu installed on the runner
  • The CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER environment variable set to aarch64-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.


The Dockerfile uses a multi-stage build:

  1. Builder stage (rust:1.92-slim) — compiles the binary with release optimizations.
  2. Runtime stage (gcr.io/distroless/cc-debian12) — minimal image containing only the binary.

To use a different runtime base image (for example, Debian slim instead of distroless):

# Runtime stage
FROM 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 appuser
USER 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.

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/*

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 dependencies
RUN mkdir -p crates/core/src crates/cli/src && \
echo "pub fn dummy() {}" > crates/core/src/lib.rs && \
echo "fn main() {}" > crates/cli/src/main.rs

And update the copy step:

COPY crates/ ./crates/

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/*

The template already includes proptest as a dev-dependency and ships with example property tests in tests/integration_test.rs.

Property tests can be placed in:

  • Unit tests: Inside crates/*.rs in 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);
}
}
}

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));
}
}

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);
}
}

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 = 8192

For detailed guidance on property-based testing strategies, see docs/testing/PROPERTY-BASED-TESTING.md.