В этой статье хочу поделиться информацией о крейтах для языка rust, про которые вы могли не знать. Большинство из них позволяют писать меньше шаблонного кода или улучшают его читаемость. Эта статья будет полезна для разработчиков всех уровней, которые пишут на rust.

Делитесь в комментариях крейтами, которые могут быть полезны другим разработчикам.

Под крейтом (crate) в данной статье понимается библиотека, которую можно подключить как зависимость в ваш проект.

Я не копировал всю документацию для каждого крейта в эту статью, поэтому если вас заинтересовал крейт, рекомендую ознакомиться с ним на crates.io или docs.rs.

Также я не приводил в пример всеми известные крейты как serde, anyhow, itertools и другие.

Preface

Вы могли заметить, что многие проекты, написанные на rust, имеют большое число первичных и вторичных зависимостей. По моему мнению, в этом нет ничего плохого и вот почему. Rust держит высокие требования к обратной совместимости стандартной библиотеки. Поэтому много функциональности предоставляется в виде third-party зависимостей, поддерживаемых сообществом, а не разработчиками языка. В то же время rust на этапе появления реализовал хорошее управление зависимостями в виде cargo, который делает использование зависимостей тривиальным. Вместе это позволяет крейтам rust развиваться быстрее, иметь меньше легаси, а также даёт пользователям возможность выбора между разными подходами к реализации функциональности, а не использования того, что дизайнеры языка заложили в стандартную библиотеку. Выше перечисленное позволяет писать крейты в стиле Unix way, когда каждая библиотека делает ровно одну вещь и делает её хорошо.

Подход "batteries included", принятый в python, хорошо работал в 1990-е года, когда распространение ПО не было таким простым. Сейчас это приводит к инициативам по очистке стандартной библиотеки python.

tap

Крейт, позволяющий преобразовать цепочку вызовов функций из префиксной формы в суффиксную. Такая форма позволяет писать более читаемый код.

Легче всего объяснить на примерах. Например, такая цепочка вызовов:

let val = last(
  third(
    second(first(original_value), another_arg)
  ),
  another_arg,
);

Может быть переписана как:

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

Или допустим вы хотите отсортировать массив "на месте" с помощью метода sort(), что потребует сначала сделать переменную мутабельной, а затем переопределить переменную, сделав её снова немутабельной:

let mut collection = stream().collect::<Vec<_>>();
collection.sort();
// potential error site: inserting other mutations here
let collection = collection; // now immutable

На помощь приходит метод .tap_mut, который передаёт в функцию-замыкание значение переменной по мутабельной ссылке:

let collection = stream.collect::<Vec<_>>().tap_mut(|v| v.sort());

Соответственно, можно определить переменную collection лишь раз и сделав её сразу немутабельной.

Данные методы не оказывают влияния на производительность кода, так как на этапе компиляции данные вызовы оптимизируются и результирующий код насколько же производителен как и "наивная" версия.

По моему мнению в обоих примерах код стал более читаем, так как мы избавились от лишних объявлений переменных и переписали вызовы функций в chain-like вид, что позволяет читать код не прыгая глазами по строчкам.

Это не все полезные методы, которые предоставляет данный крейт. Например, он содержит tap_x_dbg методы, которые работают в debug режиме и убираются в relese режиме. Также методы для конверсии между типами, реализующими Into трейт.

Рекомендую ознакомиться с докуменацией данного крейта.

strum

Крейт помогающий избавиться от boilerplate кода при работе с енамами в расте. Функционал работает за счёт derive макросов.

Пример функционала:

  1. strum::Display - реализует std::fmt::Display для енама и соответственно метод to_string() -> String.

  2. strum::AsRefStr - реализует AsRef<&static str>. Соответственно, не требует аллокации памяти при каждом вызове как при использовании to_string().

  3. strum::IntoStaticStr - реализует From<MyEnum> for &'static str. Работает аналогично предыдущему варианту.

  4. strum::EnumString - реализует std::str::FromStr и std::convert::TryFrom<&str>, что позволяет преобразовывать строки в инстансы енама.

  5. strum::EnumCount - добавляет константу COUNT: usize, которая содержит количество вариантов енама.

  6. strum::EnumIter - реализует итератор по вариантам енама. Данные внутри вариантов будут установлены в Default::default().

И даже больше. Рекомендую заглянуть в документацию данного крейта.

Пример использования макросов выше:

#[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 }]
);

Также разные макросы данного крейта поддерживают кастомизацию поведения. Например, можно менять строку, в которую будет преобразован инстанс анама атрибутом #[strum(serialize = "redred")].

derive_more

В расте популярен паттер под названием NewType. Суть заключается в "оборачивании" сторонних библиотечных типов в нашу структуру:

pub struct NonEmptyVec(Vec<i32>);

Одним из примеров использования данного паттерна является сохранение каких-то инвариантов. Для нашей структуры мы можем определить функцию-конструктор, которая будет проверять, что внутренний вектор всегда не пустой:

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

Соответсвенно, структуру можно создать только через конструктор, который проверяет требуемый инвариант. Минусом данного подхода является то, что для наша структура-враппер NonEmptyVec не наследует имплементацию трейтов внутреннего типа.

Например, что если мы захотим передать структуру в функцию, которая принимает на вход IntoIterator? Этот код не скомпилируется:

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

Мы можем написать свою реализацию:

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

Но по сути это boilerplate код, потому что это дублирование уже существующей реализации трейта внутреннего типа. Избавиться от такого кода поможет данный крейт. Просто добавляем использование derive макросов для нашей структуры-враппера:

#[derive(derive_more::AsRef, derive_more::Deref, derive_more::IntoIterator, derive_more::Index)]
pub struct NonEmptyVec(Vec<i32>);

И проверяем, что требуемый функционал работает:

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

Как видите, мы автоматически восстановили реализацию сразу нескольких полезных трейтов.

Данный крейт содержит макросы для генерации трейтов конвертации (From, IntoIterator, AsRef, etc), форматирования (Display-like), операторов (Add, Index, etc), полезных методов (Constructor, Unwap, etc).

derive_builder

Одним из популярных паттернов в расте является builder [1, 2]. Данный паттерн удобен, когда нужно создать сложную структуру со множеством полей.

Например, предположим у нас есть несколько функций, проделывающих сложную работу и возвращающих результат Calculation - структуру со множеством опциональных полей - а затем мы хотим проверить работу функций в юнит тесте:

#[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(())
}

Получилось довольно многословно. Давайте используем derive_builder:

#[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 */ }

Крейт сгенерировал билдер структуру с именем CalculationBuilder и сеттерами для каждого поля.

Теперь тест можно переписать намного короче:

#[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(())
}

Как видите, теперь мы можем присвоить значения только требуемым полям и тест получился намного короче. Если бы опциональных полей в структуре было больше, то и выигрыш в краткости и читаемости был выше.

Это только один из примеров использования builder паттерна, который облегчает данный крейт. Также крейт поддерживает валидацию полей билдера в build методе и несколько видов билдеров (owned, mutable, immutable).

Пожалуй единственный минус этого крейта, на мой взгляд, является то, что генерируемый build метод всегда возвращает Result<T>, даже когда T состоит только из опциональных полей, как в нашем случае.

insta

Библиотека для снапшот тестирования. Снапшот - ожидамый результат теста, обычно хранимый в отдельном файле. Библиотека предоставляет command line утилиту для удобного обновления снапшотов. Крейт предоставляет множество фич, можно с ними ознакомиться в официальном гайде.

Одна из полезных фич, по моему мнению, является redactions. Она позволяет тестировать значения с рандомными полями или с недерминированным порядком, например 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()
    });
}

Для данного теста был автоматически сформирован снапшот snapshots/insta__tests__redactions.snap с данным содержанием:

---
source: src/bin/insta.rs
expression: user
---
id: "[uuid]"
username: john_doe
flags:
  - aha
  - foo
  - zzz

enum_dispatch

Раст поддерживает полиморфизм с помощью статической и динамической диспетчиризации трейтов. Если вы использовали динамическую диспатчеризацию, то знаете, что она может негативно сказываться на производительности программы, так как реализация трейта ищется через vtable во время выполнения программы.

Данный трейт позволяет превратить динамическую диспетчиризацию в статическую с использованием енама. Предположим, у нас есть такой трейт и его реализации:

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

В этом примере мы используем динамическую диспетчиризацию:

#[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]
    );
}

Теперь используем данный трейт:

#[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]
    );
}

Таким образом крейт сгенерировал за нас реализацию трейта ReturnsValue для енама EnumDispatched. Авторы крейта произвели замеры производительности и выяснили, что такая реализация может ускорить использование трейта до 10-12 раз.

На мой взгляд основный минус данной библиотеки в том, что можно сгенерировать реализацию только трейта, объявленного в вашем крейте. Так как необходимо применить макрос #[enum_dispatch::enum_dispatch] непосредственно на трейт (чтобы enum_dispatch смог прочитать сигнатуры функций трейта). Соответственно применить макрос к трейту можно только в вашем крейте, который вы можете редактировать.

paste

Данный крейт позволяет конкатенировать идентификаторы во время компиляции без использовния nightly. Это полезно при написании макросов, чтобы создавать произвольные идентификаторы, используя переменные макроса и статические идентификаторы-литералы.

Сокращённый пример из readme крейта. Данный макрос создаст impl блок для типа с именем $name и создаст методы-геттеры для каждого $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

Енам общего назначения Either с двумя вариантами Left и Right. Реализует множество методов и трейтов для удобной работы с данным енамом.

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

Коллекция числовых трейтов и типов. Включает в себя дженерики для чисел, большие числа (big integers), комплексные числа и тд.

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

Крейт предоставляет макрос для реализации std::error::Error трейта на структуры и енамы.

С точки зрения обработки ошибок есть 2 типа крейтов: библиотеки и приложения. Библиотека создаётся как third-party зависимость, которая будет использоваться в приложениях. Для крейтов-библиотек важно, чтобы вызывающий код мог при необходимости проверить, какого типа возникла ошибка в коде библиотеки, и реализовать разное поведения для разных видов ошибок. Например, игнорировать ошибки ввода-вывода, а на ошибках формата данных вызывать панику. Для приложений обычно не важен конкретный тип ошибок, поэтому функции приложений обычно возвращают тип Result<T, anyhow::Error>, так как anyhow позволяет удобно конвертировать любые ошибки в anyhow::Error тип с помощью ? оператора или From трейта. Подробнее тут: [1] (немного старая статья) и [2] (более новая).

Данный крейт в основном используется для удобной реализации ошибок в крейтах-библиотеках.

Пример использования:

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

В примере выше ошибка std::num::ParseIntError была сконвертирована в SomeError::ParseInt енам. Без данного крейта нам пришлось бы прописывать все преобразования вручную.

rayon

Облегчает использование параллелизма в расте. Подходит для превращения последовательных итераторов в параллельные. Гарантирует отсутствие гонки данных (data-race). Параллельные итераторы в рантайме адаптируют своё поведение для максимальной производительности.

use rayon::prelude::*;
fn sum_of_squares(input: &[i32]) -> i32 {
    input
         .par_iter() // <-- just change that!
         .map(|&i| i * i)
         .sum()
}

В примере выше последовательный итератор был превращён в параллельным всего лишь изменением iter() в par_iter().

crossbeam

Крейт предоставляет набор инструментов для конкурентного программирования: атомики, структуры данных, управление памятью, синхронизация тредов и тд.

Например, реализация каналов (channels) данного крейта более производительная по сравнению с каналами std (по заверению разработчиков) и позволяет иметь несколько записывающих и несколько читающих потоков (multi-producer multi-consumer) в отлиии от std каналов, которые разрешают только один читающий поток (multi-producer single-consumer).

async_trait

Крейт позволяет определять трейты с асинхронными функциями. На данный момент раст не поддерживает асинхронные трейты на уровне синтаксиса языка, поэтому вы можете использовать одноимённый макрос из этого крейта. Подробнее почему здесь.

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

Макрос изменяет сигнатуры функций, чтобы они возвращали Pin<Box<dyn Future + Send + 'async_trait>>.

fs-err

Данный крейт содержит функции-врапперы с человекочитаемыми ошибками для функций из std::fs.

Если вы использовали функции из std::fs (такие как read_to_string или write) возможно вы замечали, что в случае ошибки сообщение будет не очень полезным:

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)

Из данного сообщения не понятно, какой из файлов не существует. Но если мы используем fs-err крейт, то мы получим более подробное сообщение:

failed to open file `config-not-exist.txt`
    caused by: The system cannot find the file specified. (os error 2)

tempfile

Крейт для создания временных файлов и директорий.

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

Енкодер и декодер структур в массив байтов и обратно. Использует компактный формат данных, подходящий для хранения на диске и для обмена данными между системами с разными архитектурами процессора.

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

Макросы для генерации типов-контейнеров из std. По большей части дело вкуса, ведь контейнеры имеют from и from_iter методы. Есть открытый RFC на эту тему.

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

Хэш-таблица, которая сохраняет порядок вставки элементов в неё. То есть итерация по элементам хэш-таблицы будет происходить в том же порядке, что и порядок вставки элементов. Данный порядок сохраняется до тех пор, пока вы не вызываете метод remove. Поддерживает поиск элементов по ключу и по числовым индексам (как массив) и быструю итерация по элементам. Все эти свойства исходят из того, что внутри хранится вектор пар ключ-значение и хэш-таблица от хэшей ключей к их индексам в векторе. Стоит использовать, если данные свойства подходят для вашей задачи.

getset

Данный крейт понравится бывшим Java программистам. Крейт содержит процедурные макросы для генерации методов геттеров и сеттеров.

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

Библиотека для автоматического создания mock объектов для (почти всех) трейтов и структур. Данные объекты могут использоваться в юнит-тестах вместо объектов оригинального типа, что может облегчить написание высокоуровневых юнит тестов или протестировать сложнопроверяемые крайние случаи.

#[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 объекты можно автоматиески с помощью attribute-макроса #[automock]. Но он имеет свои ограничения, поэтому иногда приходиться использовать процедурный макрос mock! с более ручной реализацией mock объектов.

quickcheck

Фреймворк для тестирования на основе свойств. Позволяет тестировать код на большом количестве произвольных входных данных. При нахождении ошибки автоматически находит минимальный тест-кейс для воспроизведения ошибки.

#[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

Также как и quickcheck является фреймворком для тестирования на основе свойств. Но обладает более гибкой генерацией входных данных по сравнению с quickcheck, хотя для сложных данных может работать сильно дольше чем 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

Библиотека для конвертации текста в разные общепринятые стили написания идентификаторов переменных, такие как CamelCase, snake_case и другие.

Например, когда вы используете атрибут rename_all в библиотеке sqlx, то внутри используется функционал heck.

use heck::ToShoutyKebabCase;

#[test]
fn test() {
    assert_eq!("i am very angry!".to_shouty_kebab_case(), "I-AM-VERY-ANGRY");
}

num_cpus

Небольшой крейт, помогающий определить количество физических ядер процессора или количество параллельных задач, которые могут эффективно выполняться на данной системе.

Например, я его использовал для определения количества тредов при прохождении туториала Ray Tracing in One Weekend.

humantime

Библиотека предоставляет форматтер и парсер для std::time::{Duration, SystemTime} в человекочитаемом виде. Также имеет интеграцию с serde с помощью крейта humantime-serde. Это позволяет, например, указывать значения Duration в конфиге приложения/сервиса в удобочитаемом виде вместо использования единиц измерения в имени переменной, что уменьшает вероятность ошибки:

# for example instead of this:
timeout_mins: 120
# you can write this:
timeout: 2 hours

Пример использования:

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

Предоставляет макрос для облегчения реализации трейтов-операторов. Данный крейт подойдёт, когда нужна более хитрая реализация оператора: в примере ниже мы сгенерировали сложение двух разных типов, которое на самом деле работает как a * b + 1. Для простых случаев подойдёт крейт derive_more.

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

Макросы для генерации итераторов по значениям енама или структуры.

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

Позволяет удобно объявлять элементы, зависящие от большого числа #[cfg], в виде if-else выражений.

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

Макросы для удобного создания массивов (array) из слайсов.

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

Предоставляет процедурные макросы для более быстрой, гибкой и декларативной реализации трейтов из стандартной библиотеки. В частности Debug, Eq, Ord, Deref и тд. Гибкость заключается в возможности исключать поля из реализации, включения trait bounds и тд.

#[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

Также как и educe предоставляет макросы для реализации трейтов стандартной библиотеки. Но последнее обновление библиотеки было в январе 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

В rust для изменения даты используется тип Duration, который представляет из себя фиксированное количество секунд и наносекунд. Так chrono::Duration имеет функции-конструкторы с именами weeks, days, hours, но не имеет конструктора month, потому что это относительная величина, которую нельзя выразить в секундах. Поэтому если вы хотите изменить дату на более привычную для человеческого понимания величину, например прибавить месяц или год, то можете использовать инструменты данного крейта.

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/

Комментарии (5)