DEV Community

Artem Slobodkin
Artem Slobodkin

Posted on

32 Rust crates you better know about

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,
);
Enter fullscreen mode Exit fullscreen mode

Can be rewritten as:

let val = original_value
  .pipe(first)
  .pipe(|v| second(v, another_arg))
  .pipe(third)
  .pipe(|v| last(v, another_arg));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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:

  1. strum::Display - implements std::fmt::Display for an enum and therefore the to_string() -> String method.
  2. strum::AsRefStr - implements AsRef<&static str>. Therefore, it does not require memory allocation as in the case of using to_string().
  3. strum::IntoStaticStr - implements From<MyEnum> for &'static str. Works similarly to the previous option.
  4. strum::EnumString - implements std::str::FromStr and std::convert::TryFrom<&str>, allowing you to convert strings into enum instances.
  5. strum::EnumCount - adds the constant COUNT: usize, which contains the number of enum variants.
  6. strum::EnumIter - implements an iterator over the enum variants. The data inside the variants will be set to Default::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 }]
);
Enter fullscreen mode Exit fullscreen mode

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>);
Enter fullscreen mode Exit fullscreen mode

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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>);
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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 */ }
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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()
    });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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]
    );
}
Enter fullscreen mode Exit fullscreen mode

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]
    );
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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"]
    );
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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(_)
    ));
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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]),
]);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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))
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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 }));
}
Enter fullscreen mode Exit fullscreen mode

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 */ }
    }
}
Enter fullscreen mode Exit fullscreen mode

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]);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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});
Enter fullscreen mode Exit fullscreen mode

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)
);
Enter fullscreen mode Exit fullscreen mode

References

  1. https://www.reddit.com/r/rust/comments/uevmnx/what_crates_would_you_consider_essential/
  2. https://www.reddit.com/r/rust/comments/ylp4nz/what_crates_are_considered_as_defacto_standard/
  3. https://blessed.rs/crates
  4. https://lib.rs/
  5. https://crates.io/crates?sort=recent-downloads
  6. https://www.reddit.com/r/rust/comments/nuq1ix/whats_your_favourite_underrated_rust_crate_and_why/

Top comments (1)

Collapse
 
michalfita profile image
Michał Fita

getset - that should be forbidden, forgotten, burnt, buried deep in the ground; one of worst programming practices are getters and setters for private variables.

Others are good selection.