Libraries

Articles in Section

Одноразовые библиотеки

Если библиотека нужна лишь в 1 функции, можно в ней объявить её использование.

pub fn hash_password(password: &str) -> String {
    use sha2::Digest;
    // ... тело функции ...
}

Library Unit-tests

Примеры:

pub fn greet_user(name: &str) -> String {
    format!("Hello {name}")
}

pub fn login(username: &str, password: &str) -> bool {
    username == "admin" && password == "P@ssw0rd"
}

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

    #[test]
    fn test_greet_user() {
        assert_eq!("Hello Alex", greet_user("Alex"));
    }

    #[test]
    fn test_login() {
        assert!(login("admin", "P@ssw0rd"));
    }
}

Запуск тестов - командой cargo test

Subsections of Libraries

Anyhow Error Handler

Библиотека работы с ошибками - https://docs.rs/anyhow/latest/anyhow/

  • Использует единый тип Error для всех ошибок;
  • Добавляет контекст к ошибкам;
  • Поддержка backtrace для debug;

Установка

cargo add anyhow

Использование

Anyhow создаёт алиас наподобие Result<T> = Result<T, Box<dyn Error>>, чтобы скрыть тип ошибок и сделать его универсальным.

// ---- Без Anyhow
fn string_error() -> Result<(), String> {
    Ok(())
}
fn io_error() -> Result<(), std::io::Error> {
    Ok(())
}
fn any_error() -> Result<(), Box<dyn Error>> {
    string_error()?;
    io_error()?;
    Ok(())
}
// ---- С Anyhow:
use anyhow::Result;

fn string_error() -> Result<()> {
    Ok(())
}
fn io_error() -> Result<()> {
    Ok(())
}
fn any_error() -> Result<()> {
    string_error()?;
    io_error()?;
    Ok(())
}

Пример неудачного чтения файла:

use anyhow::{Context, Result};

fn read_config_file(path: &str) -> Result<String> {
    std::fs::read_to_string(path).with_context(|| format!("Failed to read file {}", path))
}

fn main() -> Result<()> {
    let config_content = read_config_file("conf.txt")?;

    println!("Config content:\n{:?}", config_content);

    Ok(())
}
  • Result<T> становится алиасом к Result<T, anyhow::Error>;
  • Context, with_context() позволяет добавить подробности к ошибке, в случае неуспеха функции чтения read_to_string();
  • Оператор ? выносит ошибку вверх, при этом авто-конвертирует её тип в anyhow::Error.

Замена Box<dyn> с контекстом

Возьмём пример, в котором чтение файла std::fs::read_to_string() (может быть неудачным), далее дешифровка его контента с помощью base64 decode() (может не получиться) в цепочку байт, из которой формируется строка String::from_utf8() (может не получиться). Все эти три потенциальных ошибки имеют разный тип.

Один способ все три их принять на одну функцию, это с помощью Box<dyn std::error::Error>>, потому что все 3 ошибки применяют std::error::Error.

use base64::{self, engine, Engine};

fn decode() -> Result<(), Box<dyn std::error::Error>> {
    let input = std::fs::read_to_string("input")?;
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD.decode(line)?;
        println!("{}", String::from_utf8(bytes)?);
    }
    Ok(())
}

Подход рабочий, но при срабатывании одной из трёх ошибок, судить о происхождении проблемы можно будет лишь по сообщению внутри ошибки.

В случае применения Anyhow, можно заменить им Box<dyn>, при этом сразу добавить контекстные сообщения, которое поможет понять место:

use anyhow::Context;
use base64::{self, engine, Engine};

fn decode() -> Result<(), anyhow::Error> {
    let input = std::fs::read_to_string("input")
    .context("Failed to read file")?; // контекст 1
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD
            .decode(line)
            .context("Failed to decode the input")?; // контекст 2
        println!(
            "{}",
            String::from_utf8(bytes)
            .context("Failed to cenvert bytes")? // контекст 3
        );
    }
    Ok(())

clap CLI

Установка

Установить вместе с допом derive, чтобы добавлять clap как признак (trait) у структур данных.

cargo add clap -F derive

Как использовать

Добавка clap позволяет парсить аргументы командной строки, а также добавляет ключи “-h” и “–help” для помощи без кода:

use clap::Parser;

#[derive(Parser, Debug)]
struct Args {
    arg1: String,
    arg2: usize,
}

fn main() {
    let args = Args::parse();
    println!("{:?}", args)
}

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

Добавление описаний

Имя программы и версия вносятся отдельным признаком. Доп. поля описания вносятся с помощью спец. комментариев ///:

use clap::Parser;

#[derive(Parser, Debug)]
#[command(author = "Author Name", version, about)]
/// A very simple CLI parser
struct Args {
    /// Text argument option
    arg1: String,
    /// Number argument option
    arg2: usize,
}

fn main() {
    let args = Args::parse();
    println!("{:?}", args)
}

Добавка флагов

Флаги добавляем с помощью аннотации #[arg(short, long)] для короткого и длинного именования флага. Если у 2-х флагов одинаковая первая буква, можно указать вручную их короткую версию. Короткая версия не может быть String, можно только 1 char.

<..>
struct Args {
    #[arg(short = 'a', long)]
    /// Text argument option
    arg1: String,
    
    #[arg(short = 'A', long)]
    /// Number argument option
    arg2: usize,
}
<..>

Необязательные флаги

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

struct Args {
    #[arg(short = 'a', long)]
    /// Text argument option
    arg1: String,
    
    #[arg(short = 'A', long)]
    /// Number argument option
    arg2: Option<usize>,
}

Такой подход потребует обработать ситуацию, когда в arg2 ничего нет. Вместо так делать, можно указать значение по умолчанию:

struct Args {
    #[arg(short = 'a', long)]
    /// Text argument option
    arg1: String,
    
    #[arg(default_value_t=usize::MAX, short = 'A', long)]
    /// Number argument option
    arg2: usize,
}

Теперь arg2 по умолчанию будет равен максимальному числу usize, если не указано иное.

Валидация введённых значений

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

use clap::Parser;

#[derive(Parser, Debug)]
#[command(author = "Author Name", version, about)]
/// A very simple CLI parser
struct Args {
    #[arg(value_parser = validate_argument_name, short = 'a', long)]
    /// Text argument option
    arg1: String,
    #[arg(default_value_t=usize::MAX, short = 'A', long)]
    /// Number argument option
    arg2: usize,
}

fn validate_argument_name(name: &str) -> Result<String, String> {
    if name.trim().len() != name.len() {
        Err(String::from(
            "строка не должна начинаться или заканчиваться пробелами",
        ))
    } else {
        Ok(name.to_string())
    }
}

fn main() {
    let args = Args::parse();
    println!("{:?}", args)
}

Теперь при попытке вызвать программу tiny-clapper -- -a " " будет показана ошибка валидации.

Command CLI

Библиотека позволяет запускать команды в ОС.

Варианты использования

  • spawn — runs the program and returns a value with details
  • output — runs the program and returns the output
  • status — runs the program and returns the exit code

Использование

Output - просто и быстро

    let output = Command::new("cat")
        .arg("/etc/issue")
        .output()
        .with_context(|| "ls command failed")?;

    println!("{}", output.status);
    println!("{}", String::from_utf8_lossy(&output.stdout));
    println!("{}", String::from_utf8_lossy(&output.stderr));

❗Ограничение - можно вызывать только существующие объекты, нельзя добавлять свой текст.

spawn - гибкий ввод

Самый гибкий вариант, позволяющий делать свой ввод, а не только существующие команды и файлы-папки, это через spawn:

    let mut child = Command::new("cat") // команда
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()?;

    let stdin = child.stdin.as_mut()?;
    stdin.write_all(b"Hello Rust!\n")?; // текст к команде, /n обязателен
    let output = child.wait_with_output()?;
    
    for i in output.stdout.iter() { // цикл на случай многострочного вывода
        print!("{}", *i as char);
    }
    Ok(())

❗Ограничение - можно подавать на вход текст лишь тем командам, которые требуют сразу указать вводный текст. При этом ряд команд делают паузу перед потреблением текста на вход, с такими свой ввод работать не будет это относится и к фильтрации через pipe = | grep <...> и аналоги.

Pipe (nightly) - полный ввод (не проверенный способ)

https://doc.rust-lang.org/std/pipe/fn.pipe.html Для использования нужен nightly вариант Rust

rustup install nightly
rustup default nightly

Использование:

#![feature(anonymous_pipe)] // только в Rust Nightly
use std::pipe

let text = "| grep file".as_bytes();

// Запускаем саму команду
let child = Command::new("ls")
    .arg("/Users/test")
    .stdin({
        // Нельзя отправить просто строку в команду
        // Нужно создать файловый дескриптор (как в обычном stdin "pipe")
        // Поэтому создаём пару pipes тут
        let (reader, mut writer) = std::pipe::pipe().unwrap();

        // Пишем строку в одну pipe
        writer.write_all(text).unwrap();

        // далее превращаем вторую для передачи в команду сразу при spawn. 
        Stdio::from(reader)
    })
    .spawn()?;

itertools

External link: https://docs.rs/itertools/latest/itertools/

Working with iterators

Sorting() a string of letters (with rev() - reverse order)

use itertools::Itertools;

let text = "Hello world";
let text_sorted = text.chars().sorted().rev().collect::<String>();
// rev() - Iterate the iterable in reverse
   
println!("Text: {}, Sorted Text: {}", text, text_sorted); 
// Text: Hello world, Sorted Text: wroollledH 

Counts() подсчёт количества одинаковых элементов в Array

use itertools::Itertools;

let number_list = [1,12,3,1,5,2,7,8,7,8,2,3,12,7,7];
let mode = number_list.iter().counts(); // Itertools::counts() 
// возвращает Hashmap, где ключи взяты из массива, значения - частота
for (key, value) in &mode {  
    println!("Число {key} встречается {value} раз");  
 } 

rand

Generating random numbers

External links:

Using rand lib example:

use rand::Rng;

fn main() {
let secret_of_type = rand::rng().random::<u32>();
let secret = rand::rng().random_range(1..=100);

println!("Random nuber of type u32: {secret_of_type}");
println!("Random nuber from 1 to 100: {}", secret); }

В старой версии библиотеки применялся признак gen(), который переименовали в связи с добавлением gen() в Rust 2024.

regex

Внешние ссылки:

Установка

cargo add regex

REGEX Выражения

Один символ

.             any character except new line (includes new line with s flag)
\d            digit (\p{Nd})
\D            not digit
\pX           Unicode character class identified by a one-letter name
\p{Greek}     Unicode character class (general category or script)
\PX           Negated Unicode character class identified by a one-letter name
\P{Greek}     negated Unicode character class (general category or script)

Классы символов

[xyz]         A character class matching either x, y or z (union).
[^xyz]        A character class matching any character except x, y and z.
[a-z]         A character class matching any character in range a-z.
[[:alpha:]]   ASCII character class ([A-Za-z])
[[:^alpha:]]  Negated ASCII character class ([^A-Za-z])
[x[^xyz]]     Nested/grouping character class (matching any character except y and z)
[a-y&&xyz]    Intersection (matching x or y)
[0-9&&[^4]]   Subtraction using intersection and negation (matching 0-9 except 4)
[0-9--4]      Direct subtraction (matching 0-9 except 4)
[a-g~~b-h]    Symmetric difference (matching `a` and `h` only)
[\[\]]        Escaping in character classes (matching [ or ])

Совмещения символов

xy    concatenation (x followed by y)
x|y   alternation (x or y, prefer x)

Повторы символов

x*        zero or more of x (greedy)
x+        one or more of x (greedy)
x?        zero or one of x (greedy)
x*?       zero or more of x (ungreedy/lazy)
x+?       one or more of x (ungreedy/lazy)
x??       zero or one of x (ungreedy/lazy)
x{n,m}    at least n x and at most m x (greedy)
x{n,}     at least n x (greedy)
x{n}      exactly n x
x{n,m}?   at least n x and at most m x (ungreedy/lazy)
x{n,}?    at least n x (ungreedy/lazy)
x{n}?     exactly n x

Пустые символы

^     the beginning of text (or start-of-line with multi-line mode)
$     the end of text (or end-of-line with multi-line mode)
\A    only the beginning of text (even with multi-line mode enabled)
\z    only the end of text (even with multi-line mode enabled)
\b    a Unicode word boundary (\w on one side and \W, \A, or \z on other)
\B    not a Unicode word boundary

Группировка и флаги

(exp)          numbered capture group (indexed by opening parenthesis)
(?P<name>exp)  named (also numbered) capture group (names must be alpha-numeric)
(?<name>exp)   named (also numbered) capture group (names must be alpha-numeric)
(?:exp)        non-capturing group
(?flags)       set flags within current group
(?flags:exp)   set flags for exp (non-capturing)

Спец-символы

\*          literal *, works for any punctuation character: \.+*?()|[]{}^$
\a          bell (\x07)
\f          form feed (\x0C)
\t          horizontal tab
\n          new line
\r          carriage return
\v          vertical tab (\x0B)
\123        octal character code (up to three digits) (when enabled)
\x7F        hex character code (exactly two digits)
\x{10FFFF}  any hex character code corresponding to a Unicode code point
\u007F      hex character code (exactly four digits)
\u{7F}      any hex character code corresponding to a Unicode code point
\U0000007F  hex character code (exactly eight digits)
\U{7F}      any hex character code corresponding to a Unicode code point

Типы символов по ASCII

[[:alnum:]]    alphanumeric ([0-9A-Za-z])
[[:alpha:]]    alphabetic ([A-Za-z])
[[:ascii:]]    ASCII ([\x00-\x7F])
[[:blank:]]    blank ([\t ])
[[:cntrl:]]    control ([\x00-\x1F\x7F])
[[:digit:]]    digits ([0-9])
[[:graph:]]    graphical ([!-~])
[[:lower:]]    lower case ([a-z])
[[:print:]]    printable ([ -~])
[[:punct:]]    punctuation ([!-/:-@\[-`{-~])
[[:space:]]    whitespace ([\t\n\v\f\r ])
[[:upper:]]    upper case ([A-Z])
[[:word:]]     word characters ([0-9A-Za-z_])
[[:xdigit:]]   hex digit ([0-9A-Fa-f])

Использование REGEX

Сверка с is_match()

use regex::Regex;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = "hello, world!";
    let pattern = Regex::new(r"hello, (world|universe)!")?;
    
    let is_match = pattern.is_match(input);
    println!("Is match: {}", is_match); // true
    Ok(())
}

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

Поиск первого совпадения

    let pattern = regex::Regex::new(r"hello, (world|universe)!")?;
    let input = "hello, world!";
    let first_match = pattern.find(input);

    println!("First match: {}", first_match.unwrap().as_str());

Первое совпадение будет иметь тип Option<match>, а в случае отсутствия совпадений = None.

Поиск всех совпадений

    let pattern = regex::Regex::new(r"hello, (world|universe)!")?;
    let input = "hello, world! hello, universe!";
    let matches: Vec<_> = pattern.find_iter(input).collect(); // find_iter()
    matches.iter().for_each(|i| println!("{}", i.as_str()));
    // matches = Vec<match> и содержит все совпадения

serde

Installing

Add serde framework with Derive feature to use it in structures and functions. Also add a separate serde_json lib for converting into specifically JSON:

cargo add serde -F derive
cargo add serde_json

Usage

Add serde, then mark the structures with Serialise, Deserialise traits and use serde_json for serialising:

use serde::{Deserialize, Serialize};

#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub enum LoginRole {
    Admin,
    User,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub username: String,
    pub password: String,
    pub role: LoginRole,
}

pub fn get_default_users() -> HashMap<String, User> {
    let mut users = HashMap::new();
    users.insert(
        "admin".to_string(),
        User::new("admin", "password", LoginRole::Admin),
    );
    users.insert(
        "bob".to_string(),
        User::new("bob", "password", LoginRole::User),
    );
    users
}

pub fn get_users() -> HashMap<String, User> {
    let users_path = Path::new("users.json");
    if users_path.exists() {
        // Load the file!
        let users_json = std::fs::read_to_string(users_path).unwrap();
        let users: HashMap<String, User> = serde_json::from_str(&users_json).unwrap();
        users
    } else {
        // Create a file and return it
        let users = get_default_users();
        let users_json = serde_json::to_string(&users).unwrap();
        std::fs::write(users_path, users_json).unwrap();
        users
    }

sysinfo

Библиотека для изучения параметров системы и системных ресурсов. https://docs.rs/sysinfo/latest/sysinfo/

Установка

cargo add sysinfo

Использование

use sysinfo::{RefreshKind, System};

fn main() {
    let _sys = System::new_with_specifics(RefreshKind::nothing());
    println!("System name:             {:?}", System::name());
    println!("System kernel version:   {:?}", System::kernel_version());
    println!("System OS version:       {:?}", System::os_version());
    println!("System host name:        {:?}", System::host_name());
}

С помощью RefreshKind::nothing() отключаются все динамические вещи, такие как остаток свободной памяти, загрузка ЦПУ, сеть и так далее, что драматически ускоряет опрос системы.

Можно частично опрашивать разные системные ресурсы, по заданным интервалам.

Tokio tracing

Внешние ссылки:

Наблюдаемость / Observability

Наблюдаемость позволяет понять систему извне, задавая вопросы о ней, при этом не зная ее внутреннего устройства. Кроме того, она позволяет легко устранять неполадки и решать новые проблемы, то есть «неизвестные неизвестные». Она также поможет вам ответить на вопрос «почему что-то происходит?».

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

Терминология

  • Событие журнала (Log event/message) - событие, произошедшее в конкретный момент времени;
  • Промежуток (Span record) - запись потока исполнения в системе за период времени. Он также выполняет функции контекста для событий журнала и родителя для под-промежутков;
  • Трасса (trace) - полная запись потока исполнения в системе от получения запроса до отправки ответа. Это по сути промежуток-родитель, или корневой промежуток;
  • Подписчик (subscriber) - реализует способ сбора данных трассы, например, запись их в стандартный вывод;
  • Контекст трассировки (Tracing Context): набор значений, которые будут передаваться между службами

Установка

cargo add tracing
cargo add tracing-subscriber -F json

Использование

Инициализация

Простая инициализация и отслеживание событий:

use tracing::info;

fn main() {
    // Установка глобального сборщика по конфигурации
    tracing_subscriber::fmt::init();

    let number_of_yaks = 3;

    // новое событие, вне промежутков
    info!(number_of_yaks, "preparing to shave yaks");
}

Ручная инициализация свойств подписчика для форматирования лога:

fn setup_tracing() {
    let subscriber = tracing_subscriber::fmt()
        .json() // нужно cargo add tracing-subscriber -F json
        .with_max_level(tracing::Level::TRACE) // МАХ уровень логирования
        .compact() // компактный лог
        .with_file(true) // показывать файл-исходник
        .with_line_number(true) // показать номера строк кода
        .with_thread_ids(true) // показать ID потока с событием
        .with_target(false) // не показывать цель (модуль) события
        .finish();

    tracing::subscriber::set_global_default(subscriber).unwrap();

    tracing::info!("Starting up");
    tracing::warn!("Are you sure this is a good idea?");
    tracing::error!("This is an error!");
}

Трассировка в синхронном режиме

use tracing::{instrument, warn};

#[instrument]
fn sync_tracing() {
    warn!("event 1");
    sync_tracing_sub();
}

#[instrument]
fn sync_tracing_sub() {
    warn!("event 2");
}

fn main() {
    setup_tracing();
    sync_tracing();
}

Макрос #[instrument] автоматически создаёт промежутки (spans) для функций, а подписчик (subscriber) настроен выводить промежутки в stdout.

Трассировка потоков в асинхронном режиме

use tracing_subscriber::fmt::format::FmtSpan;

#[tracing::instrument] // инструмент следит за временем работы
async fn hello() {
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}

#[tokio::main] // cargo add tokio -F time,macros,rt-multi-thread
async fn main() -> anyhow::Result<()> {
    let subscriber = tracing_subscriber::fmt()
        .json()
        .compact()
        .with_file(true)
        .with_line_number(true)
        .with_thread_ids(true)
        .with_target(false)
        .with_span_events(FmtSpan::ENTER | FmtSpan::CLOSE) // вход и выход
        .finish();                                   // потока отслеживать

    tracing::subscriber::set_global_default(subscriber).unwrap();

    hello().await;
    Ok(())
}

В итоге получаем лог работы потока с временем задержки:

2024-12-24T14:30:17.378906Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"} {}
2024-12-24T14:30:18.383596Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"} {}
2024-12-24T14:30:18.383653Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"} {}
2024-12-24T14:30:18.383675Z  INFO ThreadId(01) hello: src/main.rs:3: {"message":"close","time.busy":"179µs","time.idle":"1.00s"} {}

Запись журнала в хранилище

Внешние ссылки:

Для записи журнала применяется отдельный модуль tracing-appender:

cargo add tracing-appender

У него много функций не только записи в облачные службы типа Datadog, но и создание журнала с дозаписью (минутной, часовой, дневной), а также запись как неблокирующее действие во время многопоточного исполнения.

Пример инициализации неблокирующей записи журнала в файл (лучше всего как JSON), одновременно вывод на экран и организация часового журнала с дозаписью (rolling):

use tracing::{instrument, warn};

// тянем std::io::Write признак в режиме неблокирования
use tracing_subscriber::fmt::writer::MakeWriterExt; 

fn setup_tracing() {
    // инициализация файла с дозаписью
    let logfile = tracing_appender::rolling::hourly("/some/directory", "app-log");
    // уровень записи при логировании = INFO
    let stdout = std::io::stdout.with_max_level(tracing::Level::INFO);

    let subscriber = tracing_subscriber::fmt()
        .with_max_level(tracing::Level::TRACE)
        .json()
        .compact()
        .with_file(true)
        .with_line_number(true)
        .with_thread_ids(true)
        .with_target(false)
        .with_writer(stdout.and(logfile)) // обязательно указать запись тут
        .finish();                        // в файл и в stdout консоль
    tracing::subscriber::set_global_default(subscriber).unwrap();
}

#[instrument]
fn sync_tracing() {
    warn!("event 1");
    sync_tracing_sub();
}

#[instrument]
fn sync_tracing_sub() {
    warn!("event 2");
}

fn main() {
    setup_tracing();
    sync_tracing();
}