Artem Slobodkin
Posted on May 5, 2023
In this article, I want to share information about crates for the Rust language that you may not know about. Most of them allow you to write less boilerplate code or improve code readability. This article will be useful for developers of all levels who write in Rust.
Share in the comments crates that may be useful to other developers.
In this article, a crate refers to a library that can be used as a dependency in your project.
I did not copy the entire documentation for each crate, so if you are interested in a crate, I recommend checking it out on crates.io or docs.rs.
Also, I did not give examples of well-known crates such as serde
, anyhow
, itertools
, and others.
Preface
You may have noticed that many projects written in Rust have a large number of primary and secondary dependencies. In my opinion, there is nothing wrong with this, and here's why. Rust has high requirements for backward compatibility of the standard library. Therefore, much of the functionality is provided in the form of third-party dependencies, maintained by the community, rather than by the language creators. At the same time, Rust implemented good dependency management in the form of Cargo at an early stage, which makes adding dependencies trivial. Together, this allows Rust crates to evolve faster, have less legacy code, and gives users the ability to choose between different implementation approaches, rather than relying solely on what the language creators included in the standard library. The above allows writing Rust crates in the Unix way, where each library does exactly one thing and does it well.
The "batteries included" approach adopted, for example, in Python worked well in the 1990s when software distribution was not as easy. Now it leads to clean-up initiatives of the Python standard library.
tap
A crate that allows converting a chain of function calls from prefix notation to postfix notation. This form allows you to write more readable code.
It is easiest to explain with examples. For example, a chain of calls like this:
let val = last(
third(
second(first(original_value), another_arg)
),
another_arg,
);
Can be rewritten as:
let val = original_value
.pipe(first)
.pipe(|v| second(v, another_arg))
.pipe(third)
.pipe(|v| last(v, another_arg));
Or, suppose you want to sort an array "in place" using the sort()
method, which will require making the variable mutable first, and then redefining the variable to make it immutable again:
let mut collection = stream().collect::<Vec<_>>();
collection.sort();
// potential error site: inserting other mutations here
let collection = collection; // now immutable
The .tap_mut
method comes to the rescue, which passes the value of the variable by mutable reference to the closure:
let collection = stream.collect::<Vec<_>>().tap_mut(|v| v.sort());
Accordingly, the variable collection
can be defined only once and be immutable from the start.
These methods do not affect the performance of the code, since at the compilation stage, these calls are optimized and the resulting code is just as performant as the naive version.
In my opinion, in both examples, the code became more readable because we got rid of unnecessary variable declarations and rewrote the function calls in a chain-like way, which allows you to read the code without jumping your eyes around the lines.
These are not all the useful methods provided by this crate. For example, it contains tap_x_dbg
methods, which work in debug mode and are removed in release mode. There are also methods for converting between types that implement the Into
trait.
I recommend to check out the documentation of this crate.
strum
The crate helps to get rid of boilerplate code when working with enums in Rust. The functionality is achieved through derive-type macros.
For example:
-
strum::Display
- implementsstd::fmt::Display
for an enum and therefore theto_string() -> String
method. -
strum::AsRefStr
- implementsAsRef<&static str>
. Therefore, it does not require memory allocation as in the case of usingto_string()
. -
strum::IntoStaticStr
- implementsFrom<MyEnum> for &'static str
. Works similarly to the previous option. -
strum::EnumString
- implementsstd::str::FromStr
andstd::convert::TryFrom<&str>
, allowing you to convert strings into enum instances. -
strum::EnumCount
- adds the constantCOUNT: usize
, which contains the number of enum variants. -
strum::EnumIter
- implements an iterator over the enum variants. The data inside the variants will be set toDefault::default()
.
And even more. I recommend taking a look at the documentation of this crate.
An example of using the above macros:
#[derive(
Debug,
PartialEq,
strum::Display,
strum::IntoStaticStr,
strum::AsRefStr,
strum::EnumString,
strum::EnumCount,
strum::EnumIter,
)]
enum Color {
Red,
Blue(usize),
Green { range: usize },
}
// convertions to String and &'static str
assert_eq!(Color::Blue(2).to_string(), "Blue");
assert_eq!(Color::Green { range: 5 }.as_ref(), "Green");
assert_eq!(<&str>::from(Color::Red), "Red");
assert_eq!(Color::Red, Color::from_str("Red").unwrap());
assert_eq!(Color::COUNT, 3);
assert_eq!(
Color::iter().collect::<Vec<_>>(),
vec![Color::Red, Color::Blue(0), Color::Green { range: 0 }]
);
Additionally, different macros of this crate support behavior customization. For example, it is possible to change the string into which an enum instance will be converted using the attribute #[strum(serialize = "redred")]
.
derive_more
The NewType pattern is pretty common in Rust. Sometimes you need wrap third-party library types in our own structure:
pub struct NonEmptyVec(Vec<i32>);
One of the examples of using this pattern is to maintain invariants. For our structure, we can define a constructor function that checks that the internal vector is never empty:
impl NonEmptyVec {
pub fn new(numbers: Vec<i32>) -> Result<Self> {
if numbers.is_empty() {
bail!("expected non empty vector of integers")
} else {
Ok(Self(numbers))
}
}
}
Therefore, the structure can only be created through a constructor that checks the required invariant. The downside of this approach is that our wrapper structure NonEmptyVec
does not inherit the implementation of traits from the internal type.
For example, what if we want to pass the structure to a function that takes IntoIterator
as input? This code will not compile:
fn collector(iter: impl IntoIterator<Item = i32>) -> Vec<i32> {
iter.into_iter().collect()
}
#[test]
fn non_emtpy_vec() -> Result<()> {
let non_empty = NonEmptyVec::new(vec![1, 2, 3])?;
assert_eq!(collector(non_empty), vec![1, 2, 3]);
Ok(())
}
We can write our own implementation:
impl IntoIterator for NonEmptyVec {
type Item = <Vec<i32> as IntoIterator>::Item;
type IntoIter = <Vec<i32> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
<Vec<i32> as IntoIterator>::into_iter(self.0)
}
}
But essentially, this is boilerplate code because it duplicates the existing implementation of the trait of the internal type. This crate can help eliminate such code. We simply add the use of the derive macros for our wrapper structure:
#[derive(derive_more::AsRef, derive_more::Deref, derive_more::IntoIterator, derive_more::Index)]
pub struct NonEmptyVec(Vec<i32>);
And check that this works:
fn collector(iter: impl IntoIterator<Item = i32>) -> Vec<i32> {
iter.into_iter().collect()
}
#[test]
fn non_emtpy_vec() -> Result<()> {
assert!(NonEmptyVec::new(vec![]).is_err());
let non_empty = NonEmptyVec::new(vec![1, 2, 3])?;
assert_eq!(non_empty.as_ref(), &[1, 2, 3]);
assert_eq!(non_empty.deref(), &[1, 2, 3]);
assert_eq!(non_empty[1], 2);
assert_eq!(collector(non_empty), vec![1, 2, 3]);
Ok(())
}
As you see, we automatically restored the implementation of several useful traits.
This crate contains macros for generating conversion traits (From
, IntoIterator
, AsRef
, etc), formatting traits (Display
-like), operator traits (Add
, Index
, etc), useful methods (Constructor
, Unwrap
, etc).
derive_builder
One of the popular patterns in Rust is the builder pattern [1, 2]. This pattern is convenient when you need to create a complex structure with many fields.
For example, let's say we have several functions that perform some complex work and return a result of Calculation
- a structure with many optional fields - and then we want to test the functions in a unit test:
#[derive(Debug, Eq, PartialEq)]
struct Calculation {
a: Option<i32>,
b: Option<i32>,
c: Option<i32>,
d: Option<i32>,
// ... can be more optional fields
}
fn qwe() -> Calculation {
// does complex calculation
Calculation {
a: Some(1),
b: None,
c: None,
d: None,
}
}
fn asd() -> Calculation {
// does complex calculation
Calculation {
a: Some(6),
b: None,
c: None,
d: Some(7),
}
}
fn zxc() -> Calculation {
// does complex calculation
Calculation {
a: None,
b: Some(2),
c: Some(3),
d: None,
}
}
#[test]
fn test() -> Result<()> {
assert_eq!(
qwe(),
Calculation {
a: Some(1),
b: None,
c: None,
d: None,
}
);
assert_eq!(
asd(),
Calculation {
a: Some(6),
b: None,
c: None,
d: Some(7),
}
);
assert_eq!(
zxc(),
Calculation {
a: None,
b: Some(2),
c: Some(3),
d: None,
}
);
Ok(())
}
Using derive_more crate, we can simplify the code and make it more concise:
#[derive(Debug, Eq, PartialEq, Default, derive_builder::Builder)]
// setters calls can be chained, each call clones builder
#[builder(pattern = "immutable")]
// if field not set then it would be default (None in our case)
#[builder(default)]
// setter method accepts T as argument and field value would be Some(T)
#[builder(setter(strip_option))]
struct Calculation {
a: Option<i32>,
b: Option<i32>,
c: Option<i32>,
d: Option<i32>,
// ... can be more optional fields
}
fn qwe() -> Calculation { /* same as before */ }
fn asd() -> Calculation { /* same as before */ }
fn zxc() -> Calculation { /* same as before */ }
The crate generated a builder structure named CalculationBuilder
with setters for each field.
Now the test can be rewritten much shorter:
#[test]
fn derive_builder() -> Result<()> {
let builder = CalculationBuilder::default();
assert_eq!(qwe(), builder.a(1).build()?);
assert_eq!(asd(), builder.a(6).d(7).build()?);
assert_eq!(zxc(), builder.b(2).c(3).build()?);
Ok(())
}
As you can see, now we can assign values only to the required fields, and the test became much shorter. If there were more optional fields in the structure, the gain in brevity and readability would be even higher.
This is just one example of using the builder
pattern, which is facilitated by this crate. The crate also supports field validation in the build
method and several types of builders (owned, mutable, immutable).
Perhaps the only downside of this crate, in my opinion, is that the generated build
method always returns Result<T>
, even when T
consists only of optional fields, as in our case.
insta
Library for snapshot testing. Snapshot represents the expected result of a test, usually stored in a separate file. The library provides a command-line utility for easy snapshot updates. The crate offers many features that can be found in the official guide.
One of the useful features, in my opinion, is redactions
. It allows testing values with random or non-deterministic fields order, such as HashSet
:
#[derive(serde::Serialize)]
pub struct User {
id: Uuid,
username: String,
flags: HashSet<&'static str>,
}
#[test]
fn redactions() {
let user = User {
id: Uuid::new_v4(),
username: "john_doe".to_string(),
flags: maplit::hashset! {"zzz", "foo", "aha"},
};
insta::assert_yaml_snapshot!(user, {
".id" => "[uuid]",
// make hashset order deterministing
".flags" => insta::sorted_redaction()
});
}
For this test, a snapshot snapshots/insta__tests__redactions.snap
was automatically generated with the following contents:
---
source: src/bin/insta.rs
expression: user
---
id: "[uuid]"
username: john_doe
flags:
- aha
- foo
- zzz
enum_dispatch
Rust supports polymorphism through static and dynamic dispatch of traits. If you have used dynamic dispatch, you know that it can negatively impact the performance of a program, as the trait implementation is looked up through a vtable at runtime.
This trait allows you to turn dynamic dispatch into static dispatch using an enum. Suppose we have such a trait and its implementations:
pub trait ReturnsValue {
fn return_value(&self) -> usize;
}
pub struct Zero;
impl ReturnsValue for Zero {
fn return_value(&self) -> usize {
0
}
}
pub struct Any(usize);
impl ReturnsValue for Any {
fn return_value(&self) -> usize {
self.0
}
}
In this example, we are using dynamic dispatch:
#[test]
fn derive_dispatch_dynamic() {
let values: Vec<Box<dyn ReturnsValue>> = vec![Box::new(Zero {}), Box::new(Any(5))];
assert_eq!(
values
.into_iter()
.map(|dispatched| dispatched.return_value())
.collect::<Vec<_>>(),
vec![0, 5]
);
}
Now let's use this trait:
#[enum_dispatch::enum_dispatch]
pub trait ReturnsValue {
fn return_value(&self) -> usize;
}
// trait implementations are same
#[enum_dispatch::enum_dispatch(ReturnsValue)]
pub enum EnumDispatched {
Zero,
Any,
}
#[test]
fn derive_dispatch_static() {
let values = vec![EnumDispatched::Zero(Zero {}), EnumDispatched::Any(Any(5))];
assert_eq!(
values
.into_iter()
.map(|dispatched| dispatched.return_value())
.collect::<Vec<_>>(),
vec![0, 5]
);
}
Thus, the crate generated the implementation of the ReturnsValue
trait for the EnumDispatched
enum. The creators of the crate conducted performance tests and found that such an implementation can speed up trait usage up to 10-12 times.
In my opinion, the main disadvantage of this library is that you can only generate an implementation for a trait declared in your crate. Since it is necessary to apply the #[enum_dispatch::enum_dispatch]
macro directly to the trait (so that enum_dispatch
can read the function signatures of the trait). Accordingly, it is possible to apply the macro to a trait only in your crate, which you can edit.
paste
The crate allows concatenating identifiers at compile-time without using nightly. This is useful when writing macros to create arbitrary identifiers using macro variables and static literal identifiers.
Here's a shortened example from the crate's readme. This macro will create an impl
block for a type with the name $name
and create getter methods for each $field
.
macro_rules! make_a_struct_and_getters {
($name:ident { $($field:ident),* }) => {
// ...
// Build an impl block with getters. This expands to:
// impl S {
// pub fn get_a(&self) -> &str { &self.a }
// pub fn get_b(&self) -> &str { &self.b }
// pub fn get_c(&self) -> &str { &self.c }
// }
paste! {
impl $name {
$(
pub fn [<get_ $field>](&self) -> &str {
&self.$field
}
)*
}
}
}
}
make_a_struct_and_getters!(S { a, b, c });
fn call_some_getters(s: &S) -> bool {
s.get_a() == s.get_b() && s.get_c().is_empty()
}
either
The general-purpose Either
enum has two variants Left
and Right
. It has a variety of methods and traits for convenient work using this enum.
use either::Either;
#[test]
fn test() {
let values = vec![
Either::Left(1),
Either::Right(true),
Either::Left(10),
Either::Right(false),
];
assert_eq!(
values
.into_iter()
.map(|int_or_bool| -> Either<i32, bool> {
let int = either::try_left!(int_or_bool);
Either::Left(int * 2)
})
.map(|int_or_bool| { either::for_both!(int_or_bool, s => s.to_string()) })
.collect::<Vec<_>>(),
["2", "true", "20", "false"]
);
}
num
The collection of numeric traits and types. Includes generics for numbers, big integers, complex numbers, and so on.
use anyhow::{anyhow, Result};
use num::*;
use std::fmt::Display;
fn bounds_to_string<N: Bounded + Display>(number: N) -> String {
format!(
"value {} min is {} max is {}",
number,
N::min_value(),
N::max_value()
)
}
#[test]
fn bounds() {
assert_eq!(bounds_to_string(12u8), "value 12 min is 0 max is 255");
assert_eq!(
bounds_to_string(33i16),
"value 33 min is -32768 max is 32767"
);
}
fn num_operations<N: Num>(a: &str, b: N) -> Result<N> {
let a = N::from_str_radix(a, 10).map_err(|_| anyhow!("could not conert value"))?;
let value = a + b - N::one();
Ok(if value.is_zero() {
value
} else {
value * (N::one() + N::one())
})
}
#[test]
fn test_num_operations() -> Result<()> {
assert_eq!(num_operations("2", 10i32)?, 22i32);
assert_eq!(num_operations("-5", 6i8)?, 0i8);
Ok(())
}
#[test]
fn greatest_common_divisor() -> Result<()> {
assert_eq!(num::integer::gcd(25u8, 15u8), 5);
assert_eq!(num::integer::gcd(1024i32, 65536i32), 1024);
Ok(())
}
thiserror
The crate provides a macro for implementing the std::error::Error
trait on structs and enums.
From the error handling perspective, there are two types of crates: libraries and applications. A library is created as a third-party dependency that will be used in applications. For library crates, it is important that the calling code can check, if necessary, what type of error occurred in the library code, and implement different behaviors for different types of errors. For example, ignore I/O errors, but panic on data format errors. For applications, the specific type of errors is usually not important, so application functions usually return a Result<T, anyhow::Error>
type, as anyhow
allows convenient conversion of any error into the anyhow::Error
type using the ?
operator or From
trait. More details can be found here: [1] (a slightly old article) and [2] (a newer one).
This crate is mainly used for convenient error implementation in library crates.
Usage example:
#[derive(thiserror::Error, Debug)]
pub enum SomeError {
#[error("io error")]
Io(#[from] std::io::Error),
#[error("int parsing error")]
ParseInt(#[from] std::num::ParseIntError),
#[error("unknown error")]
General(#[from] anyhow::Error),
}
/// library func
fn int_error(s: &str) -> Result<i32, SomeError> {
let num = i32::from_str_radix(s, 10)?;
Ok(num + 2)
}
#[test]
fn test() {
// application code
assert!(matches!(int_error("abc").unwrap_err(), SomeError::ParseInt(_)));
assert!(matches!(
std::io::Error::new(std::io::ErrorKind::Other, "oh no!").into(),
SomeError::Io(_)
));
}
In the example above, the std::num::ParseIntError
error was converted to the SomeError::ParseInt
enum. Without this crate, we would have had to manually write all these conversions.
rayon
This crate makes it easier to use parallelism in Rust. It is suitable for converting sequential iterators into parallel ones. It guarantees the absence of data races. Parallel iterators adapt their behavior at runtime for maximum performance.
use rayon::prelude::*;
fn sum_of_squares(input: &[i32]) -> i32 {
input
.par_iter() // <-- just change that!
.map(|&i| i * i)
.sum()
}
In the example above, a sequential iterator was turned into a parallel one by simply changing iter()
to par_iter()
.
crossbeam
The crate provides a set of tools for concurrent programming: atomics, data structures, memory management, thread synchronization, and more.
For example, the implementation of channels in this crate is more performant compared to std channels (according to the developers), and allows multiple producers and multiple consumers (multi-producer multi-consumer), unlike std channels which only allow a single consumer (multi-producer single-consumer).
async_trait
The crate allows defining traits with async functions. Currently, Rust does not support async traits at the language syntax level, so you can use the macro provided by this crate with the same name as the trait. You can read more about why async fn in traits are hard in this article.
use async_trait::async_trait;
#[async_trait]
trait Advertisement {
async fn run(&self);
}
struct Modal;
#[async_trait]
impl Advertisement for Modal {
async fn run(&self) {
self.render_fullscreen().await;
for _ in 0..4u16 {
remind_user_to_join_mailing_list().await;
}
self.hide_for_now().await;
}
}
The macro changes the function signatures to return Pin<Box<dyn Future + Send + 'async_trait>>
.
fs-err
This crate contains wrapper functions with human-readable errors for functions from std::fs
.
If you have used functions from std::fs
(such as read_to_string
or write
), you may have noticed that in case of an error, the error message is not very informative:
let content = File::open("file-not-exist.txt")?;
let config = File::open("config-not-exist.txt")?;
// error message would be:
// The system cannot find the file specified. (os error 2)
With the fs-err
crate, we can get more detailed error messages, such as which file does not exist:
failed to open file `config-not-exist.txt`
caused by: The system cannot find the file specified. (os error 2)
tempfile
This crate provides an API for creating temporary files and directories.
// Write
let mut tmpfile: File = tempfile::tempfile().unwrap();
write!(tmpfile, "Hello World!").unwrap();
// Seek to start
tmpfile.seek(SeekFrom::Start(0)).unwrap();
// Read
let mut buf = String::new();
tmpfile.read_to_string(&mut buf).unwrap();
assert_eq!("Hello World!", buf);
bincode
This crate provides encoding and decoding of structures to and from byte arrays. It uses a compact data format that is suitable for storage on disk and for exchanging data between systems with different processor architectures.
use anyhow::Result;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Entity {
number: i8,
name: String,
}
fn check<T>(value: &T, expected_size: usize)
where
T: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug,
{
let encoded: Vec<u8> = bincode::serialize(&value).unwrap();
assert_eq!(encoded.len(), expected_size);
let decoded: T = bincode::deserialize(&encoded[..]).unwrap();
assert_eq!(value, &decoded);
}
#[test]
fn test() -> Result<()> {
let first_size = 9; // i8 + u64 for string length
check(&Entity {number: 1, name: "".to_owned()}, first_size);
let second_size = 15; // i8 + u64 for string length + 6 bytes of string
check(&Entity {number: 2, name: "string".to_owned()}, second_size);
Ok(())
}
maplit
The crate provides macros for generating container types from std
. It is largely a matter of personal preference, as containers already have from
and from_iter
methods. There is an open RFC on this topic.
let a = btreemap! {
"a" => vec![1, 2, 3],
"b" => vec![4, 5, 6],
"c" => vec![7, 8, 9],
};
// vs
let b = BTreeMap::from([
("a", vec![1, 2, 3]),
("b", vec![4, 5, 6]),
("c", vec![7, 8, 9]),
]);
indexmap
An ordered hash map that preserves the insertion order of elements. This means that iterating over the elements of the hash map will occur in the same order as the insertion order of the elements. This order is preserved until you call the remove
method. The hash map supports searching for elements by key and by numeric index (like an array) and fast iteration over elements. All these properties arise from the fact that it stores a vector of key-value pairs and a hash table mapping keys' hashes to their indices in the vector. It's worth using if these properties fit your use case.
getset
This crate will be appreciated by former Java programmers. The crate contains procedural macros for generating getter and setter methods.
use getset::{CopyGetters, Getters, MutGetters, Setters};
#[derive(Getters, Setters, MutGetters, CopyGetters, Default)]
pub struct Foo<T>
where
T: Copy + Clone + Default,
{
/// Doc comments are supported!
/// Multiline, even.
#[getset(get, set, get_mut)]
private: T,
/// Doc comments are supported!
/// Multiline, even.
#[getset(get_copy = "pub", set = "pub", get_mut = "pub")]
public: T,
}
fn main() {
let mut foo = Foo::default();
foo.set_private(1);
(*foo.private_mut()) += 1;
assert_eq!(*foo.private(), 2);
}
mockall
This crate provides automatic mock object generation for (almost all) traits and structures. These objects can be used in unit tests instead of objects of the original type, which can make it easier to write high-level unit tests or test complex edge cases.
#[cfg(test)]
use mockall::{automock, predicate::*};
#[cfg_attr(test, automock)]
trait CalcTrait {
fn foo(&self, x: u32) -> u32;
}
fn calculation(calc: impl CalcTrait, x: u32) -> u32 {
calc.foo(x)
}
#[test]
fn test() {
let mut mock = MockCalcTrait::new();
mock.expect_foo().with(eq(4)).times(1).returning(|x| x + 1);
assert_eq!(5, calculation(mock, 4));
}
Mock objects can be automatically generated using the #[automock]
attribute macro. However, it has its limitations, so sometimes you have to use the procedural macro mock!
with a more manual implementation of mock objects.
quickcheck
QuickCheck is a framework for property-based testing. It allows testing code with a large number of arbitrary input data. If an error is found, it automatically finds the minimal test case to reproduce the error.
#[cfg(test)]
mod tests {
fn reverse<T: Clone>(xs: &[T]) -> Vec<T> {
let mut rev = vec!();
for x in xs {
rev.insert(0, x.clone())
}
rev
}
#[quickcheck]
fn double_reversal_is_identity(xs: Vec<isize>) -> bool {
xs == reverse(&reverse(&xs))
}
}
proptest
Like quickcheck
, proptest
is a property-based testing framework. However, it has a more flexible input data generation in contrast to quickcheck
, although for complex data it may take significantly longer to run than quickcheck
.
proptest! {
#[test]
fn doesnt_crash(s in "\\PC*") {
parse_date(&s);
}
#[test]
fn parses_date_back_to_original(y in 0u32..10000,
m in 1u32..13,
d in 1u32..32)
{
let result = parse_date(&format!("{:04}-{:02}-{:02}", y, m, d)).unwrap();
prop_assert_eq!((y, m, d), result);
}
}
heck
A library for converting text into various commonly used variable naming styles, such as CamelCase
, snake_case
, and others.
For example, when you use the rename_all attribute in the sqlx
library, it uses the functionality of heck
.
use heck::ToShoutyKebabCase;
#[test]
fn test() {
assert_eq!("i am very angry!".to_shouty_kebab_case(), "I-AM-VERY-ANGRY");
}
num_cpus
A small crate that helps determine the number of physical CPU cores or the number of parallel tasks that can be efficiently executed on the system.
For example, I used it to determine the number of threads when going through the tutorial Ray Tracing in One Weekend.
humantime
This library provides a formatter and parser for std::time::{Duration, SystemTime}
in a human-readable format. It also has integration with serde
through the humantime-serde
crate. This allows, for example, specifying Duration
values in the application/service config in a readable format instead of using unit of measurement in the variable name, reducing the likelihood of errors:
# for example instead of this:
timeout_mins: 120
# you can write this:
timeout: 2 hours
Usage example:
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[test]
fn format() {
let duration = Duration::new(9420, 0);
let as_str = "2h 37m";
assert_eq!(humantime::format_duration(duration).to_string(), as_str);
assert_eq!(humantime::parse_duration(as_str), Ok(duration));
}
#[derive(Serialize, Deserialize)]
struct Foo {
#[serde(with = "humantime_serde")]
timeout: Duration,
}
#[test]
fn serde() {
let input = r#" { "timeout": "3 days 1hour 12min 5s" } "#;
let foo: Foo = serde_json::from_str(input).unwrap();
assert_eq!(foo.timeout, Duration::new(263525, 0));
}
overload
This crate provides a macro for easier implementation of trait operators. This crate is useful when a more complex implementation of an operator is needed. In the example below, we generated addition of two different types, which actually works as a * b + 1
. For simpler cases, the derive_more
crate will be more suitable.
use overload::overload;
use std::ops;
#[derive(PartialEq, Debug)]
struct A {
v: i32,
}
#[derive(PartialEq, Debug)]
struct B {
v: i32,
}
// ? below generate operator for A and &A values
overload!((a: ?A) + (b: ?B) -> B { B { v: a.v * b.v + 1 } });
#[test]
fn test() {
assert_eq!(&A { v: 3 } + B { v: 5 }, B { v: 16 });
}
enum-iterator
Macros for generating iterators over the values of an enum or structure.
use enum_iterator::{all, first, last, next, Sequence};
use itertools::Itertools;
#[derive(Debug, PartialEq, Sequence)]
enum Direction {
Left,
Middle,
Right,
}
#[test]
fn test_enum() {
use Direction::*;
assert_eq!(all::<Direction>().collect_vec(), vec![Left, Middle, Right]);
assert_eq!(first::<Direction>(), Some(Left));
assert_eq!(last::<Direction>(), Some(Right));
assert_eq!(next(&Middle), Some(Right));
}
#[derive(Debug, PartialEq, Sequence)]
struct Foo {
a: bool,
b: u8,
}
#[test]
fn test_struct() {
let expected_number_of_elements = 512;
assert_eq!(
enum_iterator::cardinality::<Foo>(),
expected_number_of_elements
);
assert_eq!(first::<Foo>(), Some(Foo { a: false, b: 0 }));
assert_eq!(last::<Foo>(), Some(Foo { a: true, b: 255 }));
}
cfg-if
This crate provides convenient declaration of items that depend on a large number of #[cfg]
configurations in the form of if-else expressions.
cfg_if::cfg_if! {
if #[cfg(unix)] {
fn foo() { /* unix specific functionality */ }
} else if #[cfg(target_pointer_width = "32")] {
fn foo() { /* non-unix, 32-bit functionality */ }
} else {
fn foo() { /* fallback implementation */ }
}
}
arrayref
The macros for conveniently creating arrays from slices.
let addr: &[u8; 16] = ...;
let mut segments = [0u16; 8];
// array-based API
for i in 0 .. 8 {
let mut two_bytes = [addr[2*i], addr[2*i+1]];
segments[i] = read_u16_array(&two_bytes);
}
// array-based API with arrayref
for i in 0 .. 8 {
segments[i] = read_u16_array(array_ref![addr, 2*i, 2]);
}
educe
This crate provides procedural macros for faster, flexible and declarative implementation of traits from the standard library, like Debug
, Eq
, Ord
, Deref
and so on. The flexibility lies in the ability to exclude fields from implementation, include trait bounds, and so on.
#[derive(educe::Educe)]
// note `new` below: generate `new()` that calls Default
#[educe(Default(new))]
#[derive(Debug, PartialEq)]
struct Struct {
#[educe(Default = 3)]
f1: u8,
#[educe(Default = true)]
f2: bool,
#[educe(Default = "Hello")]
f3: String,
}
#[test]
fn test() {
let expected = Struct {
f1: 3,
f2: true,
f3: String::from("Hello"),
};
assert_eq!(Struct::default(), expected);
assert_eq!(Struct::new(), expected);
}
derivative
This crate also provides macros for implementing traits from the standard library, similar to educe
. However, the last update of this library was in January 2021.
#[derive(Derivative)]
#[derivative(PartialEq)]
struct Foo {
foo: u8,
#[derivative(PartialEq="ignore")]
bar: u8,
}
assert!(Foo { foo: 0, bar: 42 } == Foo { foo: 0, bar: 7});
assert!(Foo { foo: 42, bar: 0 } != Foo { foo: 7, bar: 0});
chronoutil
In Rust, the Duration
type is used to manipulate dates, which represents a fixed number of seconds and nanoseconds. The chrono::Duration
has constructor functions named weeks
, days
, hours
, but it doesn't have a month
constructor because it is a relative value that cannot be expressed in seconds. Therefore, if you want to manipulate dates in more human-readable units, such as adding a month or a year, you can use the tools provided by this crate.
let delta = RelativeDuration::months(1) + RelativeDuration::days(1);
assert_eq!(
NaiveDate::from_ymd(2021, 1, 28) + delta,
NaiveDate::from_ymd(2021, 3, 1)
);
assert_eq!(
NaiveDate::from_ymd(2020, 1, 28) + delta,
NaiveDate::from_ymd(2020, 2, 29)
);
References
- https://www.reddit.com/r/rust/comments/uevmnx/what_crates_would_you_consider_essential/
- https://www.reddit.com/r/rust/comments/ylp4nz/what_crates_are_considered_as_defacto_standard/
- https://blessed.rs/crates
- https://lib.rs/
- https://crates.io/crates?sort=recent-downloads
- https://www.reddit.com/r/rust/comments/nuq1ix/whats_your_favourite_underrated_rust_crate_and_why/
Posted on May 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.