Advanced Rust Design Patterns
Designing Ergonomic Rust Libraries
Designing ergonomic Rust libraries applies the official Rust API Guidelines across naming conventions, generic conversion traits (AsRef, Into), standard trait derives, and predictable borrowing signatures so callers can use your crate naturally without fighting unnecessary type conversions.
Pillars of an Ergonomic Rust Library API:
1. Naming Conventions (`as_*` free, `to_*` copies, `into_*` consumes)
2. Flexible Inputs (`impl AsRef<Path>`, `impl Into<String>`, `impl IntoIterator`)
3. Concrete, Informative Outputs (Return concrete structs/enums, not overly generic types)
4. Eager Standard Trait Derives (`Debug`, `Clone`, `PartialEq`, `Eq`, `Hash`, `Default`)
5. Thread-Safety by Default (`Send + Sync` verified by compile-time test assertions)
1. Conversion Method Naming Conventions (as_, to_, into_)
Rust developers rely on three method prefixes to know the ownership and allocation cost of a conversion at a glance:
| Prefix | Receiver | Cost | Standard Library Examples |
|---|---|---|---|
as_* |
&self (or Copy self) |
Free ($O(1)$ borrowed view) | String::as_str(&self) -> &str, Path::as_os_str(&self), Vec::as_slice(&self) |
to_* |
&self |
Expensive (allocates or converts bytes) | str::to_lowercase(&self) -> String, CStr::to_string_lossy(&self), u32::to_be_bytes(self) |
into_* |
self (consumes owner) |
Variable (often $O(1)$ ownership unwrap!) | String::into_bytes(self) -> Vec<u8>, BufWriter::into_inner(self), Vec::into_boxed_slice(self) |
Similarly, for collection and wrapper accessors:
- Omit the
get_prefix on simple field accessors: name the methodfn len(&self)orfn host(&self) -> &str(notget_len()orget_host()). Reservegetfor indexed/keyed lookups that returnOption<&T>(slice.get(i),map.get(&k)). - Pair every
.iter(&self)method with.iter_mut(&mut self)and.into_iter(self)(plus the threeIntoIteratorimplementations).
2. Accepting Generic Conversions While Avoiding Monomorphization Bloat
| When Your Function Needs... | Accept Parameter Type | Example Call Sites Enabled |
|---|---|---|
| To read a string slice without owning it | &str (simple, zero monomorphization!) |
"literal", &my_string, &cow_str (via Deref coercion!) |
To take ownership of a String (storing it in a struct) |
impl Into<String> |
"literal" (allocates inside), my_owned_string (moves with 0 extra clones!) |
| To read a file system path | impl AsRef<Path> |
"config.toml", Path::new("..."), PathBuf, OsString |
| To read an arbitrary byte buffer | &[u8] or impl AsRef<[u8]> |
b"bytes", &vec_u8, "utf8_str" (since str: AsRef<[u8]>) |
use std::path::{Path, PathBuf};
/// Eagerly derives standard traits so callers can log (`Debug`), compare (`PartialEq`, `Eq`),
/// clone (`Clone`), hash (`Hash`), and construct defaults (`Default`).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ArchiveEntry {
path: PathBuf,
label: String,
compressed_size: u64,
}
impl ArchiveEntry {
/// Accepts `impl Into<PathBuf>` and `impl Into<String>`:
/// - If caller passes an owned `PathBuf` or `String`, it moves in O(1) with 0 clones!
/// - If caller passes `&str`, it converts ergonomically at the boundary.
pub fn new(path: impl Into<PathBuf>, label: impl Into<String>, compressed_size: u64) -> Self {
Self {
path: path.into(),
label: label.into(),
compressed_size,
}
}
/// Follows Rust `as_*` / prefix-free getter conventions:
pub fn path(&self) -> &Path {
&self.path
}
pub fn label(&self) -> &str {
&self.label
}
pub fn compressed_size(&self) -> u64 {
self.compressed_size
}
/// Follows Rust `into_*` consuming convention to unwrap the owned `(PathBuf, String)` in O(1)!
pub fn into_parts(self) -> (PathBuf, String) {
(self.path, self.label)
}
}
fn main() {
// Caller can mix `&str` literals and owned `PathBuf`/`String` values effortlessly:
let owned_label = String::from("manifest");
let entry = ArchiveEntry::new("meta/Cargo.toml", owned_label, 1420);
assert_eq!(entry.path(), Path::new("meta/Cargo.toml"));
assert_eq!(entry.label(), "manifest");
assert_eq!(entry.compressed_size(), 1420);
let (path_buf, label_str) = entry.into_parts();
println!("Extracted owned parts in O(1): path={}, label={label_str}", path_buf.display());
}
Extracted owned parts in O(1): path=meta/Cargo.toml, label=manifest