Модуль 3.1 — Инженерия крейтов
Упражнение 3.1.1: Моё приложение на serde
Упражнение адаптировано из упражнения serde_lifetimes от Ferrous Systems.
Откройте exercises/3-crate-engineering/1-crate-engineering/1-my-serde-app/src/main.rs. Там находится код на Rust, с которым мы будем работать в этом упражнении.
Мы использовали макросы todo!(), чтобы отметить места, куда нужно вставить код, чтобы программа заработала. За справкой обращайтесь к API serde_json.
Подсказка
Serde предоставляет два трейта: `Serialize` и `Deserialize`. Их можно получить через `derive` для ваших типов `struct` и `enum`. Другие крейты `serde-*` используют эти трейты, чтобы преобразовывать наш тип данных в соответствующее представление и обратно (`serde_json` — в JSON, `serde_yaml` — в YAML и т. д.).Почему
mainвозвращаетanyhow::Result<()>? Еслиmainвозвращает результат, ошибки можно передавать вверх до самого рантайма. Подробнее об этом — в Rust By Example. Типanyhow::Result— более гибкий вариантResult, который позволяет легко преобразовывать типы ошибок.
Что за конструкция
r#"...? Префиксrперед строковым литералом означает «сырую» строку (raw string). Escape-последовательности (\n,\"и т. п.) в ней не работают, поэтому такие строки удобны для регулярных выражений, JSON-литералов и подобных случаев.После
rможно поставить один или несколько символов (в нашем примере это#), и строка закончится, когда встретится закрывающая двойная кавычка, за которой следует такое же количество тех же символов. Это удобно, если внутри строки нужны двойные кавычки. Для нашего примераr#" ... "#отлично подходит для JSON. В редких случаях может понадобиться два и более знака#, например, когда в JSON-строках хранятся значения цвета CSS:
#![allow(unused)]
fn main() {
// здесь `"#` не завершит строку
r##"
{
"color": "#ff00ff"
}
"##
}
Упражнение 3.1.2: Викторина
В этом упражнении вы создадите крейт Rust, который следует рекомендациям, разобранным на лекции. Кроме того, вы добавите и используете зависимости, напишете модульные тесты и документацию. Это упражнение можно считать ступенькой к финальному проекту.
Упражнение выполняется в группах по 2 человека.
3.1.2.A Подготовка ⭐
Создайте новый проект командой cargo new --name quizzer. Убедитесь, что он одновременно является и бинарником, и библиотекой. Это значит, что в крейте будут файлы src/lib.rs и src/bin/quizzer/main.rs, где quizzer — имя бинарника:
$ tree
.
├── Cargo.toml
├── quiz.json
└── src
├── bin
│ └── quizzer
│ └── main.rs
└── lib.rs
Добавьте следующие зависимости в файл Cargo.toml. Ниже приведены ссылки на их страницы на docs.rs. Составьте общее представление о том, для чего нужны эти крейты и как их использовать, но пока не погружайтесь слишком глубоко.
anyhow1.0clap4.0. Также просмотрите https://docs.rs/clap/latest/clap/_derive/_tutorial/index.htmlserde_json1.0serde1.0
Ваш Cargo.toml должен выглядеть так:
[package]
name = "quizzer"
version = "0.1.0"
edition = "2021"
### Подробнее о ключах и их значении: https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.66"
clap = { version = "4.0.18", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.87"
Для clap и serde включена нестандартная фича derive этих крейтов. В clap она позволяет вывести (derive) трейт Parser, что сильно упрощает создание CLI. Фича derive из serde позволяет вывести трейты Serialize и Deserialize для любой структуры, которую мы хотим сериализовать или десериализовать с помощью serde и его бэкендов, в нашем случае serde_json.
3.1.2.B Викторина ⭐⭐⭐
Это упражнение и о проектировании, и о поиске информации. Вам придётся придумать модель для представления вопросов викторины, а также способ сохранять их в JSON-файл и загружать его. Кроме того, нужно выяснить, как разбирать аргументы программы.
Мы будем использовать проект, который только что создали, чтобы написать создатель и исполнитель викторин. При необходимости можно добавить другие зависимости. Программа должна соответствовать следующим функциональным требованиям:
- Она работает как инструмент командной строки в терминале.
- У неё два режима: режим ввода вопросов и режим викторины. Режим выбирается подкомандой, которая передаётся программе первым аргументом.
- Режим ввода вопросов: позволяет вводить вопросы с множественным выбором, по четыре варианта ответа в каждом, ровно один из которых правильный. Вопросы сохраняются на диск в виде JSON-файла.
- Режим викторины: загружает сохранённые вопросы из JSON-файла, по одному показывает их игроку, читает и проверяет его ответы, а в конце игры выводит счёт.
- Ошибки обрабатываются корректно, то есть приложение не паникует при любой неожиданной ситуации. Используйте
anyhowи оператор?, чтобы передача ошибок вверх была лаконичной. Об операторе?можно почитать здесь: https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator - Логика создания, хранения и загрузки вопросов викторины определена в библиотечной части крейта.
- Функциональность, связанная с пользовательским вводом (разбор аргументов, чтение из stdin), определена в коде приложения, а не в библиотеке.
- Логические части крейта разделены на модули.
Прежде чем писать код, выпишите все открытые вопросы и найдите на них ответы. Также рекомендуем нарисовать простую диаграмму модульной структуры приложения, подписав каждый модуль его зонами ответственности.
Упражнение 3.1.3: BSN
BSN (Burgerservicennummer) — голландский персональный идентификационный номер. По назначению он отчасти похож на американский Social Security Number. BSN подчиняется определённым правилам. В этом упражнении мы создадим тип Rust, который гарантирует, что представляет собой корректный BSN.
3.1.3.A Newtype ⭐⭐
В этой части мы реализуем проверку номера BSN, а также конструктор, который может завершиться ошибкой.
BSN корректен тогда и только тогда, когда соответствует следующим критериям:
- Он состоит из 8 или 9 цифр.
- Он проходит вариант проверки «одиннадцатью» (elfproef, нидерл.):
Для 8-значных BSN справа дописывается 0. Цифры номера обозначаются как ABCDEFGHI.
Например, для BSN 123456789 имеем A = 1, B = 2, C = 3 и так далее до I.
Тогда (9 × A) + (8 × B) + (7 × C) + (6 × D) + (5 × E) + (4 × F) + (3 × G) + (2 × H) + (-1 × I) должно делиться на 11.
Откройте exercises/3-crate-engineering/1-crate-engineering/3-bsn в редакторе. Там вы найдёте заготовку кода и два файла:
valid_bsns.inсо списком корректных BSN;invalid_bsns.inсо списком некорректных BSN.
В src/lib.rs реализуйте Bsn::validate, чтобы прошёл тест test_validation. Также реализуйте Bsn::try_from_string.
Чтобы запустить только тест test_validation, выполните:
cargo test -- test_validation
3.1.3.B Visitor и serde ⭐⭐⭐
Далее нужно реализовать трейты serde::Serialize и serde::Deserialize, чтобы поддержать сериализацию и десериализацию Bsn.
В этом случае просто вывести (derive) эти трейты не получится: после сериализации мы хотим представлять BSN строкой.
Мы также хотим десериализовать строки напрямую в Bsn, сохраняя гарантию, что созданный экземпляр Bsn представляет корректный BSN.
Поэтому Bsn::validate нужно включить в реализацию посетителя (visitor) для десериализации.
Подробнее о реализации трейтов:
serde::Serialize: https://serde.rs/impl-serialize.htmlserde::Deserialize: https://serde.rs/impl-deserialize.html
Если всё получилось, все тесты должны проходить.
Упражнение 3.1.4: 3D-принтер
Воображаемый 3D-принтер использует филамент, чтобы создавать самые разные изделия. Его состояния можно представить следующей диаграммой:
┌─────────────────┐
│ │
│ │ Reset
│ Idle │◄────────────────────────────┐
┌────────►│ │ │
│ │ │ │
│ │ │ │
│ └────────┬────────┘ │
│ │ │
│ │ │
│ │ Start │
│ │ │
│ ▼ │
│ ┌─────────────────┐ ┌────────┴────────┐
│ │ │ │ │
│ │ │ Out of filament │ │
Product │ │ Printing ├──────────────────► │ Error │
retrieved│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ └────────┬────────┘ └─────────────────┘
│ │
│ │ Product ready
│ │
│ ▼
│ ┌─────────────────┐
│ │ │
│ │ │
│ │ Product Ready │
└─────────┤ │
│ │
│ │
└─────────────────┘
Состояния на диаграмме сохранены на английском: это те же имена, которые используются в коде (Idle, Printing, Error, Product Ready).
Принтер при включении находится в состоянии Idle (ожидание). Когда задание запущено, принтер переходит в состояние Printing (печать).
В состоянии печати принтер продолжает печатать изделие, пока оно не будет готово или пока у принтера не закончится филамент.
Если филамент закончился, принтер переходит в состояние Error (ошибка), из которого можно выйти только после сброса устройства.
Если изделие готово, принтер переходит в состояние Product Ready (изделие готово), а когда пользователь заберёт изделие, принтер возвращается в Idle.
Принтер можно представить в Rust с помощью паттерна typestate, как описано на лекции. Это позволит написать простой драйвер 3D-принтера. В exercises/3-crate-engineering/1-crate-engineering/4-3d-printer создаётся структура Printer3D. Добавьте методы, соответствующие каждому из трейтов, которые имитируют переходы между состояниями, выводя текущее состояние. Метод, имитирующий проверку того, закончился ли филамент, уже предоставлен.
Конечно, чтобы сделать принтер реалистичнее, можно добавить больше состояний и переходов.