paint-brush
You Need to Know About These 32 Rust Cratesby@artslob
2,027 reads
2,027 reads

You Need to Know About These 32 Rust Crates

by Artyom SlobodkinJune 6th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Share information about crates for the Rust language that you may not know about. Most of them allow you to write less boilerplate code and improve readability.

People Mentioned

Mention Thumbnail
featured image - You Need to Know About These 32 Rust Crates
Artyom Slobodkin HackerNoon profile picture

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 with 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 give 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 that 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 checking 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 }]
);

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 to 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 has become 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 a 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 the 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 a 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, including 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 represent 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

  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/

Also published here.