Skip to main content

Rust Cookbook

Hello world!

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

Обратите внимание: для запуска примеров вам потребуется примерно такой файл Cargo.toml (версии крейтов могут отличаться):

[package]
name = "rust_cookbook"
version = "0.1.0"
edition = "2021"

[dependencies]
chrono = "0.4.31"
crossbeam = "0.8.3"
crossbeam-channel = "0.5.10"
csv = "1.3.0"
env_logger = "0.11.3"
error-chain = "0.12.4"
glob = "0.3.1"
image = "0.25.0"
lazy_static = "1.4.0"
log = "0.4.20"
mime = "0.3.17"
num = "0.4.1"
num_cpus = "1.16.0"
postgres = "0.19.7"
rand = "0.8.5"
rayon = "1.8.0"
regex = "1.10.2"
reqwest = {version = "0.11.23", features = ["blocking", "json"]}
same-file = "1.0.6"
select = "0.6.0"
serde = {version = "1.0.193", features = ["derive"]}
serde_json = "1.0.110"
threadpool = "1.8.1"
tokio = { version = "1.35.1", features = ["full"] }
unicode-segmentation = "1.10.1"
url = "2.5.0"
walkdir = "2.4.0"
dotenv = "0.15.0"
tempfile = "3.9.0"
data-encoding = "2.5.0"
ring = "0.17.7"
clap = "4.5.2"
ansi_term = "0.12.1"
flate2 = "1.0.28"
tar = "0.4.40"
semver = "1.0.22"
percent-encoding = "2.3.1"
base64 = "0.22.0"
toml = "0.8.12"
memmap = "0.7.0"

[dependencies.rusqlite]
version = "0.31.0"
features = ["bundled"]

Также обратите внимание, что некоторые примеры работают только на Linux.

1. Алгоритмы

1.1. Генерация произвольных значений

Генерация произвольных чисел

Генерация произвольных чисел выполняется с помощью метода rand::thread_rng генератора rand::Rng. Генератор создается отдельно для каждого потока (thread). Целые числа равномерно распределяются (uniform distribution) по диапазону типа, числа с плавающей запятой/точкой равномерно распределяются от 0 до, но не включая 1.

use rand::Rng;

fn main() {
let mut rng = rand::thread_rng();

let n1: u8 = rng.gen();
let n2: u16 = rng.gen();
println!("Произвольное u8: {}", n1);
println!("Произвольное u16: {}", n2);
println!("Произвольное u32: {}", rng.gen::<u32>());
println!("Произвольное i32: {}", rng.gen::<i32>());
println!("Произвольное число с плавающей точкой: {}", rng.gen::<f64>());
}

Генерация произвольных чисел в заданном диапазоне

Пример генерации произвольного числа в диапазоне [0, 10) (не включая 10) с помощью метода Rng::gen_range:

use rand::Rng;

fn main() {
let mut rng = rand::thread_rng();
println!("Целое число: {}", rng.gen_range(0..10));
println!("Число с плавающей точкой: {}", rng.gen_range(0.0..10.0));
}

Структура Uniform позволяет генерировать равномерно распределенные значения. Результат такой же, но операция может выполняться быстрее при повторной генерации чисел в аналогичном диапазоне.

use rand::distributions::{Distribution, Uniform};

fn main() {
let mut rng = rand::thread_rng();
let die = Uniform::from(1..7);

loop {
let throw = die.sample(&mut rng);
println!("Результат броска кубика: {}", throw);
if throw == 6 {
break;
}
}
}

Генерация произвольных чисел с заданным распределением

По умолчанию произвольные числа в крейте rand имеют равномерное распределение (uniform distribution). Крейт rand_distr предоставляет другие виды распределения. Сначала создается экземпляр распределения, затем - образец распределения с помощью метода Distribution::sample, которому передается генератор rand::Rang.

Список доступных распределений.

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

use rand_distr::{Distribution, Normal, NormalError};
use rand::thread_rng;

fn main() -> Result<(), NormalError> {
let mut rng = thread_rng();
let normal = Normal::new(2.0, 3.0)?;
let v = normal.sample(&mut rng);
println!("{} из N(2, 9) распределения", v);
Ok(())
}

Генерация произвольных значений кастомного типа

Пример произвольной генерации кортежа (i32, bool, f64) и переменной пользовательского типа Point. Для произвольной генерации на типе Point реализуется трейт Distribution для структуры Standard.

use rand::Rng;
use rand::distributions::{Distribution, Standard};

#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}

impl Distribution<Point> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Point {
let (rand_x, rand_y) = rng.gen();

Point {
x: rand_x,
y: rand_y,
}
}
}

fn main() {
let mut rng = rand::thread_rng();
let rand_tuple = rng.gen::<(i32, bool, f64)>();
let rand_point: Point = rng.gen();
println!("Произвольный кортеж: {:?}", rand_tuple);
println!("Произвольная структура Point: {:?}", rand_point);
}

Генерация произвольного пароля из набора букв и чисел

Пример генерации строки заданной длины, состоящей из символов ASCII в диапазоне A-Z, a-z, 0-9, с помощью образца Alphanumeric:

use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;

fn main() {
let rand_string: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();

println!("{}", rand_string);
}

Генерация произвольного пароля из набора пользовательских символов

Пример генерации строки заданной длины, состоящей из символов ASCII кастомной пользовательской байтовой строкой, с помощью метода Rng::gen_range:

fn main() {
use rand::Rng;
// Набор пользовательских символов
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz\
0123456789)(*&^%$#@!~";
const PASSWORD_LEN: usize = 30;
let mut rng = rand::thread_rng();

let password: String = (0..PASSWORD_LEN)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();

println!("{:?}", password);
}

Несколько утилит, которые могут оказаться полезными:

use rand::distributions::{
uniform::{SampleRange, SampleUniform},
Distribution,
Standard,
};
use rand::Rng;

/// Генерирует произвольное число указанного типа
pub fn generate_random_number<T>() -> T
where
Standard: Distribution<T>,
{
let mut rng = rand::thread_rng();
rng.gen::<T>()
}

/// Генерирует произвольное число в указанном диапазоне
pub fn generate_random_number_in_range<T, R>(range: R) -> T
where
T: SampleUniform,
R: SampleRange<T>,
{
let mut rng = rand::thread_rng();
rng.gen_range(range)
}

#[derive(Debug)]
pub struct Point {
pub x: i32,
pub y: i32,
}

impl Distribution<Point> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Point {
let (rand_x, rand_y) = rng.gen();
Point {
x: rand_x,
y: rand_y,
}
}
}

/// Генерирует произвольную структуру `Point`
pub fn generate_random_point() -> Point {
let mut rng = rand::thread_rng();
rng.gen()
}

const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz\
0123456789)(*&^%$#@!~";

// Генерирует произвольную строку
pub fn generate_random_string(length: u8) -> String {
let mut rng = rand::thread_rng();
let len = CHARSET.len();

(0..length)
.map(|_| {
let i = rng.gen_range(0..len);
CHARSET[i] as char
})
.collect()
}

/// Генерирует произвольный вектор
pub fn generate_random_vector<T>(n: usize) -> Vec<T>
where
Standard: Distribution<T>,
{
let mut numbers = Vec::new();
for _ in 0..n {
numbers.push(generate_random_number());
}
numbers
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_generate_random_number() {
let n1 = generate_random_number::<i8>();
assert!(n1 >= -128 && n1 < 127);
let n2 = generate_random_number::<u16>();
assert!(n2 > 0);
}

#[test]
fn test_generate_random_number_range() {
let n1 = generate_random_number_range(0..10);
assert!(n1 >= 0 && n1 < 10);
let n2 = generate_random_number_range(10..=20);
assert!(n2 > 0 && n2 <= 20);
}

#[test]
fn test_generate_random_point() {
let rand_point: Point = generate_random_point();
assert!(
rand_point.x >= -2_147_483_648
&& rand_point.x < 2_147_483_647
&& rand_point.y >= -2_147_483_648
&& rand_point.y < 2_147_483_647
);
}

#[test]
fn test_generate_random_string() {
let rand_string = generate_random_string(10);
assert!(rand_string.len() == 10 && rand_string.is_ascii());
}

#[test]
fn test_generate_random_vec() {
let rand_vec = generate_random_vector::<u16>(10);
assert!(rand_vec.len() == 10);
}
}

1.2. Сортировка векторов

Сортировка вектора целых чисел

Пример сортировки вектора целых чисел с помощью метода vec::sort. Метод vec::sort_unstable может быть быстрее, но не гарантирует порядок одинаковых элементов.

fn main() {
let mut vec = vec![1, 5, 10, 2, 15];

vec.sort();

assert_eq!(vec, vec![1, 2, 5, 10, 15]);
}

Сортировка вектора чисел с плавающей точкой

Вектор чисел с плавающей точкой может быть отсортирован с помощью методов vec::sort_by и PartialOrd::partial_cmp:

fn main() {
let mut vec = vec![1.1, 1.15, 5.5, 1.123, 2.0];

vec.sort_by(|a, b| a.partial_cmp(b).unwrap());

assert_eq!(vec, vec![1.1, 1.123, 1.15, 2.0, 5.5]);
}

Сортировка вектора структур

Пример сортировки вектора структур Person со свойствами name и age в естественном порядке (по имени и возрасту). Для того, чтобы сделать Person сортируемой, требуется реализация четырех трейтов: Eq, PartialEq, Ord и PartialOrd. Эти трейты могут быть реализованы автоматически (derived). Для сортировки только по возрасту с помощью метода vec::sort_by необходимо реализовать кастомную функцию сравнения.

#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
struct Person {
name: String,
age: u32
}

impl Person {
pub fn new(name: String, age: u32) -> Self {
Person {
name,
age
}
}
}

fn main() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
];

// Сортируем людей в естественном порядке (по имени и возрасту)
people.sort();

assert_eq!(
people,
vec![
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
Person::new("Zoe".to_string(), 25),
]);

// Сортируем людей по возрасту
people.sort_by(|a, b| b.age.cmp(&a.age));

assert_eq!(
people,
vec![
Person::new("Al".to_string(), 60),
Person::new("Zoe".to_string(), 25),
Person::new("John".to_string(), 1),
]);

}

Несколько утилит, которые могут оказаться полезными:

/// Сортирует целые числа
pub fn sort_integers<T: std::cmp::Ord>(numbers: &mut Vec<T>) {
numbers.sort();
}

/// Сортирует числа с плавающей точкой
pub fn sort_floats<T: std::cmp::PartialOrd>(numbers: &mut Vec<T>) {
numbers.sort_by(|a, b| a.partial_cmp(b).unwrap());
}

/// Сортирует по полю
pub fn sort_by_field<T, F>(vector: &mut Vec<T>, compare: F)
where
F: FnMut(&T, &T) -> core::cmp::Ordering,
{
vector.sort_by(compare)
}

#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Person {
pub name: String,
pub age: u32,
}

impl Person {
pub fn new(name: String, age: u32) -> Self {
Person { name, age }
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_sort_integers() {
let mut numbers = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];
sort_integers(&mut numbers);
assert_eq!(numbers, vec![1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]);
}

#[test]
fn test_sort_floats() {
let mut numbers = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0, 5.0, 3.0, 5.0];
sort_floats(&mut numbers);
assert_eq!(
numbers,
vec![1.0, 1.0, 2.0, 3.0, 3.0, 4.0, 5.0, 5.0, 5.0, 6.0, 9.0]
);
}

#[test]
fn test_sort_persons() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
];
sort_integers(&mut people);
assert_eq!(
people,
vec![
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
Person::new("Zoe".to_string(), 25),
]
)
}

#[test]
fn test_sort_by_field() {
let mut people = vec![
Person::new("Zoe".to_string(), 25),
Person::new("Al".to_string(), 60),
Person::new("John".to_string(), 1),
];
sort_by_field(&mut people, |a, b| b.age.cmp(&a.age));
assert_eq!(
people,
vec![
Person::new("Al".to_string(), 60),
Person::new("Zoe".to_string(), 25),
Person::new("John".to_string(), 1),
]
)
}
}

2. Командная строка

Разбор аргументов командной строки

Пример разбора (parsing) аргументов командной строки с помощью крейта clap:

use clap::{Arg, Command};

fn main() {
let matches = Command::new("My Test Program")
.version("0.1.0")
.author("Harry Heman")
.about("Command line argument parsing")
// файл
.arg(
Arg::new("file")
// короткий флаг -f
.short('f')
// длинный флаг --file
.long("file")
.help("Файл"),
)
// число
.arg(
Arg::new("num")
.short('n')
.long("number")
.help("Ваше любимое число"),
)
.get_matches();

let myfile = matches.get_one::<String>("file").unwrap();
println!("Файл: {}", myfile);

let num_str = matches.get_one::<String>("num");
match num_str {
None => println!("Ваше любимое число неизвестно"),
Some(s) => match s.parse::<i32>() {
Ok(n) => println!("Ваше любимое число: {}", n),
Err(_) => println!("Это не число: {}", s),
},
}
}

Команда для запуска программы:

cargo run -- -f myfile.txt -n 42

Вывод:

Файл: myfile.txt
Ваше любимое число: 42

Большое количество примеров Rust CLI можно найти здесь.

Терминал ANSI

Пример использования крейта ansi_term для управления цветом и форматированием текста в терминале.

ansi_term предоставляет две основные структуры: ANSIString и Style. Style содержит информацию о стилях: цвет, вес текста и др. Существуют также варианты Colour (британский вариант color), представляющие простые цвета текста. ANSIString - это пара из строки и Style.

Печать цветного текста

use ansi_term::Colour;

fn main() {
println!("This is {} in color, {} in color and {} in color",
Colour::Red.paint("red"),
Colour::Blue.paint("blue"),
Colour::Green.paint("green"));
}

Печать жирного текста

Для более сложной стилизации, чем изменение цвета, необходимо использовать экземпляр Style. Он создается с помощью метода Style::new.

use ansi_term::Style;

fn main() {
println!("{} and this is not",
Style::new().bold().paint("This is Bold"));
}

Печать жирного и цветного текста

Colour реализует множество методов, похожих на методы Style.

use ansi_term::Colour;
use ansi_term::Style;

fn main(){
println!("{}, {} and {}",
Colour::Yellow.paint("This is colored"),
Style::new().bold().paint("this is bold"),
Colour::Yellow.bold().paint("this is bold and colored"));
}

3. Сжатие

3.2. Работа с tarball

Распаковка tarball

Пример распаковки (GzDecoder) и извлечения (Archive::unpack) всех файлов из сжатого tarball archive.tar.gz, находящего в текущей рабочей директории:

use flate2::read::GzDecoder;
use std::fs::File;
use tar::Archive;

fn main() -> Result<(), std::io::Error> {
// Название архива (путь к нему)
let path = "archive.tar.gz";
// Открываем файл (создаем дескриптор файла)
let tar_gz = File::open(path)?;
// Создаем экземпляр "распаковщика" архива, передавая в конструктор дескриптор файла
let tar = GzDecoder::new(tar_gz);
// Создаем экземпляр дескриптора архива
let mut archive = Archive::new(tar);
// Извлекаем файлы из архива в текущую директорию
archive.unpack(".")?;

Ok(())
}

Сжатие директории директорию в tarball

Пример сжатия директории /var/log в archive.tar.gz.

Создаем File, обернутый в GzEncoder и tar::Builder. Рекурсивно помещаем содержимое директории /var/log в архив, находящийся в backup/logs с помощью Builder::append_dir_all. GzEncoder отвечает за сжатие данных перед их записью в archive.tar.gz.

use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::File;

fn main() -> Result<(), std::io::Error> {
// Создаем дескриптор файла
let tar_gz = File::create("archive.tar.gz")?;
// Создаем экземпляр "упаковщика" архива, передавая в конструктор
// дескриптор файла и метод сжатия
let enc = GzEncoder::new(tar_gz, Compression::default());
// Создаем дескриптор архива
let mut tar = tar::Builder::new(enc);
// Записываем файлы из директории `backup/logs` в архив `archive.tar.gz` и
// помещаем его в директорию `/var/log`
tar.append_dir_all("backup/logs", "/var/log")?;
Ok(())
}

Распаковка tarball с удалением префикса пути

Перебираем файлы с помощью метода Archive::entries. Используем метод Path::strip_prefix для удаления префикса пути (bundle/logs). Наконец, извлекаем tar::Entry с помощью метода Entry::unpack.

use error_chain::error_chain;
use std::fs::File;
use std::path::PathBuf;
use flate2::read::GzDecoder;
use tar::Archive;

error_chain! {
foreign_links {
Io(std::io::Error);
StripPrefixError(std::path::StripPrefixError);
}
}

fn main() -> Result<()> {
let file = File::open("archive.tar.gz")?;
let mut archive = Archive::new(GzDecoder::new(file));
let prefix = "bundle/logs";

println!("Extracted the following files:");
archive
// перебираем файлы
.entries()?
.filter_map(|e| e.ok())
// для каждого файла
.map(|mut entry| -> Result<PathBuf> {
// удаляем префикс пути
let path = entry.path()?.strip_prefix(prefix)?.to_owned();
// распаковываем
entry.unpack(&path)?;
// возвращаем путь
Ok(path)
})
.filter_map(|e| e.ok())
.for_each(|x| println!("> {}", x.display()));

Ok(())
}

4. Параллелизм

4.1. Явные потоки

В следующем примере используется крейт crossbeam, который предоставляет структуры данных и функции для конкурентного (concurrent) и параллельного (parallel) программирования. Метод Scope::spawn создает (выделяет) новый поток с ограниченной областью видимости (scoped thread), который гарантированно завершается до возврата из замыкания, передаваемого в функцию crossbeam::scope, позволяя безопасно ссылаться на данные из вызывающей функции.

Делим массив пополам и обрабатываем половины в отдельных потоках:

fn main() {
let arr = &[1, 25, -4, 10];
let max = find_max(arr);
assert_eq!(max, Some(25));
}

fn find_max(arr: &[i32]) -> Option<i32> {
const THRESHOLD: usize = 2;

if arr.len() <= THRESHOLD {
// `iter()` создает итератор массива (тип значения - `Some(&i32)`)
// `cloned()` - создает новый итератор, клонирующий значения предыдущего итератора
// (`&T` преобразуется в `T`, типом значения становится - `Some(i32)`)
// `max()` - возвращает максимальный элемент итератора
return arr.iter().cloned().max();
}

// Делим массив пополам
let mid = arr.len() / 2;
let (left, right) = arr.split_at(mid);

crossbeam::scope(|s| {
// Создаем параллельные потоки для обработки левой и правой частей массива
let thread_l = s.spawn(|_| find_max(left));
let thread_r = s.spawn(|_| find_max(right));

// Получаем максимальные значения из потоков
// `join()` - ожидает завершения потока
// (заставляет основной поток ждать завершения выделенного потока)
// и возвращает его результат
let max_l = thread_l.join().unwrap()?;
let max_r = thread_r.join().unwrap()?;

// `max()` сравнивает и возвращает максимальное из двух значений
Some(max_l.max(max_r))
})
.unwrap()
}

Создание параллельного конвейера

В следующем примере используются крейты crossbeam и crossbeam_channel для создания параллельного конвейера (pipeline). Есть источник данных (sender), приемник данных (receiver) и данные, которые обрабатываются двумя параллельными рабочими потоками (worker threads, workers) на пути от источника к приемнику.

Мы используем связанные (bounded) каналы с емкостью равной 1, создаваемые с помощью метода crossbeam_channel::bounded. Производитель должен находиться в собственном потоке, поскольку он производит сообщения чаще, чем "воркеры" могут их обработать (они спят в течение 500 мс). Это означает, что производитель блокируется на 500 мс при вызове crossbeam_channel::Sender::send, пока один из воркеров не обработает данные в канале. Также обратите внимание, что данные потребляются любым воркером, который получил их первым, поэтому каждое сообщение доставляется какому-то одному воркеру, а не обоим.

Чтение из каналов через итератор crossbeam_channel::Receiver::iter блокируется в ожидании новых сообщений или закрытия канала. Поскольку каналы были созданы с помощью crossbeam::scope, они должны закрываться вручную с помощью drop во избежание блокировки всей программы циклами for воркеров. О вызовах drop можно думать как о сигналах того, что сообщений в канале больше не будет.

use std::thread;
use std::time::Duration;
use crossbeam_channel::bounded;

fn main() {
let (snd1, rcv1) = bounded(1);
let (snd2, rcv2) = bounded(1);
let n_msgs = 4;
let n_workers = 2;

crossbeam::scope(|s| {
// Поток производителя
s.spawn(|_| {
for i in 0..n_msgs {
snd1.send(i).unwrap();
println!("Source sent {}", i);
}
// Закрываем канал - это необходимо для выхода
// из цикла `for` в воркере
drop(snd1);
});

// Параллельная обработка двумя потоками/воркерами
for _ in 0..n_workers {
// Отправляем в приемник, получаем из источника
let (sendr, recvr) = (snd2.clone(), rcv1.clone());
// Создаем воркеров в отдельных потоках
s.spawn(move |_| {
thread::sleep(Duration::from_millis(500));
// Получаем сообщения до закрытия канала
for msg in recvr.iter() {
println!("Worker {:?} received {}", thread::current().id(), msg);
sendr.send(msg * 2).unwrap();
}
});
}
// Закрываем канал, иначе приемник никогда не выйдет из цикла `for`
drop(snd2);

// Приемник
for msg in rcv2.iter() {
println!("Sink received {}", msg);
}
}).unwrap();
}

Передача данных между потоками

Следующий пример демонстрирует использование крейта crossbeam_channel в схеме "один производитель - один потребитель" (single producer - single consumer, SPSC). Методы crossbeam::scope и Scope::spawn используются для управления потоком производителя. Данные передаются из одного потока в другой через канал crossbeam_channel::unbounded. Это означает, что количество валидных (пригодных для хранения) сообщений не ограничено. Поток производителя спит 100 мс между сообщениями.

use std::{thread, time};
use crossbeam_channel::unbounded;

fn main() {
let (snd, rcv) = unbounded();
let n_msgs = 5;

crossbeam::scope(|s| {
s.spawn(|_| {
for i in 0..n_msgs {
snd.send(i).unwrap();
thread::sleep(time::Duration::from_millis(100));
}
});
}).unwrap();

for _ in 0..n_msgs {
let msg = rcv.recv().unwrap();
println!("{}", msg);
}
}

Глобальное мутабельное состояние

Пример создания глобального состояния с помощью крейта lazy_static. Макрос lazy_static! создает доступную глобально static ref, мутирование которой требует Mutex (см. также RwLock). Обертка Mutex гарантирует, что состояние может быть одновременно доступно только одному потоку, что позволяет избежать гонки за данными. Для чтения или изменения значения, хранящегося в Mutex, используется MutexGuard.

use error_chain::error_chain;
use lazy_static::lazy_static;
use std::sync::Mutex;

error_chain! {}

lazy_static! {
static ref FRUIT: Mutex<Vec<String>> = Mutex::new(Vec::new());
}

fn insert(fruit: &str) -> Result<()> {
let mut db = FRUIT.lock().map_err(|_| "Failed to acquire MutexGuard")?;
db.push(fruit.to_string());
Ok(())
}

fn main() -> Result<()> {
insert("apple")?;
insert("orange")?;
insert("peach")?;
{
// Блокируем запись новых значений
let db = FRUIT.lock().map_err(|_| "Failed to acquire MutexGuard")?;

db.iter()
.enumerate()
.for_each(|(i, item)| println!("{}: {}", i, item));
}
insert("grape")?;
Ok(())
}

4.2. Параллельная обработка данных

Параллельная модификация элементов массива

В следующем примере используется rayon - библиотека Rust для параллельной обработки данных. rayon предоставляет метод par_iter_mut для любого параллельно перебираемого типа данных. par_iter_mut возвращает подобную итератору цепочку (iterator-like chain), которая потенциально выполняется параллельно.

use rayon::prelude::*;

fn main() {
let mut arr = [0, 7, 9, 11];
arr.par_iter_mut().for_each(|p| *p -= 1);
println!("{:?}", arr);
}

Параллельный поиск совпадения элемента коллекции с предикатом

Следующий пример демонстрирует использование методов rayon::any и rayon::all, которые являются "параллельными" аналогами std::any и std::all. rayon::any параллельно проверяет, совпадает ли какой-нибудь элемент итератора с предикатом и возвращается, как только такой элемент обнаружен. rayon::all параллельно проверяет, совпадают ли все элементы итератора с предикатом и возвращается, как только обнаружен несовпадающий элемент.

use rayon::prelude::*;

fn main() {
let mut vec = vec![2, 4, 6, 8];

assert!(!vec.par_iter().any(|n| (*n % 2) != 0));
assert!(vec.par_iter().all(|n| (*n % 2) == 0));
assert!(!vec.par_iter().any(|n| *n > 8 ));
assert!(vec.par_iter().all(|n| *n <= 8 ));

vec.push(9);

assert!(vec.par_iter().any(|n| (*n % 2) != 0));
assert!(!vec.par_iter().all(|n| (*n % 2) == 0));
assert!(vec.par_iter().any(|n| *n > 8 ));
assert!(!vec.par_iter().all(|n| *n <= 8 ));
}

Параллельный поиск элемента

В следующем примере мы используем методы rayon::find_any и par_iter для поиска элемента в векторе, который удовлетворяет предикату в замыкании.

rayon::find_any возвращает первый элемент, совпавший с предикатом.

Обратите внимание, что аргумент в замыкании - это ссылка на ссылку (&&x). См. обсуждение этого в std::find.

use rayon::prelude::*;

fn main() {
let v = vec![6, 2, 1, 9, 3, 8, 11];

let f1 = v.par_iter().find_any(|&&x| x == 9);
let f2 = v.par_iter().find_any(|&&x| x % 2 == 0 && x > 6);
let f3 = v.par_iter().find_any(|&&x| x > 8);

assert_eq!(f1, Some(&9));
assert_eq!(f2, Some(&8));
assert!(f3 > Some(&8));
}

Параллельная сортировка вектора

Следующий пример является демонстрацией параллельной сортировки вектора строк.

Создаем вектор пустых строк. Метод par_iter_mut().for_each параллельно заполняет вектор произвольными значениями. Хотя существует несколько вариантов сортировки перечисляемого типа данных, par_iter_unstable, обычно, быстрее, чем алгоритмы стабильной сортировки.

use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rayon::prelude::*;

fn main() {
let mut vec = vec![String::new(); 100];
vec.par_iter_mut().for_each(|p| {
let mut rng = thread_rng();

*p = (0..5)
// Заполняем вектор произвольными значениями
.map(|_| rng.sample(&Alphanumeric))
.map(char::from)
.collect()
});
vec.par_sort_unstable();
println!("{:?}", vec);
}

Параллельный map-reduce

В следующем примере мы используем методы rayon::filter, rayon::map и rayon::reduce для вычисления среднего возраста людей (объект Person) старше 30 (поле age).

rayon::filter возвращает элементы коллекции, удовлетворяющие предикату. rayon::map выполняет операцию с каждым элементом, создавая новую итерацию. rayon::reduce выполняет операцию с результатом предыдущих операций и текущим элементом. Применение метода rayon::sum приводит к аналогичному результату.

use rayon::prelude::*;

struct Person {
age: u32,
}

fn main() {
let v: Vec<Person> = vec![
Person { age: 23 },
Person { age: 19 },
Person { age: 42 },
Person { age: 17 },
Person { age: 17 },
Person { age: 31 },
Person { age: 30 },
];

let num_over_30 = v.par_iter().filter(|&x| x.age > 30).count() as f32;
let sum_over_30 = v.par_iter()
.map(|x| x.age)
.filter(|&x| x > 30)
.reduce(|| 0, |x, y| x + y);

let alt_sum_30: u32 = v.par_iter()
.map(|x| x.age)
.filter(|&x| x > 30)
.sum();

let avg_over_30 = sum_over_30 as f32 / num_over_30;
let alt_avg_over_30 = alt_sum_30 as f32/ num_over_30;

// Особенности арифметики чисел с плавающей точкой
assert!((avg_over_30 - alt_avg_over_30).abs() < std::f32::EPSILON);
println!("The average age of people older than 30 is {}", avg_over_30);
}

Параллельная генерация миниатюр в формате JPG

В следующем примере мы генерируем миниатюры для всех файлов .jpg в текущей директории и сохраняем их в директории thumbnails.

Метод glob::glob_with ищет файлы .jpg в текущей директории. rayon меняет размеры изображений с помощью метода DynamicImage::resize. Он делает это параллельно с помощью метода par_iter.

use error_chain::error_chain;

use std::fs::create_dir_all;
use std::path::Path;

use error_chain::ChainedError;
use glob::{glob_with, MatchOptions};
use image::{imageops::FilterType, ImageError};
use rayon::prelude::*;

error_chain! {
foreign_links {
Image(ImageError);
Io(std::io::Error);
Glob(glob::PatternError);
}
}

fn main() -> Result<()> {
let options: MatchOptions = Default::default();
// Находим все файлы JPG в текущей директории
let files: Vec<_> = glob_with("*.jpg", options)?
.filter_map(|x| x.ok())
.collect();

// Если таких файлов нет
if files.len() == 0 {
error_chain::bail!("No .jpg files found in current directory");
}

// Создаем директорию назначения
let thumb_dir = "thumbnails";
create_dir_all(thumb_dir)?;

println!("Saving {} thumbnails into '{}'...", files.len(), thumb_dir);

// Вектор провалов создания миниатюр
let image_failures: Vec<_> = files
.par_iter()
.map(|path| {
// Создаем миниатюру изображения
make_thumbnail(path, thumb_dir, 300)
.map_err(|e| e.chain_err(|| path.display().to_string()))
})
.filter_map(|x| x.err())
.collect();

image_failures
.iter()
.for_each(|x| println!("{}", x.display_chain()));

println!(
"{} thumbnails saved successfully",
files.len() - image_failures.len()
);
Ok(())
}

fn make_thumbnail<PA, PB>(original: PA, thumb_dir: PB, longest_edge: u32) -> Result<()>
where
PA: AsRef<Path>,
PB: AsRef<Path>,
{
// Дескриптор изображения
let img = image::open(original.as_ref())?;
// Путь к файлу
let file_path = thumb_dir.as_ref().join(original);

// Пропорционально меняем размер изображения до 300 пикселей и
// записываем его в директорию назначения
Ok(img
.resize(longest_edge, longest_edge, FilterType::Nearest)
.save(file_path)?)
}

5. Криптография

5.1. Хеширование

Вычисление контрольной суммы файла с помощью алгоритма SHA-256

Записываем данные в файл, затем вычисляем SHA-256 digest::Digest содержимого файла с помощью digest::Context:

use error_chain::error_chain;
use data_encoding::HEXUPPER;
use ring::digest::{Context, Digest, SHA256};
use std::fs::File;
use std::io::{BufReader, Read, Write};

error_chain! {
foreign_links {
Io(std::io::Error);
Decode(data_encoding::DecodeError);
}
}

/// Создает digest содержимого файла с помощью алгоритма SHA-256
fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest> {
let mut context = Context::new(&SHA256);
let mut buffer = [0; 1024];

loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
context.update(&buffer[..count]);
}

Ok(context.finish())
}

fn main() -> Result<()> {
let path = "file.txt";

// Создаем дескриптор файла
let mut output = File::create(path)?;
// Записываем данные в файл
write!(output, "We will generate a digest of this text")?;

let input = File::open(path)?;
// Читаем данные из файла
let reader = BufReader::new(input);
// Создаем digest
let digest = sha256_digest(reader)?;

println!("SHA-256 digest is {}", HEXUPPER.encode(digest.as_ref()));

Ok(())
}

Подпись и подтверждение сообщения с помощью HMAC digest

Используем ring::hmac для создания структуры hmac::Signature из строки, затем проверяем корректность подписи:

use ring::{hmac, rand};
use ring::rand::SecureRandom;
use ring::error::Unspecified;

fn main() -> Result<(), Unspecified> {
let mut key_value = [0u8; 48];
let rng = rand::SystemRandom::new();
rng.fill(&mut key_value)?;
let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value);

let message = "Legitimate important message";
// Подписываем строку
let signature = hmac::sign(&key, message.as_bytes());
// Проверяем корректность подписи
hmac::verify(&key, message.as_bytes(), signature.as_ref())?;

Ok(())
}

5.2. Шифрование

Соление и хеширование пароля с помощью PBKDF2

Используем ring::pbkdf2 для хеширования "засоленного" (salted) пароля с помощью функции получения ключа pbkdf2::derive. Проверяем корректность хеша с помощью метода pbkdf2::verify. Соль генерируется с помощью функции SecureRandom::fill, которая заполняет массив байтов соли безопасно сгенерированными числами.

use data_encoding::HEXUPPER;
use ring::error::Unspecified;
use ring::rand::SecureRandom;
use ring::{digest, pbkdf2, rand};
use std::num::NonZeroU32;

fn main() -> Result<(), Unspecified> {
const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN;
let n_iter = NonZeroU32::new(100_000).unwrap();
let rng = rand::SystemRandom::new();

let mut salt = [0u8; CREDENTIAL_LEN];
rng.fill(&mut salt)?;

let password = "Guess Me If You Can!";
let mut pbkdf2_hash = [0u8; CREDENTIAL_LEN];
// Хешируем пароль
pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA512,
n_iter,
&salt,
password.as_bytes(),
&mut pbkdf2_hash,
);
println!("Salt: {}", HEXUPPER.encode(&salt));
println!("PBKDF2 hash: {}", HEXUPPER.encode(&pbkdf2_hash));
// Проверяем корректность хеша
let should_succeed = pbkdf2::verify(
pbkdf2::PBKDF2_HMAC_SHA512,
n_iter,
&salt,
password.as_bytes(),
&pbkdf2_hash,
);
let wrong_password = "Definitely not the correct password";
let should_fail = pbkdf2::verify(
pbkdf2::PBKDF2_HMAC_SHA512,
n_iter,
&salt,
wrong_password.as_bytes(),
&pbkdf2_hash,
);

assert!(should_succeed.is_ok());
assert!(!should_fail.is_ok());

Ok(())
}

7. База данных

7.1. SQLite

Создание БД SQLite

Пример использования крейта rusqlite для подключения/создания БД SQLite. При возникновении проблем с компиляцией кода в Windows, ищите решение здесь.

Метод Connection::open создает БД при отсутствии.

use rusqlite::{Connection, Result};

fn main() -> Result<()> {
// Подключаемся к/создаем БД `cats`
let conn = Connection::open("cats.sqlite")?;

// Создаем таблицу `cat_colors`
conn.execute(
"create table if not exists cat_colors (
id integer primary key,
name text not null unique
)",
(),
)?;
// Создаем таблицу `cats`
conn.execute(
"create table if not exists cats (
id integer primary key,
name text not null,
color_id integer not null references cat_colors(id)
)",
(),
)?;

Ok(())
}

Добавление и извлечение данных

Метод Connection::open открывает БД cats, созданную в предыдущем примере. Метод Connection::execute добавляет данные в таблицы cat_colors и cats. После добавления записи о цвете, метод Connection::last_insert_rowid используется для получения id последней добавленной записи. Этот id используется для добавления записи в таблицу cats. Затем с помощью метода prepare готовится запрос выборки данных (select query). prepare возвращает структуру Statement. Выборка выполняется с помощью метода Statement::query_map.

use rusqlite::{Connection, Result};
use std::collections::HashMap;

#[derive(Debug)]
struct Cat {
name: String,
color: String,
}

fn main() -> Result<()> {
// Подключаемся к БД `cats`
let conn = Connection::open("cats.sqlite")?;

let mut cat_colors = HashMap::new();
cat_colors.insert(String::from("Blue"), vec!["Tigger", "Sammy"]);
cat_colors.insert(String::from("Black"), vec!["Oreo", "Biscuit"]);

// Перебираем записи карты
for (color, catnames) in &cat_colors {
// Записываем данные в таблицу `cat_colors`
conn.execute(
"INSERT INTO cat_colors (name) values (?1)",
&[&color.to_string()],
)?;
let last_id: String = conn.last_insert_rowid().to_string();

// Перебираем имена кошек
for cat in catnames {
// Записываем данные в таблицу `cats`
conn.execute(
"INSERT INTO cats (name, color_id) values (?1, ?2)",
&[&cat.to_string(), &last_id],
)?;
}
}
// Запрос выборки данных
let mut stmt = conn.prepare(
"SELECT c.name, cc.name from cats c
INNER JOIN cat_colors cc
ON cc.id = c.color_id;",
)?;
// Извлекаем данные
let cats = stmt.query_map((), |row| {
Ok(Cat {
name: row.get(0)?,
color: row.get(1)?,
})
})?;

for cat in cats {
println!("Found cat {:?}", cat);
}

Ok(())
}

Использование транзакций

Метод Connection::open выполняет подключение к БД cats.

Метод Connection::transaction запускает транзакцию. Транзакции откатываются, если не фиксируются явно с помощью метода Transaction::commit.

В следующем примере цвета добавляются в таблицу, которая имеет уникальное ограничение на название цвета. При попытке добавить дублирующийся цвет транзакция откатывается (roll back).

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

use rusqlite::{Connection, Result};

#[derive(Debug)]
struct Cat {
name: String,
color: String,
}

fn main() -> Result<()> {
// Подключаемся к БД `cats`
let mut conn = Connection::open("cats.sqlite")?;
// Выполняем успешную транзакцию
successful_tx(&mut conn)?;
// Выполняем "откатывающуюся" транзакцию
let res = rolled_back_tx(&mut conn);
assert!(res.is_err());

Ok(())
}

// Успешная транзакция
fn successful_tx(conn: &mut Connection) -> Result<()> {
let tx = conn.transaction()?;

tx.execute("delete from cat_colors", ())?;
tx.execute("insert into cat_colors (name) values (?1)", &[&"lavender"])?;
tx.execute("insert into cat_colors (name) values (?1)", &[&"blue"])?;

tx.commit()
}
// Откатывающаяся транзакция
fn rolled_back_tx(conn: &mut Connection) -> Result<()> {
let tx = conn.transaction()?;

tx.execute("delete from cat_colors", ())?;
tx.execute("insert into cat_colors (name) values (?1)", &[&"lavender"])?;
tx.execute("insert into cat_colors (name) values (?1)", &[&"blue"])?;
// Попытка добавить дубликат приводит к ошибке и откату транзакции
tx.execute("insert into cat_colors (name) values (?1)", &[&"lavender"])?;

tx.commit()
}

7.2. Postgres

Создание таблицы

Для работы с БД PostgreSQL используется крейт postgres.

Метод Client::connect используется для подключения к существующей БД. В следующем примере для подключения к БД используется URL БД в виде строки. Предполагается наличие БД library, для доступа к которой используются имя пользователя postgres и пароль postgres.

use postgres::{Client, NoTls, Error};

fn main() -> Result<(), Error> {
// Подключаемся к БД `library`
let mut client = Client::connect("postgresql://postgres:postgres@localhost/library", NoTls)?;

// Создаем таблицу `author`
client.batch_execute("
CREATE TABLE IF NOT EXISTS author (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
country VARCHAR NOT NULL
)
")?;
// Создаем таблицу `book`
client.batch_execute("
CREATE TABLE IF NOT EXISTS book (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
author_id INTEGER NOT NULL REFERENCES author
)
")?;

Ok(())
}

Добавление и извлечение данных

Для добавления данных в таблицу используется метод Client::execute. Для извлечения данных - метод Client::query:

use postgres::{Client, Error, NoTls};
use std::collections::HashMap;

struct Author {
_id: i32,
name: String,
country: String,
}

fn main() -> Result<(), Error> {
// Подключаемся к БД `library`
let mut client = Client::connect("postgresql://postgres:postgres@localhost/library", NoTls)?;

let mut authors = HashMap::new();
authors.insert(String::from("Chinua Achebe"), "Nigeria");
authors.insert(String::from("Rabindranath Tagore"), "India");
authors.insert(String::from("Anita Nair"), "India");
// Очищаем таблицу `author`
client.execute("DELETE FROM author", &[])?;
// Перебираем записи карты авторов
for (key, value) in &authors {
let author = Author {
_id: 0,
name: key.to_string(),
country: value.to_string(),
};
// Добавляем данные в таблицу `author`
client.execute(
"INSERT INTO author (name, country) VALUES ($1, $2)",
&[&author.name, &author.country],
)?;
}

// Перебираем сущности выборки данных
for row in client.query("SELECT id, name, country FROM author", &[])? {
let author = Author {
_id: row.get(0),
name: row.get(1),
country: row.get(2),
};
println!("Author {} is from {}", author.name, author.country);
}

Ok(())
}

8. Дата и время

8.1. Продолжительность и вычисление даты и времени

Измерение прошедшего времени

Пример измерения time::Instant::elapsed, прошедшего с time::Instant::now.

Метод time::Instant::elapsed возвращает time::Duration. Вызов этого метода не меняет и не сбрасывает объект time::Instant.

use std::time::{Duration, Instant};
use std::thread;

// "Дорогая" с точки зрения вычислений функция
fn expensive_function() {
// Искусственная задержка в 1 сек
thread::sleep(Duration::from_secs(1));
}

fn main() {
let start = Instant::now();
expensive_function();
// Измеряем продолжительность выполнения функции
let duration = start.elapsed();

println!("Time elapsed in expensive_function() is: {:?}", duration);
}

Вычисление даты и времени

Пример вычисления даты и времени через две недели от текущих с помощью DateTime::checked_add_signed и даты предшествующего дня с помощью DateTime::checked_sub_signed. Эти методы возвращают None, если дата и время не могут быть вычислены.

Escape-последовательности, доступные для DateTime::format, можно найти в Chronic::format::strftime.

use chrono::{DateTime, Duration, Utc};

// Функция вычисления даты предшествующего дня
fn day_earlier(date_time: DateTime<Utc>) -> Option<DateTime<Utc>> {
date_time.checked_sub_signed(Duration::days(1))
}

fn main() {
let now = Utc::now();
println!("{}", now);

// Добавляем 2 недели
let almost_three_weeks_from_now = now.checked_add_signed(Duration::weeks(2))
// добавляем неделю
.and_then(|in_2weeks| in_2weeks.checked_add_signed(Duration::weeks(1)))
// вычитаем день
.and_then(day_earlier);

match almost_three_weeks_from_now {
Some(x) => println!("{}", x),
None => eprintln!("Almost three weeks from now overflows!"),
}

// Пытаемся добавить к текущей дате максимальную продолжительность
match now.checked_add_signed(Duration::max_value()) {
Some(x) => println!("{}", x),
None => eprintln!("We can't use chrono to tell the time for the Solar System to complete more than one full orbit around the galactic center."),
}
}

Преобразование локального времени в другую временную зону

Пример получения локального времени с помощью offset::Local::now и его преобразование в UTC с помощью DateTime::from_utc. Затем UTC-время преобразуется в UTC+8 и UTC-2 с помощью offset:FixedOffset.

use chrono::{DateTime, FixedOffset, Local, Utc};

fn main() {
// Локальное время
let local_time = Local::now();
// Время в формате UTC
let utc_time = DateTime::<Utc>::from_utc(local_time.naive_utc(), Utc);
// Время UTC+8
let china_timezone = FixedOffset::east(8 * 3600);
// Время UTC-2
let rio_timezone = FixedOffset::west(2 * 3600);
println!("Local time now is {}", local_time);
println!("UTC time now is {}", utc_time);
println!(
"Time in Hong Kong now is {}",
utc_time.with_timezone(&china_timezone)
);
println!("Time in Rio de Janeiro now is {}", utc_time.with_timezone(&rio_timezone));
}

8.2. Разбор и отображение даты и времени

Получение даты и времени

Пример получения текущего DateTime в формате UTC, его часов/минут/секунд через Timelike и лет/месяцев/дней недели через Datelike:

use chrono::{Datelike, Timelike, Utc};

fn main() {
let now = Utc::now();

let (is_pm, hour) = now.hour12();
println!(
"The current UTC time is {:02}:{:02}:{:02} {}",
hour,
now.minute(),
now.second(),
if is_pm { "PM" } else { "AM" }
);
// Количество секунд, прошедшее с полуночи
println!(
"And there have been {} seconds since midnight",
now.num_seconds_from_midnight()
);

let (is_common_era, year) = now.year_ce();
println!(
"The current UTC date is {}-{:02}-{:02} {:?} ({})",
year,
now.month(),
now.day(),
now.weekday(),
if is_common_era { "CE" } else { "BCE" }
);
// Количество дней, прошедших с начала новой эры
println!(
"And the Common Era began {} days ago",
now.num_days_from_ce()
);
}

Преобразование даты в метку времени UNIX и наоборот

Пример преобразования даты из NaiveDate::from_ymd и NaiveTime::from_hms в метку времени (timestamp) UNIX с помощью NaiveDateTime::timestamp и вычисления даты спустя миллиард секунд после 1970-01-01 0:00:00 UTC с помощью NaiveDateTime::from_timestamp.

use chrono::{NaiveDate, NaiveDateTime};

fn main() {
let date_time: NaiveDateTime = NaiveDate::from_ymd(2017, 11, 12).and_hms(17, 33, 44);
println!(
"Количество секунд между 1970-01-01 00:00:00 и {} равняется {}.",
date_time, date_time.timestamp());

let date_time_after_a_billion_seconds = NaiveDateTime::from_timestamp(1_000_000_000, 0);
println!(
"Дата через миллиард секунд после 1970-01-01 00:00:00: {}.",
date_time_after_a_billion_seconds);
}

Форматирование даты и времени

Пример получения текущей даты в формате UTC с помощью Utc::now и ее форматирование в популярные форматы RFC 2822 с помощью DateTime::to_rfc2822 и RFC 3339 с помощью DateTime::to_rfc3339, а также в кастомный формат с помощью DateTime::format:

use chrono::{DateTime, Utc};

fn main() {
let now: DateTime<Utc> = Utc::now();

println!("UTC now is: {}", now);
println!("UTC now in RFC 2822 is: {}", now.to_rfc2822());
println!("UTC now in RFC 3339 is: {}", now.to_rfc3339());
println!("UTC now in a custom format is: {}", now.format("%a %b %e %T %Y"));
}

Преобразование строки в структуру DateTime

Пример преобразования строк в структуры DateTime, представляющие популярные форматы RFC 2822, RFC 3339 и кастомный формат с помощью DateTime::parse_from_rfc2822, DateTime::parse_from_rfc3339 и DateTime::parse_from_str, соответственно.

Escape-последовательности, доступные для DateTime::parse_from_str можно найти в chrono::format::strftime. Обратите внимание, что DateTime::parse_from_str требует, чтобы создаваемая структура DateTime однозначно идентифицировала дату и время. Для разбора даты и времени без часовых поясов используются NaiveDate, NaiveTime и NaiveDateTime.

use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime};
use chrono::format::ParseError;

fn main() -> Result<(), ParseError> {
let rfc2822 = DateTime::parse_from_rfc2822("Tue, 1 Jul 2003 10:52:37 +0200")?;
println!("{}", rfc2822);

let rfc3339 = DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00")?;
println!("{}", rfc3339);

let custom = DateTime::parse_from_str("5.8.1994 8:00 am +0000", "%d.%m.%Y %H:%M %P %z")?;
println!("{}", custom);

let time_only = NaiveTime::parse_from_str("23:56:04", "%H:%M:%S")?;
println!("{}", time_only);

let date_only = NaiveDate::parse_from_str("2015-09-05", "%Y-%m-%d")?;
println!("{}", date_only);

let no_timezone = NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")?;
println!("{}", no_timezone);

Ok(())
}

9. Инструменты для разработки

9.1. Отладка

Вывод сообщения об отладке в консоль

Крейт log предоставляет разные утилиты логирования. Крейт env_logger позволяет настраивать логирование через переменные среды окружения. Макрос log::debug! работает аналогично std::fmt.

fn execute_query(query: &str) {
log::debug!("Выполнение запроса: {}", query);
}

fn main() {
env_logger::init();

execute_query("DROP TABLE students");
}

При запуске этого кода в консоль ничего не выводится, поскольку дефолтным уровнем логирования является error, а уровни ниже игнорируются.

Для печати сообщений нужно установить переменную среды окружения RUST_LOG в значение debug:

RUST_LOG=debug cargo run

Вывод сообщения об ошибке в консоль

Пример вывода в консоль сообщения об ошибке с помощью макроса log::error!:


fn execute_query(_query: &str) -> Result<(), &'static str> {
Err("Боюсь, я не могу этого сделать")
}

fn main() {
env_logger::init();

let response = execute_query("DROP TABLE students");
if let Err(err) = response {
log::error!("Выполнить запрос не удалось: {}", err);
}
}

Вывод сообщения в stdout вместо stderr

Пример кастомной настройки логера с помощью Builder::target - установка цели вывода сообщения на Target::Stdout:

use env_logger::{Builder, Target};

fn main() {
Builder::new()
.target(Target::Stdout)
.init();

log::error!("Эта ошибка выводится в stdout");
}

Вывод сообщения с помощью кастомного логера

Пример реализации кастомного логера ConsoleLogger, который выводит сообщения в stdout. ConsoleLogger реализует трейт log::Log для того, чтобы иметь возможность использовать макросы логирования. log::set_logger используется для установки ConsoleLogger.

use log::{Record, Level, Metadata, LevelFilter, SetLoggerError};

static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;

struct ConsoleLogger;

impl log::Log for ConsoleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Info
}

fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
println!("Rust говорит: {} - {}", record.level(), record.args());
}
}

fn flush(&self) {}
}

fn main() -> Result<(), SetLoggerError> {
log::set_logger(&CONSOLE_LOGGER)?;
log::set_max_level(LevelFilter::Info);

log::info!("привет");
log::warn!("предупреждение");
log::error!("упс");
Ok(())
}

Определение уровня логирования для модуля

Создаем два модуля: foo и вложенный foo::bar. Определяем уровни логирования для каждого модуля через директивы логирования с помощью переменной среды окружения RUST_LOG.

mod foo {
mod bar {
pub fn run() {
log::warn!("[bar] warn");
log::info!("[bar] info");
log::debug!("[bar] debug");
}
}

pub fn run() {
log::warn!("[foo] warn");
log::info!("[foo] info");
log::debug!("[foo] debug");
bar::run();
}
}

fn main() {
env_logger::init();
log::warn!("[root] warn");
log::info!("[root] info");
log::debug!("[root] debug");
foo::run();
}

RUST_LOG управляет выводом env_logger. Команда для запуска примера может выглядеть так (предполагается, что проект называется test):

RUST_LOG="warn,test::foo=info,test::foo::bar=debug" ./test

Эта команда устанавливает дефолтный log::level в значение warn, уровни логирования в модулях foo и foo::bar в значения info и debug, соответственно.

Вывод:

WARN:test: [root] warn
WARN:test::foo: [foo] warn
INFO:test::foo: [foo] info
WARN:test::foo::bar: [bar] warn
INFO:test::foo::bar: [bar] info
DEBUG:test::foo::bar: [bar] debug

Использование кастомной переменной среды окружения для настройки логирования

Для настройки логирования используется структура Builder.

Метод Builder::parse разбирает переменную среды окружения MY_APP_LOG в синтаксис RUST_LOG. Затем метод Builder::init инициализирует логер. Обычно, все это делается автоматически при вызове метода env_logger::init.

use std::env;
use env_logger::Builder;

fn main() {
Builder::new()
.parse(&env::var("MY_APP_LOG").unwrap_or_default())
.init();

log::info!("информация");
log::warn!("предупреждение");
log::error!("сообщение об {}", "ошибке");
}

Добавление метки времени в сообщение об отладке

Создаем кастомную настройку логера с помощью Builder. Каждая сущность логирования вызывает Local::now для получения текущего DateTime в локальной временной зоне и использует DateTime::format с strftime:specifiers для формирования метки времени, которая используется в финальном выводе.

Метод Builder::format вызывается для установки замыкания, которое форматирует каждое сообщение, добавляя в него метку времени, Record::level и тело (Record::args).

use std::io::Write;
use chrono::Local;
use env_logger::Builder;
use log::LevelFilter;

fn main() {
Builder::new()
.format(|buf, record| {
writeln!(buf,
"{} [{}] - {}",
Local::now().format("%Y-%m-%dT%H:%M:%S"),
record.level(),
record.args()
)
})
.filter(None, LevelFilter::Info)
.init();

log::warn!("warn");
log::info!("info");
log::debug!("debug");
}

9.2. Версионирование

Разбор и увеличение версии

Создаем структуру semver::Version из строкового литерала с помощью метода Version::parse, затем увеличиваем патчевый, минорный и мажорный номера версии один за другим.

use semver::{BuildMetadata, Error, Prerelease, Version};

fn main() -> Result<(), Error> {
let parsed_version = Version::parse("0.2.6")?;

assert_eq!(
parsed_version,
Version {
major: 0,
minor: 2,
patch: 6,
pre: Prerelease::EMPTY,
build: BuildMetadata::EMPTY,
}
);

Ok(())
}

Разбор сложной версии

Создаем semver::Version из сложной строки с помощью Version::parse. Строка содержит номер предрелиза (pre-release) и метаданные о сборке (build metadata) согласно спецификации семантического версионирования.

use semver::{BuildMetadata, Error, Prerelease, Version};

fn main() -> Result<(), Error> {
let version_str = "1.0.49-125+g72ee7853";
let parsed_version = Version::parse(version_str)?;

assert_eq!(
parsed_version,
Version {
major: 1,
minor: 0,
patch: 49,
pre: Prerelease::new("125").unwrap(),
build: BuildMetadata::new("g72ee7853").unwrap()
}
);

let serialized_version = parsed_version.to_string();
assert_eq!(&serialized_version, version_str);

Ok(())
}

Поиск последней версии, входящей в диапазон

Имеется список версий, необходимо найти последнюю версию, удовлетворяющую условию. Структура semver::VersionReq фильтрует список с помощью метода VersionReq::matches.

use semver::{Error, Version, VersionReq};

fn find_max_matching_version<'a, I>(
version_req_str: &str,
iterable: I,
) -> Result<Option<Version>, Error>
where
I: IntoIterator<Item = &'a str>,
{
let vreq = VersionReq::parse(version_req_str)?;

Ok(iterable
.into_iter()
.filter_map(|s| Version::parse(s).ok())
.filter(|s| vreq.matches(s))
.max())
}

fn main() -> Result<(), Error> {
assert_eq!(
find_max_matching_version("<= 1.0.0", vec!["0.9.0", "1.0.0", "1.0.1"])?,
Some(Version::parse("1.0.0")?)
);

assert_eq!(
find_max_matching_version(
">1.2.3-alpha.3",
vec![
"1.2.3-alpha.3",
"1.2.3-alpha.4",
"1.2.3-alpha.10",
"1.2.3-beta.4",
"3.4.5-alpha.9",
]
)?,
Some(Version::parse("1.2.3-beta.4")?)
);

Ok(())
}

Проверка внешней версии команды на совместимость

Запускаем git --version с помощью структуры Command, затем разбираем номер версии в структуру semver::Version с помощью метода Version::parse. Метод Version::matches сравнивает структуру semver:VersionReq с разобранной версией. Программа печатает "git version x.y.z".

use error_chain::error_chain;

use semver::{Version, VersionReq};
use std::process::Command;

error_chain! {
foreign_links {
Io(std::io::Error);
Utf8(std::string::FromUtf8Error);
SemVer(semver::Error);
}
}

fn main() -> Result<()> {
let version_constraint = ">=2.39.2";
let version_test = VersionReq::parse(version_constraint)?;
let output = Command::new("git").arg("--version").output()?;

if !output.status.success() {
error_chain::bail!("Выполнение команды завершилось ошибкой");
}

let stdout = String::from_utf8(output.stdout)?;
let version = stdout
.split(" ")
.last()
.ok_or_else(|| "Невалидный вывод команды")?;
// Версия Git может иметь вид 2.39.2.windows.1, разбор которой завершается ошибкой
let version = version.split(".").take(3).collect::<Vec<_>>().join(".");
let parsed_version = Version::parse(&version)?;

if !version_test.matches(&parsed_version) {
error_chain::bail!(
"Версия команды ниже минимальной поддерживаемой версии (обнаружена {}, требуется {})",
parsed_version,
version_constraint
);
}

Ok(())
}

10. Кодирование

10.1. Наборы символов

Процентное кодирование строки

Пример процентного кодирования строки с помощью функции uft8_percent_encode из крейта percent-encoding. Декодирование строки выполняется в помощью функции percent_decode.

use percent_encoding::{utf8_percent_encode, percent_decode, AsciiSet, CONTROLS};
use std::str::Utf8Error;

/// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');

fn main() -> Result<(), Utf8Error> {
let input = "confident, productive systems programming";

// Кодируем и собираем строку
let iter = utf8_percent_encode(input, FRAGMENT);
let encoded: String = iter.collect();
assert_eq!(encoded, "confident,%20productive%20systems%20programming");

// Декодируем строку
let iter = percent_decode(encoded.as_bytes());
let decoded = iter.decode_utf8()?;
assert_eq!(decoded, "confident, productive systems programming");

Ok(())
}

Набор кодировок (FRAGMENT) определяет, какие байты (помимо байтов, отличных от ASCII, и элементов управления (controls)) должны кодироваться. Состав этого набора зависит от контекста. Например, url кодирует ? в пути (path) URL, но не в строке запроса (query string).

utf8_percent_encode возвращает итератор срезов &str, которые собираются (collect) в String.

Кодирование строки в application/x-www-form-urlencoded

Пример кодирования строки в application/x-www-form-urlencoded с помощью метода form_urlencoded::byte_serialize. Декодирование выполняется с помощью метода form_urlencoded::parse. Обе функции возвращают итераторы, которые собираются (collect) в String.

use url::form_urlencoded::{byte_serialize, parse};

fn main() {
// Кодируем строку
let urlencoded: String = byte_serialize("What is ❤?".as_bytes()).collect();
assert_eq!(urlencoded, "What+is+%E2%9D%A4%3F");

// Декодируем строку
let decoded: String = parse(urlencoded.as_bytes())
.map(|(key, val)| [key, val].concat())
.collect();
assert_eq!(decoded, "What is ❤?");
}

Шестнадцатеричное кодирование и декодирование

Крейт data_encoding предоставляет метод HEXUPPER::encode, который принимает &[u8] и возвращает String, содержащую шестнадцатеричное представление данных.

Этот крейт также предоставляет метод HEXUPPER::decode, который принимает &[u8] и возвращает Vec<u8> при успешном декодировании данных.

use data_encoding::{DecodeError, HEXUPPER};

fn main() -> Result<(), DecodeError> {
let original = b"The quick brown fox jumps over the lazy dog.";
let expected = "54686520717569636B2062726F776E20666F78206A756D7073206F76\
657220746865206C617A7920646F672E";

// Кодируем данные
let encoded = HEXUPPER.encode(original);
assert_eq!(encoded, expected);

// Декодируем данные
let decoded = HEXUPPER.decode(&encoded.into_bytes())?;
assert_eq!(decoded, original);

Ok(())
}

base64 кодирование и декодирование

Крейт base64 предоставляет методы encode и decode для кодирования и декодирования байтовых срезов в base64:

use error_chain::error_chain;

use base64::{engine::general_purpose::STANDARD, Engine as _};
use std::str;

error_chain! {
foreign_links {
Base64(base64::DecodeError);
Utf8Error(str::Utf8Error);
}
}

fn main() -> Result<()> {
let hello = b"hello rustaceans";
let encoded = STANDARD.encode(hello);
let decoded = STANDARD.decode(&encoded)?;

println!("origin: {}", str::from_utf8(hello)?);
println!("base64 encoded: {}", encoded);
println!("back to origin: {}", str::from_utf8(&decoded)?);

Ok(())
}

10.2. Обработка CSV

Чтение записей CSV

Пример чтения стандартных записей CSV в структуру csv::StringRecord - слаботипизированное представление данных, которое ожидает валидные строки UTF-8. В качестве альтернативы можно использовать структуру ByteRecord, которая не проверяет строки.

use csv::Error;

fn main() -> Result<(), Error> {
let csv = "year,make,model,description
1948,Porsche,356,Luxury sports car
1967,Ford,Mustang fastback 1967,American car";

let mut reader = csv::Reader::from_reader(csv.as_bytes());
for record in reader.records() {
let record = record?;
println!(
"In {}, {} built the {} model. It is a {}.",
&record[0], &record[1], &record[2], &record[3]
);
}

Ok(())
}

Метод csv::Reader::deserialize десериализует данные в строготипизированные структуры. Обратите внимание на явную типизацию десериализуемой записи.

use serde::Deserialize;

#[derive(Deserialize)]
struct Record {
year: u16,
make: String,
model: String,
description: String,
}

fn main() -> Result<(), csv::Error> {
let csv = "year,make,model,description
1948,Porsche,356,Luxury sports car
1967,Ford,Mustang fastback 1967,American car";

let mut reader = csv::Reader::from_reader(csv.as_bytes());

for record in reader.deserialize() {
// Типизация записи
let record: Record = record?;
println!(
"In {}, {} built the {} model. It is a {}.",
record.year,
record.make,
record.model,
record.description
);
}

Ok(())
}

Чтение записей CSV с другим разделителем

Пример чтения записей CSV, разделителем которых является таб:

use csv::Error;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Record {
name: String,
place: String,
#[serde(deserialize_with = "csv::invalid_option")]
id: Option<u64>,
}

use csv::ReaderBuilder;

fn main() -> Result<(), Error> {
let data = "name\tplace\tid
Mark\tMelbourne\t46
Ashley\tZurich\t92";

let mut reader = ReaderBuilder::new()
// указываем разделитель
.delimiter(b'\t')
.from_reader(data.as_bytes());
// Другой способ типизации записи
for result in reader.deserialize::<Record>() {
println!("{:?}", result?);
}

Ok(())
}

Фильтрация записей CSV, совпадающих с предикатом

В следующем примере возвращаются только те строки data, которые совпадают с query:

use error_chain::error_chain;

use std::io;

error_chain! {
foreign_links {
Io(std::io::Error);
CsvError(csv::Error);
}
}

fn main() -> Result<()> {
let query = "CA";
let data = "\
City,State,Population,Latitude,Longitude
Kenai,AK,7610,60.5544444,-151.2583333
Oakman,AL,,33.7133333,-87.3886111
Sandfort,AL,,32.3380556,-85.2233333
West Hollywood,CA,37031,34.0900000,-118.3608333";

// Средство чтения CSV
let mut rdr = csv::ReaderBuilder::new().from_reader(