Книга рецептов Rust. 2/2

от автора

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.

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(data.as_bytes());     // Средство записи данных в stdout (терминал)     let mut wtr = csv::Writer::from_writer(io::stdout());      // Пишем в терминал заголовки CSV     wtr.write_record(rdr.headers()?)?;      for result in rdr.records() {         let record = result?;         // Пишем в терминал запись, содержащую поле, совпадающее с `query` (`CA`)         if record.iter().any(|field| field == query) {             wtr.write_record(&record)?;         }     }      // `writer` использует внутренний буфер, см. ниже     wtr.flush()?;     Ok(()) }

Обработка невалидных данных с помощью serde

Файлы CSV часто содержат невалидные данные. Для таких случаяв крейт csv предоставляет кастомный десериализатор, csv::invalid_option, который автоматически преобразует невалидные данные в значения None:

use csv::Error; use serde::Deserialize;  #[derive(Debug, Deserialize)] struct Record {     name: String,     place: String,     #[serde(deserialize_with = "csv::invalid_option")]     id: Option<u64>, }  fn main() -> Result<(), Error> {     // Последняя запись содержит невалидный ID     let data = "name,place,id mark,sydney,46.5 ashley,zurich,92 akshat,delhi,37 alisha,colombo,xyz";      let mut rdr = csv::Reader::from_reader(data.as_bytes());     for result in rdr.deserialize() {         let record: Record = result?;         // Результат десериализации последней записи выглядит как         // `Record { name: "alisha", place: "colombo", id: None }`         println!("{:?}", record);     }      Ok(()) }

Сериализация записей в CSV

Пример сериализации кортежей Rust. Структура csv::writer поддерживает автоматическую сериализацию типов Rust в записи CSV. Метод write_record предназначен для работы с простыми записями, содержащими только строковые данные. Для работы с данными, содержащими более сложные значения, такие как целые числа, числа с плавающей точкой и опциональные значения, используются метод serialize. Поскольку в средстве записи (writer) используется внутренний буфер, необходимо явно вызывать метод flush для его очистки.

use error_chain::error_chain;  use std::io;  error_chain! {     foreign_links {         CSVError(csv::Error);         IOError(std::io::Error);    } }  fn main() -> Result<()> {     let mut wtr = csv::Writer::from_writer(io::stdout());      wtr.write_record(&["Name", "Place", "ID"])?;      wtr.serialize(("Mark", "Sydney", 87))?;     wtr.serialize(("Ashley", "Dublin", 32))?;     wtr.serialize(("Akshat", "Delhi", 11))?;      wtr.flush()?;     Ok(()) }

Сериализация записей в CSV с помощью serde

Пример сериализации кастомной структуры в запись CSV с помощью крейта serde:

use error_chain::error_chain; use serde::Serialize; use std::io;  error_chain! {    foreign_links {        IOError(std::io::Error);        CSVError(csv::Error);    } }  #[derive(Serialize)] struct Record<'a> {     name: &'a str,     place: &'a str,     id: u64, }  fn main() -> Result<()> {     let mut wtr = csv::Writer::from_writer(io::stdout());      let rec1 = Record {         name: "Mark",         place: "Melbourne",         id: 56,     };     let rec2 = Record {         name: "Ashley",         place: "Sydney",         id: 64,     };     let rec3 = Record {         name: "Akshat",         place: "Delhi",         id: 98,     };      wtr.serialize(rec1)?;     wtr.serialize(rec2)?;     wtr.serialize(rec3)?;      wtr.flush()?;      Ok(()) }

10.3. Структурированные данные

Сериализация и десериализация неструктурированного JSON

Крейт serde_json предоставляет функцию from_str для разбора &str в формате JSON.

Неструктурированный JSON разбирается в универсальный тип serde_json::Value, который может представлять любой валидный JSON.

Следующим пример демонстрирует разбор &str JSON. Макрос json! используется для определения ожидаемого значения.

use serde_json::json; use serde_json::{Value, Error};  fn main() -> Result<(), Error> {     let j = r#"{                  "userid": 103609,                  "verified": true,                  "access_privileges": [                    "user",                    "admin"                  ]                }"#;      let parsed: Value = serde_json::from_str(j)?;      let expected = json!({         "userid": 103609,         "verified": true,         "access_privileges": [             "user",             "admin"         ]     });      assert_eq!(parsed, expected);      Ok(()) }

Десериализация TOML

Пример разбора TOML в универсальное toml::Value, которое может представлять любые валидные данные в формате TOML:

use toml::{Value, de::Error};  fn main() -> Result<(), Error> {     let toml_content = r#"           [package]           name = "your_package"           version = "0.1.0"           authors = ["You! <you@example.org>"]            [dependencies]           serde = "1.0"           "#;      let package_info: Value = toml::from_str(toml_content)?;      assert_eq!(package_info["dependencies"]["serde"].as_str(), Some("1.0"));     assert_eq!(package_info["package"]["name"].as_str(),                Some("your_package"));      Ok(()) }

Крейт serde позволяет разбирать TOML в кастомные структуры:

use serde::Deserialize; use std::collections::HashMap; use toml::de::Error;  #[derive(Deserialize)] struct Config {     package: Package,     dependencies: HashMap<String, String>, }  #[derive(Deserialize)] struct Package {     name: String,     version: String,     authors: Vec<String>, }  fn main() -> Result<(), Error> {     let toml_content = r#"           [package]           name = "your_package"           version = "0.1.0"           authors = ["You! <you@example.org>"]            [dependencies]           serde = "1.0"           "#;      let package_info: Config = toml::from_str(toml_content)?;      assert_eq!(package_info.package.name, "your_package");     assert_eq!(package_info.package.version, "0.1.0");     assert_eq!(package_info.package.authors, vec!["You! <you@example.org>"]);     assert_eq!(package_info.dependencies["serde"], "1.0");      Ok(()) }

11. Обработка ошибок

Правильная обработка ошибок в main

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

Io(std::io::Error) внутри foreign_links автоматически преобразует структуру std::io::Error в тип, определенный макросом error_chain! и реализующий трейт Error.

В следующем примере мы пытаемся выяснить, сколько времени работает система путем открытия файла Unix /proc/uptime и разбора его содержимого для извлечения первого числа. Функция read_uptime возвращает время безотказной работы или ошибку.

use error_chain::error_chain;  use std::fs::File; use std::io::Read;  error_chain!{     foreign_links {         Io(std::io::Error);         ParseInt(std::num::ParseIntError);     } }  fn read_uptime() -> Result<u64> {     let mut uptime = String::new();     File::open("/proc/uptime")?.read_to_string(&mut uptime)?;      Ok(uptime         .split('.')         .next()         .ok_or("Невозможно разобрать данные")?         .parse()?) }  fn main() {     match read_uptime() {         Ok(uptime) => println!("Время безотказной работы: {} секунд", uptime),         Err(err) => eprintln!("Ошибка: {}", err),     }; }

Обработка всех возможных ошибок

Крейт error-chain делает возможным и относительно компактным сопоставление разных типов ошибок, возвращаемых функцией. Тип ошибки определяется перечислением ErrorKind.

Используем reqwest::blocking для получения произвольного целого числа из веб-сервиса. Преобразуем строку из ответа в целое число. Стандартная библиотека Rust, reqwest и веб-сервис могут генерировать ошибки. Мы определяем ошибки с помощью foreign_links. Дополнительный вариант ErrorKind для веб-сервиса использует блок errors макроса error_chain!.

use error_chain::error_chain;  error_chain! {     foreign_links {         Io(std::io::Error);         Reqwest(reqwest::Error);         ParseIntError(std::num::ParseIntError);     }     errors { RandomResponseError(t: String) } }  fn parse_response(response: reqwest::blocking::Response) -> Result<u32> {     let mut body = response.text()?;     body.pop();     body.parse::<u32>()         .chain_err(|| ErrorKind::RandomResponseError(body)) }  fn run() -> Result<()> {     let url =         format!("https://www.random.org/integers/?num=1&min=0&max=10&col=1&base=10&format=plain");     let response = reqwest::blocking::get(&url)?;     let random_value: u32 = parse_response(response)?;     println!("Произвольное целое число между 0 и 10: {}", random_value);     Ok(()) }  fn main() {     if let Err(error) = run() {         match *error.kind() {             ErrorKind::Io(_) => println!("Стандартная ошибка ввода/вывода: {:?}", error),             ErrorKind::Reqwest(_) => println!("Ошибка Reqwest: {:?}", error),             ErrorKind::ParseIntError(_) => {                 println!("Стандартная ошибка разбора целого числа: {:?}", error)             }             ErrorKind::RandomResponseError(_) => println!("Кастомная ошибка: {:?}", error),             _ => println!("Другая ошибка: {:?}", error),         }     } }

Получение трассировки сложной ошибки

Следующий пример демонстрирует обработку сложной ошибки и вывод ее трассировки. chain_err используется для расширения списка возможных ошибок путем добавления новых ошибок. Стек ошибки может быть распутан (unwound), что предоставляет лучший контекст для понимания того, почему возникла ошибка.

В примере мы пытаемся десериализовать значение 256 в u8. Ошибка всплывает (bubble up) из serde через csv в пользовательский код.

use error_chain::error_chain; use serde::Deserialize;  use std::fmt;  error_chain! {     foreign_links {         Reader(csv::Error);     } }  #[derive(Debug, Deserialize)] struct Rgb {     red: u8,     blue: u8,     green: u8, }  impl Rgb {     fn from_reader(csv_data: &[u8]) -> Result<Rgb> {         let color: Rgb = csv::Reader::from_reader(csv_data)             .deserialize()             .nth(0)             .ok_or("Невозможно разобрать первую запись CSV")?             .chain_err(|| "Невозможно разобрать цвет RGB")?;          Ok(color)     } }  impl fmt::UpperHex for Rgb {     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {         let hexa = u32::from(self.red) << 16 | u32::from(self.blue) << 8 | u32::from(self.green);         write!(f, "{:X}", hexa)     } }  fn run() -> Result<()> {     let csv = "red,blue,green 102,256,204";      let rgb = Rgb::from_reader(csv.as_bytes()).chain_err(|| "Невозможно прочитать данные CSV")?;     println!("{:?} в шестнадцатеричном формате: #{:X}", rgb, rgb);      Ok(()) }  fn main() {     if let Err(ref errors) = run() {         eprintln!("Уровень ошибки - описание");         errors             .iter()             .enumerate()             .for_each(|(index, error)| eprintln!("└> {} - {}", index, error));          if let Some(backtrace) = errors.backtrace() {             eprintln!("{:?}", backtrace);         }          // В реальном приложении ошибки должны обрабатываться. Например, так:         // std::process::exit(1);     } }

Обратная трассировка ошибки:

Уровень ошибки - описание └> 0 - Невозможно прочитать данные CSV └> 1 - Невозможно разобрать цвет RGB └> 2 - CSV deserialize error: record 1 (line: 2, byte: 15): field 1: number too large to fit in target type

Запустите пример с RUST_BACKTRACE=1 для отображения подробной обратной трассировки этой ошибки.

12. Файловая система

12.1. Чтение и запись

Чтение линий строк из файла

Записываем сообщение, состоящее из трех строк, в файл, затем читаем его построчно с помощью итератора Lines, созданного с помощью метода BufRead::lines. Структура File реализует трейт Read, который предоставляет трейт BufReader. Метод File::create открывает файл для записи, а метод File::open — для чтения.

use std::fs::File; use std::io::{Write, BufReader, BufRead, Error};  fn main() -> Result<(), Error> {     let path = "lines.txt";      let mut output = File::create(path)?;     write!(output, "Rust\n?\nFun")?;      let input = File::open(path)?;     let buffered = BufReader::new(input);      for line in buffered.lines() {         println!("{}", line?);     }      Ok(()) }

Блокировка одновременного чтения и записи файла

Структура same_file::Handle используется для сравнения обработчика файла с другими обработчиками. В следующем примере сравниваются обработчики чтения и записи файла:

use same_file::Handle; use std::fs::File; use std::io::{BufRead, BufReader, Error, ErrorKind}; use std::path::Path;  fn main() -> Result<(), Error> {     let path_to_read = Path::new("message.txt");      let stdout_handle = Handle::stdout()?;     let handle = Handle::from_path(path_to_read)?;      if stdout_handle == handle {         return Err(Error::new(             ErrorKind::Other,             "Вы читаете и пишете в один и тот же файл",         ));     } else {         let file = File::open(&path_to_read)?;         let file = BufReader::new(file);         for (num, line) in file.lines().enumerate() {             println!("{} : {}", num, line?.to_uppercase());         }     }      Ok(()) }

cargo run отображает содержимое файла message.txt, а cargo run >> ./message.txt завершается ошибкой, поскольку операции чтения и записи выполняются над одним файлом.

Произвольный доступ к файлу с помощью карты памяти

Создаем карту памяти (memory map) с помощью memmap и имитируем произвольные чтения файла. Использование карты памяти означает, что мы индексируем фрагмент, а не пытаемся перемещаться по файлу.

Функция Mmap::map предполагает, что файл из карты памяти не модифицируется в то же время другим процессом. Иначе возникнет гонка за данными.

use memmap::Mmap; use std::fs::File; use std::io::{Write, Error};  fn main() -> Result<(), Error> {     write!(File::create("content.txt")?, "My hovercraft is full of eels!")?;      let file = File::open("content.txt")?;     let map = unsafe { Mmap::map(&file)? };      let random_indexes = [0, 1, 2, 19, 22, 10, 11, 29];     assert_eq!(&map[3..13], b"hovercraft");     let random_bytes: Vec<u8> = random_indexes.iter()         .map(|&idx| map[idx])         .collect();     assert_eq!(&random_bytes[..], b"My loaf!");     Ok(()) }

12.2. Обход директории

Получение названий файлов, модифицированных в течение последних 24 часов

Получаем текущую рабочую директорию путем вызова env::current_dir, затем для каждой сущности в fs::read_dir, извлекаем DirEntry::path и получаем метаданные через fs::Metadata. Metadata::modified возвращает SystemTime::elapsed — время, прошедшее с момента последней модификации. Duration::as_secs преобразует время в секунды и сравнивает его с 24 часами (24 60 60). Metadata::is_file отфильтровывает директории.

use error_chain::error_chain;  use std::{env, fs};  error_chain! {     foreign_links {         Io(std::io::Error);         SystemTimeError(std::time::SystemTimeError);     } }  fn main() -> Result<()> {     // Получаем текущую директорию     let current_dir = env::current_dir()?;     println!(         "Файлы, модифицированные в течение последних 24 часов в {:?}:",         current_dir     );      // Перебираем сущности, находящиеся в текущей директории     for entry in fs::read_dir(current_dir)? {         let entry = entry?;         // Получаем путь сущности         let path = entry.path();         // Извлекаем метаданные сущности         let metadata = fs::metadata(&path)?;         // Получаем время последней модификации и преобразуем его в секунды         let last_modified = metadata.modified()?.elapsed()?.as_secs();          // Если с момента последней модификации прошло меньше 24 часов и         // сущность является файлом         if last_modified < 24 * 3600 && metadata.is_file() {             println!(                 "Файл: {:?}, с момента последней модификации прошло {:?} секунд, файл доступен только для чтения: {:?}, размер: {:?} байтов.",                 path.file_name().ok_or("Название файла отсутствует")?,                 last_modified,                 metadata.permissions().readonly(),                 metadata.len(),             );         }     }      Ok(()) }

Рекурсивный поиск дубликатов

Пример рекурсивного поиска повторяющихся файлов, находящихся в текущей директории:

use std::collections::HashMap; use walkdir::WalkDir;  fn main() {     let mut filenames = HashMap::new();      // Перебираем сущности, находящиеся в текущей директории     for entry in WalkDir::new(".")             .into_iter()             .filter_map(Result::ok)             // игнорируем директории             .filter(|e| !e.file_type().is_dir()) {         // Получаем название файла         let f_name = String::from(entry.file_name().to_string_lossy());         // Счетчик количества названий файлов         let counter = filenames.entry(f_name.clone()).or_insert(0);         *counter += 1;         // Если название файла дублируется         if *counter == 2 {             println!("{}", f_name);         }     } }

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

Пример поиска всех файлов JSON, находящихся в текущей директории и модифицированных в течение последних 24 часов. Метод follow_links считает символические ссылки обычными директориями и файлами.

use error_chain::error_chain;  use walkdir::WalkDir;  error_chain! {     foreign_links {         WalkDir(walkdir::Error);         Io(std::io::Error);         SystemTime(std::time::SystemTimeError);     } }  fn main() -> Result<()> {     for entry in WalkDir::new(".")             // учитываем символические ссылки             .follow_links(true)             .into_iter()             .filter_map(|e| e.ok()) {         let f_name = entry.file_name().to_string_lossy();         let sec = entry.metadata()?.modified()?;          // Если мы имеем дело с файлом JSON и с момента его         //  последнего изменения прошло меньше 24 часов         if f_name.ends_with(".json") && sec.elapsed()?.as_secs() < 86400 {             println!("{}", f_name);         }     }      Ok(()) }

Обход директорий с пропуском файлов, название которых начинается с точки

Используем метод filter_entry для рекурсивного перебора сущностей. Функция is_not_hidden возвращает индикатор того, является ли файл или директория скрытыми (если название сущности начинается с точки, значит сущность является скрытой). Iterator::filter применяется к каждой WalkDir::DirEntry, даже если предком сущности является скрытая директория.

Корневая директория не считается скрытой благодаря использованию WalkDir::depth в is_not_hidden.

use walkdir::{DirEntry, WalkDir};  fn is_not_hidden(entry: &DirEntry) -> bool {     entry         .file_name()         .to_str()         // сущность является корневой директорией или ее название начинается с точки         .map(|s| entry.depth() == 0 || !s.starts_with("."))         .unwrap_or(false) }  fn main() {     WalkDir::new(".")         .into_iter()         // отфильтровываем скрытые сущности         .filter_entry(|e| is_not_hidden(e))         .filter_map(|v| v.ok())         .for_each(|x| println!("{}", x.path().display())); }

Рекурсивное вычисление размера файлов до заданной глубины

Глубина рекурсии может быть гибко установлена с помощью методов WalkDir::min_depth и WalkDir::max_depth. Вычисляем размер файлов на глубине трех поддиректорий, игнорируя файлы в корневой директории:

use walkdir::WalkDir;  fn main() {     let total_size = WalkDir::new(".")         .min_depth(1)         .max_depth(3)         .into_iter()         .filter_map(|entry| entry.ok())         .filter_map(|entry| entry.metadata().ok())         .filter(|metadata| metadata.is_file())         .fold(0, |acc, m| acc + m.len());      println!("Общий размер: {} байтов.", total_size); }

Рекурсивный поиск всех файлов PNG

Пример рекурсивного поиска всех файлов PNG в текущей директории. В данном случае паттерн ** совпадает с текущей директорией и всеми ее поддиректориями.

Паттерн ** может использоваться в любом месте пути. Например, /media/**/*.png совпадает со всеми файлами PNG в директории media и всех вложенных директориях.

use error_chain::error_chain;  use glob::glob;  error_chain! {     foreign_links {         Glob(glob::GlobError);         Pattern(glob::PatternError);     } }  fn main() -> Result<()> {     for entry in glob("**/*.png")? {         println!("{}", entry?.display());     }      Ok(()) }

Поиск всех файлов PNG, совпадающий с заданным паттерном, независимо от регистра

Пример поиска всех изображений в директории media, совпадающих с паттерном img_[0-9]*.png.

В функцию glob_with передается структура MatchOptions с настройкой case_sensitive: false, что делает поиск нечувствительным к регистру. Остальные настройки остаются дефолтными.

use error_chain::error_chain; use glob::{glob_with, MatchOptions};  error_chain! {     foreign_links {         Glob(glob::GlobError);         Pattern(glob::PatternError);     } }  fn main() -> Result<()> {     let options = MatchOptions {         case_sensitive: false,         ..Default::default()     };      for entry in glob_with("/media/img_[0-9]*.png", options)? {         println!("{}", entry?.display());     }      Ok(()) }

13. Разное

Проверка количества логических ядер центрального процессора

fn main() {     println!("Количество логических ядер ЦП: {}", num_cpus::get()); }

Определение лениво оцениваемой константы

Пример определения лениво оцениваемой (lazy evaluated) константной HashMap. HashMap оценивается один раз и хранится за глобальной статической ссылкой.

use lazy_static::lazy_static; use std::collections::HashMap;  lazy_static! {     static ref PRIVILEGES: HashMap<&'static str, Vec<&'static str>> = {         let mut map = HashMap::new();         map.insert("Игорь", vec!["user", "admin"]);         map.insert("Алекс", vec!["user"]);         map     }; }  fn show_access(name: &str) {     let access = PRIVILEGES.get(name);     println!("{}: {:?}", name, access); }  fn main() {     let access = PRIVILEGES.get("Игорь");     println!("Игорь: {:?}", access);      show_access("Алекс"); }

Обработка запросов на неиспользуемом порту

В следующем примере порт отображается в терминале и программа принимает подключения до получения запроса. При установке порта в значение 0, SocketAddrV4 присваивает произвольный порт.

use std::net::{SocketAddrV4, Ipv4Addr, TcpListener}; use std::io::{Read, Error};  fn main() -> Result<(), Error> {     let loopback = Ipv4Addr::new(127, 0, 0, 1);     let socket = SocketAddrV4::new(loopback, 0);     let listener = TcpListener::bind(socket)?;     let port = listener.local_addr()?;     println!("Listening on {}, access this port to end the program", port);     let (mut tcp_stream, addr) = listener.accept()?; // блокировка до получения запроса     println!("Connection received! {:?} is sending data.", addr);     let mut input = String::new();     let _ = tcp_stream.read_to_string(&mut input)?;     println!("{:?} says {}", addr, input);     Ok(()) }

14. Операционная система

14.1. Внешняя команда

Запуск внешней команды и обработка stdout

Запускаем git log --oneline как внешнюю Command и исследуем ее Output с помощью Regex для получения хеша и сообщений последних 5 коммитов:

use error_chain::error_chain;  use std::process::Command; use regex::Regex;  error_chain!{     foreign_links {         Io(std::io::Error);         Regex(regex::Error);         Utf8(std::string::FromUtf8Error);     } }  #[derive(PartialEq, Default, Clone, Debug)] struct Commit {     hash: String,     message: String, }  fn main() -> Result<()> {     let output = Command::new("git").arg("log").arg("--oneline").output()?;      if !output.status.success() {         error_chain::bail!("Выполнение команды завершилось кодом ошибки");     }      let pattern = Regex::new(r"(?x)                                ([0-9a-fA-F]+) # хеш коммита                                (.*)           # сообщение коммита")?;      String::from_utf8(output.stdout)?         .lines()         .filter_map(|line| pattern.captures(line))         .map(|cap| {                  Commit {                      hash: cap[1].to_string(),                      message: cap[2].trim().to_string(),                  }              })         .take(5)         .for_each(|x| println!("{:?}", x));      Ok(()) }

Обратите внимание: эта программа должна выполняться в директории с инициализированным GIT (fatal: not a git repository (or any of the parent directories): .git), содержащим хотя бы один коммит (fatal: your current branch 'master' does not have any commits yet).

Запуск внешней команды, передача ей stdin и проверка кода ошибки

Запускаем интерпретатор python с помощью внешней Command и передаем ему инструкцию для выполнения. Затем разбираем Output.

use error_chain::error_chain;  use std::collections::HashSet; use std::io::Write; use std::process::{Command, Stdio};  error_chain!{     errors { CmdError }     foreign_links {         Io(std::io::Error);         Utf8(std::string::FromUtf8Error);     } }  fn main() -> Result<()> {     let mut child = Command::new("python").stdin(Stdio::piped())         .stderr(Stdio::piped())         .stdout(Stdio::piped())         .spawn()?;      child.stdin         .as_mut()         .ok_or("stdin дочернего процесса не был перехвачен")?         .write_all(b"import this; copyright(); credits(); exit()")?;      let output = child.wait_with_output()?;      if output.status.success() {         let raw_output = String::from_utf8(output.stdout)?;         let words = raw_output.split_whitespace()             .map(|s| s.to_lowercase())             .collect::<HashSet<_>>();         println!("Найдено {} уникальных слов:", words.len());         println!("{:#?}", words);         Ok(())     } else {         let err = String::from_utf8(output.stderr)?;         error_chain::bail!("Выполнение внешней команды провалилось:\n {}", err)     } }

Обратите внимание: для успешного выполнения этой программы на вашей машине должен быть установлен Python.

Запуск внешних команд в конвейере

Получаем список из 10 самых больших файлов и директорий, находящихся в текущей рабочей директории с помощью команды du -ah . | sort -hr | head -n 10, выполняемой программно.

Command представляет процесс. Вывод (output) дочернего процесса перехватывается с помощью Stdio::piped между предком и ребенком.

use error_chain::error_chain;  use std::process::{Command, Stdio};  error_chain! {     foreign_links {         Io(std::io::Error);         Utf8(std::string::FromUtf8Error);     } }  fn main() -> Result<()> {     // Путь к текущей директории     let directory = std::env::current_dir()?;     // du -ah .     let mut du_output_child = Command::new("du")         .arg("-ah")         .arg(&directory)         .stdout(Stdio::piped())         .spawn()?;      if let Some(du_output) = du_output_child.stdout.take() {         // sort -hr         let mut sort_output_child = Command::new("sort")             .arg("-hr")             .stdin(du_output)             .stdout(Stdio::piped())             .spawn()?;          du_output_child.wait()?;          if let Some(sort_output) = sort_output_child.stdout.take() {             // head -n 10             let head_output_child = Command::new("head")                 .args(&["-n", "10"])                 .stdin(sort_output)                 .stdout(Stdio::piped())                 .spawn()?;              let head_stdout = head_output_child.wait_with_output()?;              sort_output_child.wait()?;              println!(                 "10 самых больших файлов и директорий в '{}':\n{}",                 directory.display(),                 String::from_utf8(head_stdout.stdout).unwrap()             );         }     }      Ok(()) }

Обратите внимание: эта программа предназначена для выполнения в системах Unix. В Windows аналогичную команду можно выполнить с помощью bash -c "du -ah . | sort -hr | head -n 10".

Перенаправление stdout и stderr дочернего процесса в один файл

Создаем (spawn) дочерний процесс и перенаправляем stdout и stderr в один и тот же файл. Этот пример похож на предыдущий, за исключением того, что process::Stdio пишет в указанный файл. File::try_clone ссылается на один обработчик для stdout и stderr. Это гарантирует, что оба дескриптора пишут с одной и той же позиции курсора. Выполнение этой программы аналогично выполнению команды ls . oops >out.txt 2>&1.

use std::fs::File; use std::io::Error; use std::process::{Command, Stdio};  fn main() -> Result<(), Error> {     let outputs = File::create("out.txt")?;     let errors = outputs.try_clone()?;      Command::new("ls")         // вызываем ошибку         .args(&[".", "oops"])         .stdout(Stdio::from(outputs))         .stderr(Stdio::from(errors))         .spawn()?         .wait_with_output()?;      Ok(()) }

Обратите внимание: эта программа предназначена для выполнения в системах Unix. В Windows аналогичную команду можно выполнить с помощью bash -c "ls . oops >out.txt 2>&1".

Непрерывная обработка входных данных дочернего процесса

В примере «Запуск внешней команды и обработка stdout» обработка начиналась только после завершения выполнения внешней Command. В следующем примере мы создаем конвейер с помощью Stdio::piped и непрерывно (continuously) читаем stdout при обновлении BufReader. Выполнение этой программы эквивалентно выполнению команды journalctl | grep usb.

use std::process::{Command, Stdio}; use std::io::{BufRead, BufReader, Error, ErrorKind};  fn main() -> Result<(), Error> {     let stdout = Command::new("journalctl")         .stdout(Stdio::piped())         .spawn()?         .stdout         .ok_or_else(|| Error::new(ErrorKind::Other, "Невозможно перехватить stdout"))?;      let reader = BufReader::new(stdout);      reader         .lines()         .filter_map(|line| line.ok())         .filter(|line| line.find("usb").is_some())         .for_each(|line| println!("{}", line));       Ok(()) }

Обратите внимание: эта программа предназначена для выполнения в системах Unix. В Windows аналогичную команду можно выполнить с помощью bash -c "journalctl | grep usb".

Чтение переменных среды окружения

Пример чтения переменной среды окружения с помощью std::env::var:

use std::env; use std::fs; use std::io::Error;  fn main() -> Result<(), Error> {     // Читаем `config_path` из переменной среды окружения `CONFIG`.     // Если переменная `CONFIG` не установлена, используется дефолтный путь     let config_path = env::var("CONFIG")         .unwrap_or("/etc/myapp/config".to_string());      let config: String = fs::read_to_string(config_path)?;     println!("Настройки: {}", config);      Ok(()) }

Для чтения переменных из файлов .env* используется крейт dotenv.

15. Обработка текста

15.1. Регулярные выражения

Проверка и извлечение логина из адреса email

Пример валидации email и извлечения всего, что предшествует @:

use lazy_static::lazy_static; use regex::Regex;  // Функция извлечения логина fn extract_login(input: &str) -> Option<&str> {     // Лениво оцениваемая статическая ссылка - регулярное выражение     lazy_static! {         static ref RE: Regex = Regex::new(r"(?x)             ^(?P<login>[^@\s]+)@             ([[:word:]]+\.)*             [[:word:]]+$             ").unwrap();     }     RE.captures(input).and_then(|cap| {         // login - захваченная группа (capture group)         cap.name("login").map(|login| login.as_str())     }) }  fn main() {     assert_eq!(extract_login(r"I❤email@example.com"), Some(r"I❤email"));     assert_eq!(         extract_login(r"sdf+sdsfsd.as.sdsd@jhkk.d.rl"),         Some(r"sdf+sdsfsd.as.sdsd")     );     assert_eq!(extract_login(r"More@Than@One@at.com"), None);     assert_eq!(extract_login(r"Not an email@email"), None); }

Извлечение списка уникальных хештегов из текста

Пример извлечения, сортировки и удаления дублирующихся хештегов из текста.

Регулярное выражение для проверки хештега учитывает только латинские хештеги, которые начинаются с буквы. Полная регулярка проверки хештегов Twitter гораздо сложнее.

use lazy_static::lazy_static;  use regex::Regex; use std::collections::HashSet;  // Функция извлечения хештегов fn extract_hashtags(text: &str) -> HashSet<&str> {     // Лениво оцениваемая статическая ссылка - регулярное выражение     lazy_static! {         static ref RE: Regex = Regex::new(                 r"\#[a-zA-Z][0-9a-zA-Z_]*"             ).unwrap();     }     RE.find_iter(text).map(|mat| mat.as_str()).collect() }  fn main() {     let tweet = "Hey #world, I just got my new #dog, say hello to Till. #dog #forever #2 #_ ";     let tags = extract_hashtags(tweet);     assert!(tags.contains("#dog") && tags.contains("#forever") && tags.contains("#world"));     assert_eq!(tags.len(), 3); }

Извлечение из текста номеров телефона

Пример обработки текста с помощью Regex::captures_iter для захвата нескольких номеров телефона. Регулярное выражение учитывает только американские номера.

use error_chain::error_chain;  use regex::Regex; use std::fmt;  error_chain!{     foreign_links {         Regex(regex::Error);         Io(std::io::Error);     } }  struct PhoneNumber<'a> {     area: &'a str,     exchange: &'a str,     subscriber: &'a str, }  impl<'a> fmt::Display for PhoneNumber<'a> {     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {         write!(f, "1 ({}) {}-{}", self.area, self.exchange, self.subscriber)     } }  fn main() -> Result<()> {     let phone_text = "     +1 505 881 9292 (v) +1 505 778 2212 (c) +1 505 881 9297 (f)     (202) 991 9534     Alex 5553920011     1 (800) 233-2010     1.299.339.1020";      let re = Regex::new(         r#"(?x)           (?:\+?1)?                       # опциональный код страны           [\s\.]?           (([2-9]\d{2})|\(([2-9]\d{2})\)) # код региона           [\s\.\-]?           ([2-9]\d{2})                    # код обмена           [\s\.\-]?           (\d{4})                         # код подписчика"#,     )?;      let phone_numbers = re.captures_iter(phone_text).filter_map(|cap| {         let groups = (cap.get(2).or(cap.get(3)), cap.get(4), cap.get(5));         match groups {             (Some(area), Some(ext), Some(sub)) => Some(PhoneNumber {                 area: area.as_str(),                 exchange: ext.as_str(),                 subscriber: sub.as_str(),             }),             _ => None,         }     });      assert_eq!(         phone_numbers.map(|m| m.to_string()).collect::<Vec<_>>(),         vec![             "1 (505) 881-9292",             "1 (505) 778-2212",             "1 (505) 881-9297",             "1 (202) 991-9534",             "1 (555) 392-0011",             "1 (800) 233-2010",             "1 (299) 339-1020",         ]     );      Ok(()) }

Замена всех подстрок в строке

Пример замены всех стандартных дат ISO 8601 YYYY-MM-DD эквивалентными датами в привычном нам формате. Например 2013-01-15 становится 15.01.2013.

Метод Regex::replace_all заменяет все вхождения всего регулярного выражения. &str реализует трейт Replacer, который позволяет переменным вроде $abcde ссылаться на соответствующие захваченные группы (?P<abcde>REGEX) из результатов поиска регулярки. См. синтаксис замены в строке для примеров и деталей экранирования.

use lazy_static::lazy_static;  use std::borrow::Cow; use regex::Regex;  // Функция форматирования даты fn reformat_dates(before: &str) -> Cow<str> {     // Лениво оцениваемая статическая ссылка - регулярное выражение     lazy_static! {         static ref RE : Regex = Regex::new(             r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})"             ).unwrap();     }     RE.replace_all(before, "$d.$m.$y") }  fn main() {     let before = "2012-03-14, 2013-01-15 и 2014-07-05";     let after = reformat_dates(before);     assert_eq!(after, "14.03.2012, 15.01.2013 и 05.07.2014"); }

15.3. Разбор строки

Сбор графем Юникода

Собираем индивидуальные графемы Юникода из UTF-8 строки с помощью метода UnicodeSegmentation::graphemes из крейта unicode-segmentation:

use unicode_segmentation::UnicodeSegmentation;  fn main() {     let name = "Йогурт захватил мир\r\n";     let graphemes = UnicodeSegmentation::graphemes(name, true)         .collect::<Vec<&str>>();     assert_eq!(graphemes[0], "Й"); }

Реализация трейта FromStr для кастомной структуры

Создаем кастомную структуру RGB и реализуем на ней трейт FromStr для преобразования цвета HEX в цвет RGB:

use std::str::FromStr;  #[derive(Debug, PartialEq)] struct RGB {     r: u8,     g: u8,     b: u8, }  impl FromStr for RGB {     type Err = std::num::ParseIntError;      // Преобразует цвет HEX `#rRgGbB` в экземпляр `RGB`     fn from_str(hex_code: &str) -> Result<Self, Self::Err> {         // `u8::from_str_radix(src: &str, radix: u32)` преобразует строковый срез         // в u8 в указанной системе счисления         let r: u8 = u8::from_str_radix(&hex_code[1..3], 16)?;         let g: u8 = u8::from_str_radix(&hex_code[3..5], 16)?;         let b: u8 = u8::from_str_radix(&hex_code[5..7], 16)?;          Ok(RGB { r, g, b })     } }  fn main() {     let code = "#fa7268";     match RGB::from_str(code) {         Ok(rgb) => {             println!(                 "The RGB color code is: R: {} G: {} B: {}",                 rgb.r, rgb.g, rgb.b             );         }         Err(_) => {             println!("{} is not a valid color hex code!", code);         }     }      assert_eq!(         RGB::from_str(&r"#fa7268").unwrap(),         RGB {             r: 250,             g: 114,             b: 104         }     ); }

16. Веб-разработка

16.1. Извлечение ссылок

Извлечение всех ссылок из HTML-страницы

Выполняем GET-запрос HTTP с помощью reqwest::get и разбираем ответ в документ HTML с помощью Document::from_read. find с a в Name извлекает все ссылки. Вызов filter_map на Selection извлекает URL из ссылок, имеющих attr (атрибут) href.

use error_chain::error_chain; use select::document::Document; use select::predicate::Name;  error_chain! {       foreign_links {           ReqError(reqwest::Error);           IoError(std::io::Error);       } }  #[tokio::main] async fn main() -> Result<()> {     // Выполняем GET-запрос     let res = reqwest::get("https://www.rust-lang.org/en-US/")         .await?         // преобразуем ответ в текст         .text()         .await?;      // Разбираем текст ответа     Document::from(res.as_str())         // находим ссылки         .find(Name("a"))         // отфильтровываем ссылки без атрибута `href`         .filter_map(|n| n.attr("href"))         .for_each(|x| println!("{}", x));    Ok(()) }

Поиск сломанных ссылок на веб-странице

Вызываем get_base_url для извлечения базового URL. Если документ имеет базовый тег, получаем из него значение attr (атрибута) href. По умолчанию используется Position::BeforePath оригинального URL.

Перебираем ссылки документа и создаем задачу с помощью tokio::spawn для разбора индивидуальной ссылки с помощью url::ParseOptions и Url::parse. Задача выполняет запрос с помощью reqwest и проверяет StatusCode ответа. Программа await (ожидает) выполнения задач перед завершением.

use error_chain::error_chain; use reqwest::StatusCode; use select::document::Document; use select::predicate::Name; use std::collections::HashSet; use url::{Position, Url};  error_chain! {   foreign_links {       ReqError(reqwest::Error);       IoError(std::io::Error);       UrlParseError(url::ParseError);       JoinError(tokio::task::JoinError);   } }  // Функция получения базового URL async fn get_base_url(url: &Url, doc: &Document) -> Result<Url> {   let base_tag_href = doc.find(Name("base")).filter_map(|n| n.attr("href")).nth(0);   let base_url =     base_tag_href.map_or_else(|| Url::parse(&url[..Position::BeforePath]), Url::parse)?;   Ok(base_url) }  // Функция проверки ссылки async fn check_link(url: &Url) -> Result<bool> {   let res = reqwest::get(url.as_ref()).await?;   // Проверяем только отсутствующие страницы (ошибка 404)   Ok(res.status() != StatusCode::NOT_FOUND) }  #[tokio::main] async fn main() -> Result<()> {   let url = Url::parse("https://www.rust-lang.org/en-US/")?;   let res = reqwest::get(url.as_ref()).await?.text().await?;   let document = Document::from(res.as_str());   let base_url = get_base_url(&url, &document).await?;   let base_parser = Url::options().base_url(Some(&base_url));   let links: HashSet<Url> = document     .find(Name("a"))     .filter_map(|n| n.attr("href"))     .filter_map(|link| base_parser.parse(link).ok())     .collect();     let mut tasks = vec![];      for link in links {         tasks.push(tokio::spawn(async move {             if check_link(&link).await.unwrap() {                 println!("Ссылка `{}` в порядке", link);             } else {                 println!("Ссылка `{}` сломана", link);             }         }));     }      for task in tasks {         task.await?     }    Ok(()) }

Извлечение уникальных ссылок из разметки MediaWiki

Получаем страницу MediaWiki с помощью reqwest::get и ищем все внутренние и внешние ссылки с помощью Regex::captures_iter. Использование Cow позволяет избежать чрезмерного выделения (allocation) String.

Описание синтаксиса MediaWiki можно найти здесь.

use lazy_static::lazy_static; use regex::Regex; use std::borrow::Cow; use std::collections::HashSet; use std::error::Error;  // Функция извлечения ссылок с помощью регулярного выражения fn extract_links(content: &str) -> HashSet<Cow<str>> {   lazy_static! {     static ref WIKI_REGEX: Regex = Regex::new(         r"(?x)             \[\[(?P<internal>[^\[\]|]*)[^\[\]]*\]\]    # внутренние ссылки             |             (url=|URL\||\[)(?P<external>http.*?)[ \|}] # внешние ссылки         "     )     .unwrap();   }    let links: HashSet<_> = WIKI_REGEX     // ищем ссылки на странице     .captures_iter(content)     // ищем совпадение с образцом-кортежем     .map(|c| match (c.name("internal"), c.name("external")) {       (Some(val), None) => Cow::from(val.as_str().to_lowercase()),       (None, Some(val)) => Cow::from(val.as_str()),       _ => unreachable!(),     })     .collect();    links }  #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> {   let content = reqwest::get(     "https://en.wikipedia.org/w/index.php?title=Rust_(programming_language)&action=raw",   )   .await?   .text()   .await?;    println!("{:#?}", extract_links(content.as_str()));    Ok(()) }

16.2. URL

Разбор URL из строки в тип Url

Метод parse крейта url валидирует и разбирает &str в структуру Url. Строка может быть повреждена, поэтому parse возвращает Result<Url, ParseError>.

use url::{Url, ParseError};  fn main() -> Result<(), ParseError> {     let s = "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open";      let parsed = Url::parse(s)?;     println!("Путь URL: {}", parsed.path());     // Путь URL: /rust-lang/rust/issues      Ok(()) }

Создание базового URL путем удаления сегментов пути

Базовый URL включает протокол и домен. Такие URL не содержат директорий, файлов и строк запроса (query string). Метод PathSegmentsMut::clear удаляет пути, а метод Url::set_query удаляет строку запроса.

use error_chain::error_chain; use url::Url;  error_chain! {     foreign_links {         UrlParse(url::ParseError);     }     errors {         CannotBeABase     } }  fn main() -> Result<()> {     let full = "https://github.com/rust-lang/cargo?asdf";      let url = Url::parse(full)?;     let base = base_url(url)?;      println!("Базовый URL: {}", base);     // Базовый URL: https://github.com/      Ok(()) }  fn base_url(mut url: Url) -> Result<Url> {     match url.path_segments_mut() {         Ok(mut path) => {             path.clear();         }         Err(_) => {             return Err(Error::from_kind(ErrorKind::CannotBeABase));         }     }      url.set_query(None);      Ok(url) }

Создание новых URL из базового

Метод join позволяет создавать новые URL из базового и относительного путей:

use url::{Url, ParseError};  fn main() -> Result<(), ParseError> {     let path = "/rust-lang/cargo";      let gh = build_github_url(path)?;      println!("Объединенный URL: {}", gh);     // Объединенный URL: https://github.com/rust-lang/cargo      Ok(()) }  fn build_github_url(path: &str) -> Result<Url, ParseError> {     const GITHUB: &'static str = "https://github.com";      let base = Url::parse(GITHUB)?;     let joined = base.join(path)?;      Ok(joined) }

Извлечение источника (схема / хост / порт)

Структура Url предоставляет разные методы для извлечения информации об URL, который она представляет:

use url::{Url, Host, ParseError};  fn main() -> Result<(), ParseError> {     let s = "ftp://rust-lang.org/examples";      let url = Url::parse(s)?;      assert_eq!(url.scheme(), "ftp");     assert_eq!(url.host(), Some(Host::Domain("rust-lang.org")));     assert_eq!(url.port_or_known_default(), Some(21));      Ok(()) }

Аналогичный результат можно получить с помощью метода origin:

use error_chain::error_chain; use url::{Url, Origin, Host};  error_chain! {     foreign_links {         UrlParse(url::ParseError);     } }  fn main() -> Result<()> {     let s = "ftp://rust-lang.org/examples";      let url = Url::parse(s)?;      let expected_scheme = "ftp".to_owned();     let expected_host = Host::Domain("rust-lang.org".to_owned());     let expected_port = 21;     let expected = Origin::Tuple(expected_scheme, expected_host, expected_port);      let origin = url.origin();     assert_eq!(origin, expected);      Ok(()) }

Удаление идентификаторов фрагментов и пар запросов из URL

Разбираем строку в структуру Url и обрезаем URL с помощью url::Position для удаления лишних частей:

use url::{Url, Position, ParseError};  fn main() -> Result<(), ParseError> {     let parsed = Url::parse("https://github.com/rust-lang/rust/issues?labels=E-easy&state=open")?;     let cleaned: &str = &parsed[..Position::AfterPath];     println!("Очищенный URL: {}", cleaned);     // Очищенный URL: https://github.com/rust-lang/rust/issues     Ok(()) }

16.3. Типы медиа

Извлечение MIME-типа из строки

Следующий пример демонстрирует разбор строки в тип MIME с помощью крейта mime. Структура FromStrError генерирует дефолтный MIME-тип в методе unwrap_or.

use mime::{Mime, APPLICATION_OCTET_STREAM};  fn main() {     let invalid_mime_type = "i n v a l i d";     // Дефолтный [MIME-тип]     let default_mime = invalid_mime_type         .parse::<Mime>()         .unwrap_or(APPLICATION_OCTET_STREAM);      println!(         "MIME для {:?} - дефолтный {:?}",         invalid_mime_type, default_mime     );     // MIME для "i n v a l i d" - дефолтный "application/octet-stream"      let valid_mime_type = "TEXT/PLAIN";     let parsed_mime = valid_mime_type         .parse::<Mime>()         .unwrap_or(APPLICATION_OCTET_STREAM);      println!(         "MIME для {:?} был разобран как {:?}",         valid_mime_type, parsed_mime     );     // MIME для "TEXT/PLAIN" был разобран как "text/plain" }

Извлечение MIME-типа из названия файла

Следующий пример демонстрирует извлечение корректного MIME-типа из названия файла с помощью крейта mime. Программа проверяет расширение файла и ищет совпадение с известным списком. Возвращаемым значением является mime::Mime.

use mime::Mime;  fn find_mimetype (filename : &String) -> Mime {     //  Разбиваем название файла на части по точке     let parts : Vec<&str> = filename.split('.').collect();      // Ищем совпадение с последней частью названия файла - его расширением     let res = match parts.last() {             Some(v) =>                 match *v {                     "png" => mime::IMAGE_PNG,                     "jpg" => mime::IMAGE_JPEG,                     "json" => mime::APPLICATION_JSON,                     _ => mime::TEXT_PLAIN,                 },             None => mime::TEXT_PLAIN,         };      return res; }  fn main() {     let filenames = vec!("foobar.jpg", "foo.bar", "foobar.png");     for file in filenames {         let mime = find_mimetype(&file.to_owned());         println!("MIME для {}: {}", file, mime);      } }

Извлечение MIME-типа из ответа HTTP

При получении ответа HTTP с помощью reqwest MIME-тип можно найти в заголовке Content-Type. Метод reqwest::header::HeaderMap::get извлекает заголовок как reqwest::header::HeaderValue, которое может быть преобразовано в строку. Крейт mime затем может разобрать эту строку в значение mime::Mime.

Крейт mime определяет некоторые распространенные MIME-типы.

Обратите внимание, что модуль reqwest::header экспортируется из крейта http.

use error_chain::error_chain; use mime::Mime; use std::str::FromStr; use reqwest::header::CONTENT_TYPE;  error_chain! {     foreign_links {         Reqwest(reqwest::Error);         Header(reqwest::header::ToStrError);         Mime(mime::FromStrError);     } }  #[tokio::main] async fn main() -> Result<()> {     let response = reqwest::get("https://www.rust-lang.org/logos/rust-logo-32x32.png").await?;     // Извлекает заголовки из ответа     let headers = response.headers();      match headers.get(CONTENT_TYPE) {         None => {             println!("Ответ не содердит заголовка `Content-Type`");         }         Some(content_type) => {             let content_type = Mime::from_str(content_type.to_str()?)?;             let media_type = match (content_type.type_(), content_type.subtype()) {                 (mime::TEXT, mime::HTML) => "документ HTML",                 (mime::TEXT, _) => "текст",                 (mime::IMAGE, mime::PNG) => "изображение PNG",                 (mime::IMAGE, _) => "изображение",                 _ => "не текст и не изображение",             };              println!("Ответ содержит {}", media_type);             // Ответ содержит изображение PNG         }     };      Ok(()) }

16.3. Клиенты

Отправка запроса HTTP

Отправляем синхронный GET-запрос HTTP с помощью метода reqwest::blocking::get, получаем структуру reqwest::blocking::Response, читаем тело ответа в String с помощью метода read_to_string, печатаем статус, заголовки и тело ответа:

use error_chain::error_chain; use std::io::Read;  error_chain! {     foreign_links {         Io(std::io::Error);         HttpRequest(reqwest::Error);     } }  fn main() -> Result<()> {     // Отправляем запрос/получаем ответ     let mut res = reqwest::blocking::get("http://httpbin.org/get")?;     let mut body = String::new();     // Читаем тело ответа в строку     res.read_to_string(&mut body)?;      println!("Статус: {}", res.status());     println!("Заголовки:\n{:#?}", res.headers());     println!("Тело ответа:\n{}", body);      Ok(()) }

Async

Асинхронный вариант предыдущего примера с использованием крейта tokio:

use error_chain::error_chain;  error_chain! {     foreign_links {         Io(std::io::Error);         HttpRequest(reqwest::Error);     } }  #[tokio::main] async fn main() -> Result<()> {     let res = reqwest::get("http://httpbin.org/get").await?;     println!("Статус: {}", res.status());     println!("Заголовки:\n{:#?}", res.headers());      // Читаем тело запроса как текст     let body = res.text().await?;     println!("Тело ответа:\n{}", body);     Ok(()) }

Обращение к GitHub API

Отправляем запрос к stargazers API v3 с помощью reqwest::get для получения списка пользователей, поставивших звезду проекту GitHub. Структура reqwest::Response десериализуется в структуру User, реализующую трейт serde::Deserialize.

tokio::main используется для установки асинхронного исполнителя (executor). Процесс ждет (await) завершения запроса перед обработкой ответа.

use reqwest::{header::USER_AGENT, Error}; use serde::Deserialize;  #[derive(Deserialize, Debug)] struct User {     login: String,     id: u32, }  #[tokio::main] async fn main() -> Result<(), Error> {     // Формируем URL     let request_url = format!(         "https://api.github.com/repos/{owner}/{repo}/stargazers",         owner = "harryheman",         repo = "my-js"     );     println!("{}", request_url);      let client = reqwest::Client::new();     // Отправляем запрос     let response = client         .get(request_url)         // Обязательный заголовок         .header(USER_AGENT, "")         .send()         .await?;      // Преобразуем тело ответа в формате JSON в объекты `User`     let users: Vec<User> = response.json().await?;     println!("{:?}", users);     Ok(()) }

Проверка существования ресурса API

Отправляем HEAD-запрос HTTP (Client::head) в конечную точку пользователей GitHub и определяем успех по статусу ответа. Так можно быстро проверить существование ресурса без получения тела ответа. Настройка reqwest::Client с помощью метода ClientBuilder::timeout отменяет запрос, если он выполняется дольше 5 секунд.

Поскольку методы ClientBuilder::build и ReqwestBuilder::send возвращают типы reqwest::Error, в качестве типа значения, возвращаемого функцией main, используется reqwest::Result.

use reqwest::header::USER_AGENT; use reqwest::ClientBuilder; use reqwest::Result; use std::time::Duration;  #[tokio::main] async fn main() -> Result<()> {     // Имя пользователя     let user = "harryheman";     // Конечная точка     let request_url = format!("https://api.github.com/users/{}", user);     println!("{}", request_url);      // Таймаут, по истечении которого запрос отменяется     let timeout = Duration::new(5, 0);     // Создаем и настраиваем экземпляр клиента     let client = ClientBuilder::new().timeout(timeout).build()?;     // Отправляем HEAD-запрос     let response = client         .head(&request_url)         // Обязательный заголовок         .header(USER_AGENT, "")         .send()         .await?;      // Определяем успех запроса по статусу ответа (200 ОК)     if response.status().is_success() {         println!("{} является пользователем", user);     } else {         println!("{} не является пользователем", user);     }      Ok(()) }

Создание и удаление Gist с помощью GitHub API

Создаем gist с помощью POST-запроса HTTP (Client::post) к gists API v3 и удаляем его с помощью DELETE-запроса (Client::delete).

Структура reqwest::Client отвечает за формирование запроса, включая URL, тело и аутентификацию. Тело запроса в формате JSON формируется с помощью макроса serde_json::json!. Оно устанавливается с помощью метода RequestBuilder::json. Заголовок авторизации устанавливается с помощью метода RequestBuilder::header. Метод RequestBuilder::send отправляет запрос.

Для авторизации в GitHub API необходимо создать токен доступа (не забудьте поставить галочку gist), и добавить его в файл .env в корне проекта (GH_TOKEN=ghp_...). Для доступа к переменным среды окружения из этого файла используется крейт dotenv.

use dotenv::dotenv; use error_chain::error_chain; use reqwest::{     header::{AUTHORIZATION, USER_AGENT},     Client, }; use serde::Deserialize; use serde_json::json; use std::env;  error_chain! {     foreign_links {         EnvVar(env::VarError);         HttpRequest(reqwest::Error);     } }  #[derive(Deserialize, Debug)] struct Gist {     id: String,     html_url: String, }  #[tokio::main] async fn main() -> Result<()> {     // Получаем переменные среды окружения из файла `.env`,     // находящего в корневой директории     dotenv().ok();      // Тело запроса     let gist_body = json!({         "description": "описание gist",         "public": true,         "files": {             "main.rs": {             "content": r#"fn main() { println!("всем привет!");}"#             }     }});      // Конечная точка     let request_url = "https://api.github.com/gists";     // Отправляем POST-запрос на создание gist     let response = Client::new()         .post(request_url)         // Обязательный заголовок         .header(USER_AGENT, "")         // Заголовок авторизации         .header(AUTHORIZATION, format!("Bearer {}", env::var("GH_TOKEN")?))         // Добавляем тело         .json(&gist_body)         .send()         .await?;      if response.status().is_success() {         let gist: Gist = response.json().await?;         println!("Создан {:?}", gist);          // Конечная точка         let request_url = format!("{}/{}", request_url, gist.id);         // Отправляем DELETE-запрос на удаление gist         let response = Client::new()             .delete(&request_url)             // Обязательный заголовок             .header(USER_AGENT, "")             // Заголовок авторизации             .header(AUTHORIZATION, format!("Bearer {}", env::var("GH_TOKEN")?))             .send()             .await?;          if response.status().is_success() {             println!(                 "Gist {} удален. Статус-код: {}",                 gist.id,                 response.status()             );         } else {             println!("Запрос провалился. Статус-код: {}", response.status());         }     } else {         println!("Запрос провалился. Статус-код: {}", response.status());     }      Ok(()) }

Скачивание файла во временную директорию

Создаем временную директорию с помощью структуры tempfile::Builder и асинхронно скачиваем в нее файл через HTTP с помощью метода reqwest::get.

Создаем целевой File с названием, извлеченным из Response::url, внутри метода tempdir, и копируем в нее скачанные данные с помощью метода io::copy. Временная директория автоматически удаляется после завершения программы.

use error_chain::error_chain; use std::fs::File; use std::io::copy; use tempfile::Builder;  error_chain! {      foreign_links {          Io(std::io::Error);          HttpRequest(reqwest::Error);      } }  #[tokio::main] async fn main() -> Result<()> {     // Временная директория     let tmp_dir = Builder::new().prefix("example").tempdir()?;     // Целевой файл     let target = "https://www.rust-lang.org/logos/rust-logo-512x512.png";     // Отправляем запрос     let response = reqwest::get(target).await?;      let mut dest = {         let fname = response             .url()             .path_segments()             .and_then(|segments| segments.last())             .and_then(|name| if name.is_empty() { None } else { Some(name) })             .unwrap_or("tmp.bin");         // Названием файла является последняя часть пути или `tmp.bin`         println!("файл для скачивания: '{}'", fname);         let fname = tmp_dir.path().join(fname);         // Путь к временной директории + название файла         println!("будет находиться в: '{:?}'", fname);         // Создаем и возвращаем дескриптор файла         File::create(fname)?     };     // Читаем тело ответа как текст     let content = response.text().await?;     // Копируем содержимое в файл     copy(&mut content.as_bytes(), &mut dest)?;     // Файл и временная директория будут существовать на протяжении 5 секунд,     // после чего автоматически удалятся     std::thread::sleep(std::time::Duration::from_secs(5));     Ok(()) }

Отправка файла в paste-rs

reqwest::Client устанавливает соединение с https://paste.rs с помощью паттерна reqwest::RequestBuilder. Client::post определяет назначение POST-запроса HTTP, RequestBuilder::body устанавливает тело запроса, а RequestBuilder::send отправляет запрос, блокирует поток до загрузки файла и получения ответа.

use error_chain::error_chain; use std::fs::File; use std::io::Read;  error_chain! {     foreign_links {         HttpRequest(reqwest::Error);         IoError(::std::io::Error);     } }  #[tokio::main] async fn main() -> Result<()> {     // Конечная точка     let paste_api = "https://paste.rs";     // Дескриптор файла     let mut file = File::open("message.txt")?;      let mut contents = String::new();     // Читаем содержимое файла в строку     file.read_to_string(&mut contents)?;      // Создаем клиента     let client = reqwest::Client::new();     // Отправляем запрос     let res = client.post(paste_api).body(contents).send().await?;     // Читаем ответ как текст     let response_text = res.text().await?;     println!("{}", response_text);     Ok(()) }

Обратите внимание: для корректной работы программы нужно создать непустой файл message.txt в корне проекта.

Частичная загрузка файла по HTTP с помощью заголовка диапазона

Используем reqwest::blocking::Client::head для получения Content-Length (размера содержимого) ответа.

Используем reqwest::blocking::Client::get для загрузки содержимого по частям размером 10240 байт с отслеживанием прогресса. Часть и позиция определяются с помощью заголовка Range, который определяется в RFC7233.

use error_chain::error_chain; use reqwest::header::{HeaderValue, CONTENT_LENGTH, RANGE}; use reqwest::StatusCode; use std::fs::File; use std::str::FromStr;  error_chain! {     foreign_links {         Io(std::io::Error);         Reqwest(reqwest::Error);         Header(reqwest::header::ToStrError);     } }  struct PartialRangeIter {     start: u64,     end: u64,     buffer_size: u32, }  impl PartialRangeIter {     pub fn new(start: u64, end: u64, buffer_size: u32) -> Result<Self> {         if buffer_size == 0 {             Err("невалидный `buffer_size`, размер буфера должен превышать 0")?;         }         Ok(PartialRangeIter {             start,             end,             buffer_size,         })     } }  impl Iterator for PartialRangeIter {     type Item = HeaderValue;      fn next(&mut self) -> Option<Self::Item> {         if self.start > self.end {             None         } else {             let prev_start = self.start;              self.start += std::cmp::min(self.buffer_size as u64, self.end - self.start + 1);              Some(                 HeaderValue::from_str(&format!("bytes={}-{}", prev_start, self.start - 1)).unwrap(),             )         }     } }  fn main() -> Result<()> {     // Конечная точка     let url = "https://httpbin.org/range/102400?duration=2";     // Размер части     const CHUNK_SIZE: u32 = 10240;      let client = reqwest::blocking::Client::new();     // Отправляем HEAD-запрос     let response = client.head(url).send()?;     // Получаем заголовок `Content-Length`     let length = response         .headers()         .get(CONTENT_LENGTH)         .ok_or("Ответ не содержит размера содержимого")?;     // Получаем размер содержимого     let length =         u64::from_str(length.to_str()?).map_err(|_| "Невалидный заголовок `Content-Length`")?;      // Дескриптор файла     let mut output_file = File::create("download.bin")?;      println!("Начинаем загрузку...");     for range in PartialRangeIter::new(0, length - 1, CHUNK_SIZE)? {         println!("Диапазон {:?}", range);         // Отправляем GET-запрос с заголовком `Range`         let mut response = client.get(url).header(RANGE, range).send()?;          let status = response.status();         if !(status == StatusCode::OK || status == StatusCode::PARTIAL_CONTENT) {             error_chain::bail!("Неожиданный ответ сервера: {}", status)         }         // Копируем содержимое ответа в файл         std::io::copy(&mut response, &mut output_file)?;     }      // Читаем ответ как текст     let content = response.text()?;     // Копируем байты ответа в файл     std::io::copy(&mut content.as_bytes(), &mut output_file)?;      println!("Загрузка успешно завершена");      Ok(()) }

Обратите внимание: в результате выполнения программы в корне проекта должен появиться файл download.bin.

Базовая аутентификация

Для выполнения базовой аутентификации HTTP используется метод reqwest::RequestBuilder::basic_auth:

use reqwest::blocking::Client; use reqwest::Error;  fn main() -> Result<(), Error> {     let client = Client::new();      let user_name = "testuser".to_string();     let password: Option<String> = None;      // Отправляем GET-запрос с базовой аутентификацией     let response = client         .get("https://httpbin.org/")         .basic_auth(user_name, password)         .send()?;      println!("{:?}", response);      Ok(()) }

Это конец второй части и Книги в целом.

Happy coding!


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале


ссылка на оригинал статьи https://habr.com/ru/articles/813791/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *