Всем привет!
Мы продолжаем писать веб-сервис на Rust. Оглавление:
Часть 1: прототип
Часть 2: читаем INI; multirust
Часть 3: обновляем базу из консоли
Часть 4: переходим к REST API
Часть 5 (эта): обработчики, рефакторинг, и макросы
Теперь мы рассмотрим собственно обработчики запросов к API и перепишем предыдущий, страшный код. И вообще, это последняя статья из цикла, поэтому здесь будут рефакторинг, стиль, макросы и все-все-все. Это самая длинная часть.
Вот как сейчас выглядит код, настраивающий пути к API:
Для начала — сами обработчики. Вот, например, handlers::get_records():
Его сигнатура — причина того, что нам пришлось клонировать Arc с базой данных внутри замыкания:
Как видите, Arc сюда передаётся по значению (т.е. с владением), и он не является типом, который тривиально копируется. По этой причине мы и клонировали Arc для передачи в обработчик.
В целом, обработчики однотипны, поэтому я относительно подробно рассмотрю только get_records — он самый сложный. Хочу отметить, в обработчиках активно используется сопоставление с образцом для определения ошибочных ситуаций.
Сначала мы получаем Url в формате rust-url из Url Iron’а.
Мы делаем это, чтобы затем воспользоваться методом query_pairs, который разбирает URL как данные application/x-www-form-urlencoded и (возможно) возвращает итератор по парам ключ-значение.
Сейчас я покажу новый синтаксис «if let», а затем расскажу, в чём его суть.
Вы, возможно, уже догадались, что означает эта запись. Оператор «if let» делает попытку сопоставления с образцом, и, если она успешна, передаёт исполнение в блок за if let. В этом блоке будет доступно имя, с которым мы только что связали значение — в данном случае, qp. Если же сопоставить значение с шаблоном не удалось (query_pairs() вернул None), то выполняется ветвь else — похоже на обычный if.
Соответственно, если итератор нам не вернули, это ошибка:
Здесь у нас в скобках кортеж, описывающий ответ сервера: HTTP-статус и сообщение.
Если же нам вернули итератор, мы обходим его, чтобы получить параметр name и сохранить его в переменную name:
Цикл здесь нужен, чтобы обойти итератор, вытащить нужный элемент из вектора пар, и не иметь при этом проблем с владением. Но на самом деле любая ситуация, когда нам не передали ровно один параметр запроса, который называется name, является ошибочной. Давайте попробуем убрать цикл.
.query_pairs() на самом деле возвращает Option<Vec<(String, String)>>. Поэтому мы можем просто проверить длину вектора и имя единственного параметра:
Теперь мы не обходим вектор, а проверяем его длину и обращаемся сразу к параметру, который нас интересует.
Здесь есть важный момент:
Принципиально использовать pop() — он передаёт нам элемент вектора с владением. Обычное обращение по индексу (qp[0]) дало бы ссылку, и мы не смогли бы переместить value из пары в Some(value), чтобы положить всё это в name.
Стоит также отметить, что в нашем векторе хранятся пары (String, String). Но дальше мы напрямую сравниваем key с «name» — строковым литералом:
Он, как вы помните, имеет тип &'static str. Это работает, потому что String реализует типаж PartialEq для сравнения с &'a str:
Поэтому здесь не происходит никаких преобразований типов.
Если же такого типажа не было бы, мы могли бы преобразовать String в &str с помощью синтаксиса срезов: &key[..] вернёт срез по всей строке, т.е. ссылку-&str с тем же содержимым.
Далее мы производим собственно доступ к базе данных.
Сначала объявим имя для JSON-записей, которые должна будет вернуть наша точка доступа REST:
Хм, мы не инициализируем его никаким значением? Хотим выстрелить себе в ногу?
Нет, Rust не даст нам использовать объявленное имя, пока оно не будет инициализировано. Например, в таком коде
произойдёт ошибка:
Далее мы читаем записи из базы данных:
Почему в аргументах происходит нечто странное?
Сейчас я объясню. Сначала посмотрим на сигнатуру нашей ::db::read():
Как видите, она принимает name в виде Option<&str>. Наш же name имеет тип
К сожалению, т.к. &String завёрнут в Option, он не преобразуется в &str автоматически. Поэтому мы используем вышеупомянутый синтаксис срезов в анонимной функции:
.map применяет функцию к содержимому Option и преобразует T из
Есть тонкость: мы не могли вызвать .map сразу на name:
В случае с .as_ref() же ссылка живёт, пока живёт сам Option, поэтому всё работает.
Давайте заглянем в ::db::read и посмотрим, как работает хвалёная защита от гонок данных.
Мы хотим вызвать show:
Эта функция принимает ссылку на Connection, а у нас
Итак, мы хотим завладеть мьютексом. Он вложен в счётчик ссылок. Здесь в дело вступают две вещи: преобразование при разыменовании и авто-разыменование при вызове методов.
Пока проигнорируем странное &* и посмотрим на сам sdb.lock(). sdb — это Arc, но
Таким образом,
Дальше в дело вступает авто-разыменование при вызове методов. Вкратце, компилятор вставит столько разыменований при вызове метода, сколько надо.
Вот простой пример:
Все четыре последние строки делают одно и то же.
Mutex::lock вернёт нам
То самое
Почему lock() может напрямую работать с
После получения наших записей мы хотим сериализовать их в JSON. Для этого воспользуемся rustc_serialize:
Как видите, мы можем импортировать модули не только глобально, но и в области видимости отдельной функции или блока. Это помогает не засорять глобальное пространство имён.
Сама сериализация делается этим кодом:
При этом код сериализатора генерируется автоматически! Нам нужно лишь объявить тип наших записей, как сериализуемый (и заодно, десериализуемый):
Наконец, мы оборачиваем наш JSON в правильный HTTP с соответствующими заголовками и возвращаем его:
Остальные обработчики работают аналогично, поэтому вместо повторения займёмся рефакторингом нашего кода.
В целом же, наша программа закончена! Теперь нашу замечательную телефонную книгу можно обновлять не только из командной строки, но и через модное веб-API. Если хотите посмотреть, как всё работает, возьмите версию кода по тегу feature-complete с GitHub.
Рефакторинг не так сложен, и я показываю этот процесс просто затем, чтобы убедить вас, что код на Rust тоже может быть красивым. Тот нечитабельный бардак, который мы развели в процессе реализации функционала — это просто результат спешки. Rust в этом не виноват — на нём можно писать элегантный код.
В первую очередь, давайте разберёмся с тем двойным клонированием Arc, о котором я говорил в предыдущей части:
Это очень просто победить. Изменим сигнатуру handlers::get_records с
на
И вообще, используем
С огромным main тоже стоит разобраться. Просто выносим все действия в свои функции и получаем классный компактный main:
Напоследок сладкое: rustfmt! Утилита форматирования исходного кода на Rust ещё не закончена, но уже годится для украшения кода нашего маленького проекта.
Склонировав репозиторий, сделаем cargo build --release, а затем скопируем полученный исполняемый файл куда-нибудь в $PATH. Затем, в корне нашего проекта сделаем
И всё, код всего проекта мгновенно отформатирован! rustfmt следует по ссылкам на другие модули и форматирует и их тоже.
В отличие от gofmt, rustfmt позволяет довольно подробно настроить стиль, в котором исходник будет переписан.
Нынешний стиль по-умолчанию — примерно такой, в котором написан сам компилятор. Однако по мере доработки официального руководства по стилю, rustfmt тоже будет допилен.
На этом «разумный» рефакторинг заканчивается, и начинается… нечто спорное, но однозначно весёлое: давайте уберём оставшееся повторение похожего кода с помощью макросов.
Про какое повторение я говорю? Про это:
Очевидно, тут есть некоторая высокоуровневая структура, которую нам не удалось отразить в коде. Поскольку эти блоки отличаются методом, который нужно вызывать у router, для обработки всех этих вариантов в обычной функции понадобился бы match по некоторому перечислению, который вызывал бы соответствующий метод в зависимости от аргумента.
Это, вообще говоря, вариант, и скорее всего я бы попытался сделать это, если бы писал этот код на работе, но тут мы веселимся, а мне давно хотелось попробовать макросы в Rust. Так что приступим.
Для начала, повторяющаяся структура тут — это блок, который клонирует Arc и затем выполняет оператор. Попробуем написать соответствующий макрос:
Первая строка говорит, что мы начали определять макрос под названием clone_pass_bound. Дурацкое название, но лучше придумать не получилось. Это — само по себе симптом того, что так, наверное, не стоит делать в рабочем коде. Но да ладно — это сейчас не наш случай.
Макросы в Rust типизированные, и наш принимает два аргумента — $arc типа «идентификатор» (ident) и $stmt типа «оператор» (statement, stmt). Если присмотреться, можно заметить похожесть определения макроса на match — здесь сопоставляется определённая комбинация аргументов определённому телу. Ветвей у макроса может быть много, как и у match — и это полезно в случае рекурсии.
После стрелки идут две пары фигурных скобок. Одни требуются согласно синтаксису описания макроса — в общем, как и в обычном match.
С помощью второй пары мы говорим, что наш макрос раскрывается в блок. Внутри блока пишем практически обычный код, заменяя sdb на $arc. Это тривиальное обобщение. За клонированием следует наш оператор.
Вот как этот макрос вызывается:
Пока мы ничего не сэкономили по объёму, только получили непонятный вызов. Но не отчаиваемся — мы только начали!
Теперь становится видно, что один обработчик можно описать с помощью четырёх параметров: соединение с БД, router, какой метод ему добавлять (get, post, и т.д.), и как называется определённый нами обработчик. Напишем макрос для этого:
Здесь в первую очередь стоит ещё раз подчеркнуть сходство вызова макроса с обычным сопоставлением с образцом. Как видите, разделителем аргументов макроса не обязательно должна быть запятая — router и его метод мы разделили точкой, для большего сходства с обычным кодом.
Затем тупо заменяем все конкретные имена на мета-переменные макроса и вызываем наш предыдущий макрос — не так уж страшно и сложно. Оба этих макроса я вообще написал с первой попытки.
Теперь мы написали два десятка строк безумных макросов, и код, который мы хотели сократить, наконец, начал уменьшаться:
Это не предел — сейчас мы определим последний макрос, который сделает наше определение очень компактным и довольно понятным. Теперь изменяющиеся части кода совсем очевидны, и сделать код совсем DRY ничто не помешает.
В нашем последнем макросе будет ровно 1 (один) нетривиальный момент.
Вот как выглядит последний макрос:
Он довольно небольшой. Нетривиальный момент — это то, что мы ввели повторяемость в аргументах:
Далее — тело нашего макроса-монстра. Сначала я написал так:
На что компилятор возразил:
Как я говорил, часть вызова, определяющая $method, $route и $handler может повторяться. В макросах Rust действует правило, что мета-переменная, находящаяся на определённом «уровне» повторений в вызове, должна находится на том же уровне повторений при использовании.
Об этом можно думать так — кортежи параметров макро-вызовов перебираются одновременно с соответствующими телами. Т.е. одному набору параметров должно соответствовать одно тело. Таким образом становится легче понять структуру макроса — тело становится похоже на вызов.
А сейчас у нас макрос записан так, будто у него всего одно тело — получается, что параметры вызова повторяются, а тела повторить нельзя. Тогда, какой же именно $method должен быть в теле? Непонятно. Вот для избежания таких ситуаций и придумано правило перебора параметров «в ногу» с телами.
Для нас это всё значит, что нужно обернуть тело в такой же модификатор повторяемости, как у параметров:
Теперь $method, $route и $handler соответствуют повторяющимся параметрам. А $connection и $router «глобальны» — они не находятся ни под одним модификатором повторяемости, поэтому они будут дублироваться в каждом теле.
В награду за этот мозговынос мы получаем красивое определение всех путей в нашем API:
Никакого лишнего дублирования, и в окончательном варианте выглядит даже относительно понятно для непосвящённого.
Хочу отметить, что макросы в Rust гигиеничны — столкновения имён внутри макроса с именами снаружи исключены.
Ах да, чуть не забыл — в отладке макросов очень помогает опция компилятора --pretty-print=expand. Так он напечатает код после раскрытия макросов в поток стандартного вывода. Похоже на опцию -E у компиляторов Си и C++.
На этом всё. Теперь уже совсем всё — я думаю, этот цикл статей рассказал достаточно, чтобы вы сами могли начать строить свой код на Rust, в том числе для веба.
Если вы уже начали делать что-то на Rust — пишите об этом в комментариях. А также заходите к нам в чат с вопросами, возникающими по ходу дела — вам там рады помочь.
Мы продолжаем писать веб-сервис на Rust. Оглавление:
Часть 1: прототип
Часть 2: читаем INI; multirust
Часть 3: обновляем базу из консоли
Часть 4: переходим к REST API
Часть 5 (эта): обработчики, рефакторинг, и макросы
Теперь мы рассмотрим собственно обработчики запросов к API и перепишем предыдущий, страшный код. И вообще, это последняя статья из цикла, поэтому здесь будут рефакторинг, стиль, макросы и все-все-все. Это самая длинная часть.
Почему мы клонировали Arc дважды
Вот как сейчас выглядит код, настраивающий пути к API:
let sdb = Arc::new(Mutex::new(db));
let mut router = router::Router::new();
{
let sdb_ = sdb.clone();
router.get("/api/v1/records",
move |req: &mut Request|
handlers::get_records(sdb_.clone(), req));
}
{
let sdb_ = sdb.clone();
router.get("/api/v1/records/:id",
move |req: &mut Request|
handlers::get_record(sdb_.clone(), req));
}
…
Для начала — сами обработчики. Вот, например, handlers::get_records():
handlers::get_records
pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> {
let url = req.url.clone().into_generic_url();
let mut name: Option<String> = None;
if let Some(qp) = url.query_pairs() {
for (key, value) in qp {
match (&key[..], value) {
("name", n) => {
if let None = name {
name = Some(n);
} else {
return Ok(Response::with((status::BadRequest, "passed name in query more than once")));
}
}
_ => return Ok(Response::with((status::BadRequest, "unexpected query parameters"))),
}
}
} else {
return Ok(Response::with((status::BadRequest, "passed names don’t parse as application/x-www-form-urlencoded or there are no parameters")));
}
let json_records;
if let Ok(recs) = ::db::read(sdb, name.as_ref().map(|s| &s[..])) {
use rustc_serialize::json;
if let Ok(json) = json::encode(&recs) {
json_records = Some(json);
} else {
return Ok(Response::with((status::InternalServerError, "couldn't convert records to JSON")));
}
} else {
return Ok(Response::with((status::InternalServerError, "couldn't read records from database")));
}
let content_type = Mime(
TopLevel::Application, SubLevel::Json, Vec::new());
Ok(Response::with(
(content_type, status::Ok, json_records.unwrap())))
}
Его сигнатура — причина того, что нам пришлось клонировать Arc с базой данных внутри замыкания:
pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> {
Как видите, Arc сюда передаётся по значению (т.е. с владением), и он не является типом, который тривиально копируется. По этой причине мы и клонировали Arc для передачи в обработчик.
Что происходит в обработчиках
В целом, обработчики однотипны, поэтому я относительно подробно рассмотрю только get_records — он самый сложный. Хочу отметить, в обработчиках активно используется сопоставление с образцом для определения ошибочных ситуаций.
Сначала мы получаем Url в формате rust-url из Url Iron’а.
let url = req.url.clone().into_generic_url();
Мы делаем это, чтобы затем воспользоваться методом query_pairs, который разбирает URL как данные application/x-www-form-urlencoded и (возможно) возвращает итератор по парам ключ-значение.
if let
Сейчас я покажу новый синтаксис «if let», а затем расскажу, в чём его суть.
if let Some(qp) = url.query_pairs() {
for (key, value) in qp {
Вы, возможно, уже догадались, что означает эта запись. Оператор «if let» делает попытку сопоставления с образцом, и, если она успешна, передаёт исполнение в блок за if let. В этом блоке будет доступно имя, с которым мы только что связали значение — в данном случае, qp. Если же сопоставить значение с шаблоном не удалось (query_pairs() вернул None), то выполняется ветвь else — похоже на обычный if.
Возврат ошибочных HTTP-статусов
Соответственно, если итератор нам не вернули, это ошибка:
} else {
return Ok(Response::with((status::BadRequest, “passed names don’t parse as application/x-www-form-urlencoded or there are no parameters”)));
}
Здесь у нас в скобках кортеж, описывающий ответ сервера: HTTP-статус и сообщение.
Получаем параметры запроса
Если же нам вернули итератор, мы обходим его, чтобы получить параметр name и сохранить его в переменную name:
let mut name: Option<String> = None;
if let Some(qp) = url.query_pairs() {
for (key, value) in qp {
match (&key[..], value) {
("name", n) => {
if let None = name {
name = Some(n);
} else {
return Ok(Response::with((status::BadRequest, "passed name in query more than once")));
}
}
_ => return Ok(Response::with((status::BadRequest, "unexpected query parameters"))),
}
}
}
Цикл здесь нужен, чтобы обойти итератор, вытащить нужный элемент из вектора пар, и не иметь при этом проблем с владением. Но на самом деле любая ситуация, когда нам не передали ровно один параметр запроса, который называется name, является ошибочной. Давайте попробуем убрать цикл.
Убираем цикл по параметрам
.query_pairs() на самом деле возвращает Option<Vec<(String, String)>>. Поэтому мы можем просто проверить длину вектора и имя единственного параметра:
let mut name: Option<String> = None;
if let Some(mut qp) = url.query_pairs() {
if qp.len() != 1 {
return Ok(Response::with((status::BadRequest, "passed more than one parameter or no parameters at all")));
}
let (key, value) = qp.pop().unwrap();
if key == "name" {
name = Some(value);
}
} else {
Теперь мы не обходим вектор, а проверяем его длину и обращаемся сразу к параметру, который нас интересует.
Здесь есть важный момент:
let (key, value) = qp.pop().unwrap();
Принципиально использовать pop() — он передаёт нам элемент вектора с владением. Обычное обращение по индексу (qp[0]) дало бы ссылку, и мы не смогли бы переместить value из пары в Some(value), чтобы положить всё это в name.
Почему работает сравнение String со &str?
Стоит также отметить, что в нашем векторе хранятся пары (String, String). Но дальше мы напрямую сравниваем key с «name» — строковым литералом:
if key == "name" {
name = Some(value);
}
Он, как вы помните, имеет тип &'static str. Это работает, потому что String реализует типаж PartialEq для сравнения с &'a str:
impl<'a> PartialEq<&'a str> for String
Поэтому здесь не происходит никаких преобразований типов.
Если же такого типажа не было бы, мы могли бы преобразовать String в &str с помощью синтаксиса срезов: &key[..] вернёт срез по всей строке, т.е. ссылку-&str с тем же содержимым.
Далее мы производим собственно доступ к базе данных.
Неинициализированные переменные — это опасно?
Сначала объявим имя для JSON-записей, которые должна будет вернуть наша точка доступа REST:
let json_records;
Хм, мы не инициализируем его никаким значением? Хотим выстрелить себе в ногу?
Нет, Rust не даст нам использовать объявленное имя, пока оно не будет инициализировано. Например, в таком коде
fn main() {
let a;
if true {
a = 5;
} else {
println!("{}", a);
}
}
произойдёт ошибка:
test.rs:6:24: 6:25 error: use of possibly uninitialized variable: `a` [E0381]
test.rs:6 println!("{}", a);
^
Читаем записи из БД. Пользуемся Option::map
Далее мы читаем записи из базы данных:
if let Ok(recs) = ::db::read(sdb, name.as_ref().map(|s| &s[..])) {
Почему в аргументах происходит нечто странное?
name.as_ref().map(|s| &s[..])
Сейчас я объясню. Сначала посмотрим на сигнатуру нашей ::db::read():
pub fn read(sdb: Arc<Mutex<Connection>>, name: Option<&str>) -> Result<Vec<Record>, ()> {
Как видите, она принимает name в виде Option<&str>. Наш же name имеет тип
Option<String>
. Но не беда: метод .as_ref() превращает Option в Option<&T> — таким образом, мы получаем Option<&String>
.К сожалению, т.к. &String завёрнут в Option, он не преобразуется в &str автоматически. Поэтому мы используем вышеупомянутый синтаксис срезов в анонимной функции:
.map(|s| &s[..])
.map применяет функцию к содержимому Option и преобразует T из
Option<T>
в некоторый другой тип. В данном случае, мы преобразуем &String в &str. Это похоже на хаскельный fmap :: Functor f => (a -> b) -> f a -> f b.Есть тонкость: мы не могли вызвать .map сразу на name:
Option<T>
, т.к. тогда ссылка будет валидна только в области видимости параметров функции при вызове. В таком случае мы получим ссылку внутри замыкания, и жить она будет только столько, сколько живёт замыкание. А оно нигде не сохраняется и будет уничтожено после передачи параметра в функцию. Такая ссылка будет временным объектом:handlers.rs:25:53: 25:54 error: `s` does not live long enough handlers.rs:25 if let Ok(recs) = ::db::read(sdb, name.map(|s| &s[..])) { ^ handlers.rs:25:23: 25:60 note: reference must be valid for the call at 25:22... handlers.rs:25 if let Ok(recs) = ::db::read(sdb, name.map(|s| &s[..])) { ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ handlers.rs:25:52: 25:58 note: ...but borrowed value is only valid for the scope of parameters for function at 25:51 handlers.rs:25 if let Ok(recs) = ::db::read(sdb, name.map(|s| &s[..])) { ^~~~~~
В случае с .as_ref() же ссылка живёт, пока живёт сам Option, поэтому всё работает.
А что же многопоточность?
Давайте заглянем в ::db::read и посмотрим, как работает хвалёная защита от гонок данных.
if let Ok(rs) = show(&*sdb.lock().unwrap(), name) {
Ok(rs)
} else {
Err(())
}
Мы хотим вызвать show:
pub fn show(db: &Connection, arg: Option<&str>) -> ::postgres::Result<Vec<Record>> {
Эта функция принимает ссылку на Connection, а у нас
Arc<Mutex<Connection>>
. Мы никак не сможем добраться до интересующего нас соединения с БД, кроме как развернув счётчик ссылок и завладев мьютексом. Система типов делает неверные состояния непредставимыми.Почти магия
Итак, мы хотим завладеть мьютексом. Он вложен в счётчик ссылок. Здесь в дело вступают две вещи: преобразование при разыменовании и авто-разыменование при вызове методов.
Пока проигнорируем странное &* и посмотрим на сам sdb.lock(). sdb — это Arc, но
Arc<T>
реализует Deref<T>
. impl<T> Deref for Arc<T> where T: ?Sized
type Target = T
fn deref(&self) -> &T
Таким образом,
Arc<T>
будет автоматически преобразовано в &T, если это необходимо. Это даст нам &Mutex<Connection>
.Дальше в дело вступает авто-разыменование при вызове методов. Вкратце, компилятор вставит столько разыменований при вызове метода, сколько надо.
Вот простой пример:
struct Foo;
impl Foo {
fn foo(&self) { println!("Foo"); }
}
let f = Foo;
f.foo();
(&f).foo();
(&&f).foo();
(&&&&&&&&f).foo();
Все четыре последние строки делают одно и то же.
Безопасное освобождение мьютекса с помощью RAII
Mutex::lock вернёт нам
LockResult<MutexGuard<T>>
. Result позволяет нам обработать ошибку, а MutexGuard<T>
— это RAII-значение, которое автоматически откроет мьютекс, как только мы перестанем с ним работать.То самое
&*
преобразует MutexGuard<T>
в &T – сначала мы его разыменовываем и получаем T, а затем берём адрес чтобы получить обычную ссылку, &T.Почему lock() может напрямую работать с
Arc<Mutex<Connection>>
, а MutexGuard нужно преобразовывать вручную? Потому что lock – это метод, а вызов методов на самом деле будет не только разыменовывать ссылки, но и преобразовывать одни ссылки в другие (т.е. делать аналог &*
). При передаче аргументов в функцию это надо делать вручную.Сериализация
После получения наших записей мы хотим сериализовать их в JSON. Для этого воспользуемся rustc_serialize:
use rustc_serialize::json;
Как видите, мы можем импортировать модули не только глобально, но и в области видимости отдельной функции или блока. Это помогает не засорять глобальное пространство имён.
Сама сериализация делается этим кодом:
if let Ok(json) = json::encode(&recs) {
json_records = Some(json);
}
...
При этом код сериализатора генерируется автоматически! Нам нужно лишь объявить тип наших записей, как сериализуемый (и заодно, десериализуемый):
#[derive(RustcEncodable, RustcDecodable)]
pub struct Record {
id: Option<i32>,
pub name: String,
pub phone: String,
}
Отправляем всё назад
Наконец, мы оборачиваем наш JSON в правильный HTTP с соответствующими заголовками и возвращаем его:
let content_type = Mime(
TopLevel::Application, SubLevel::Json, Vec::new());
Ok(Response::with(
(content_type, status::Ok, json_records.unwrap())))
Остальные обработчики работают аналогично, поэтому вместо повторения займёмся рефакторингом нашего кода.
В целом же, наша программа закончена! Теперь нашу замечательную телефонную книгу можно обновлять не только из командной строки, но и через модное веб-API. Если хотите посмотреть, как всё работает, возьмите версию кода по тегу feature-complete с GitHub.
Рефакторинг не так сложен, и я показываю этот процесс просто затем, чтобы убедить вас, что код на Rust тоже может быть красивым. Тот нечитабельный бардак, который мы развели в процессе реализации функционала — это просто результат спешки. Rust в этом не виноват — на нём можно писать элегантный код.
Не клонируем клонов
В первую очередь, давайте разберёмся с тем двойным клонированием Arc, о котором я говорил в предыдущей части:
{
let sdb_ = sdb.clone();
router.get("/api/v1/records",
move |req: &mut Request|
handlers::get_records(sdb_.clone(), req));
}
Это очень просто победить. Изменим сигнатуру handlers::get_records с
pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> {
на
pub fn get_records(sdb: &Mutex<Connection>, req: &mut Request) -> IronResult<Response> {
И вообще, используем
&Mutex<Connection>
везде — в обработчиках и в функциях работы с БД. Всё, двойное клонирование больше не нужно: {
let sdb = sdb.clone();
router.get("/api/v1/records",
move |req: &mut Request|
handlers::get_records(&*sdb, req));
}
С огромным main тоже стоит разобраться. Просто выносим все действия в свои функции и получаем классный компактный main:
fn main() {
let (params, sslmode) = params();
let db = Connection::connect(params, &sslmode).unwrap();
init_db(&db);
let args: Vec<String> = std::env::args().collect();
match args.get(1) {
Some(text) => {
match text.as_ref() {
"add" => add(&db, &args),
"del" => del(&db, &args),
"edit" => edit(&db, &args),
"show" => show(&db, &args),
"help" => println!("{}", HELP),
"serve" => serve(db),
command => panic!(
format!("Invalid command: {}", command))
}
}
None => panic!("No command supplied"),
}
}
rustfmt!
Напоследок сладкое: rustfmt! Утилита форматирования исходного кода на Rust ещё не закончена, но уже годится для украшения кода нашего маленького проекта.
Склонировав репозиторий, сделаем cargo build --release, а затем скопируем полученный исполняемый файл куда-нибудь в $PATH. Затем, в корне нашего проекта сделаем
$ rustfmt src/main.rs
И всё, код всего проекта мгновенно отформатирован! rustfmt следует по ссылкам на другие модули и форматирует и их тоже.
В отличие от gofmt, rustfmt позволяет довольно подробно настроить стиль, в котором исходник будет переписан.
Нынешний стиль по-умолчанию — примерно такой, в котором написан сам компилятор. Однако по мере доработки официального руководства по стилю, rustfmt тоже будет допилен.
На этом «разумный» рефакторинг заканчивается, и начинается… нечто спорное, но однозначно весёлое: давайте уберём оставшееся повторение похожего кода с помощью макросов.
Макросы
Про какое повторение я говорю? Про это:
{
let sdb = sdb.clone();
router.get("/api/v1/records",
move |req: &mut Request|
handlers::get_records(&*sdb, req));
}
{
let sdb = sdb.clone();
router.get("/api/v1/records/:id",
move |req: &mut Request|
handlers::get_record(&*sdb, req));
}
{
let sdb = sdb.clone();
router.post("/api/v1/records",
move |req: &mut Request|
handlers::add_record(&*sdb, req));
}
{
let sdb = sdb.clone();
router.put("/api/v1/records/:id",
move |req: &mut Request|
handlers::update_record(&*sdb, req));
}
{
let sdb = sdb.clone();
router.delete("/api/v1/records/:id",
move |req: &mut Request|
handlers::delete_record(&*sdb, req));
}
Очевидно, тут есть некоторая высокоуровневая структура, которую нам не удалось отразить в коде. Поскольку эти блоки отличаются методом, который нужно вызывать у router, для обработки всех этих вариантов в обычной функции понадобился бы match по некоторому перечислению, который вызывал бы соответствующий метод в зависимости от аргумента.
Это, вообще говоря, вариант, и скорее всего я бы попытался сделать это, если бы писал этот код на работе, но тут мы веселимся, а мне давно хотелось попробовать макросы в Rust. Так что приступим.
Для начала, повторяющаяся структура тут — это блок, который клонирует Arc и затем выполняет оператор. Попробуем написать соответствующий макрос:
macro_rules! clone_pass_bound {
($arc:ident, $stmt:stmt) => {
{
let $arc = $arc.clone();
$stmt;
}
}
}
Первая строка говорит, что мы начали определять макрос под названием clone_pass_bound. Дурацкое название, но лучше придумать не получилось. Это — само по себе симптом того, что так, наверное, не стоит делать в рабочем коде. Но да ладно — это сейчас не наш случай.
Макросы в Rust типизированные, и наш принимает два аргумента — $arc типа «идентификатор» (ident) и $stmt типа «оператор» (statement, stmt). Если присмотреться, можно заметить похожесть определения макроса на match — здесь сопоставляется определённая комбинация аргументов определённому телу. Ветвей у макроса может быть много, как и у match — и это полезно в случае рекурсии.
После стрелки идут две пары фигурных скобок. Одни требуются согласно синтаксису описания макроса — в общем, как и в обычном match.
С помощью второй пары мы говорим, что наш макрос раскрывается в блок. Внутри блока пишем практически обычный код, заменяя sdb на $arc. Это тривиальное обобщение. За клонированием следует наш оператор.
Вот как этот макрос вызывается:
clone_pass_bound!(
sdb,
router.get("/api/v1/records",
move |req: &mut Request|
handlers::get_records(&*sdb, req)));
Пока мы ничего не сэкономили по объёму, только получили непонятный вызов. Но не отчаиваемся — мы только начали!
Макрос на макросе
Теперь становится видно, что один обработчик можно описать с помощью четырёх параметров: соединение с БД, router, какой метод ему добавлять (get, post, и т.д.), и как называется определённый нами обработчик. Напишем макрос для этого:
macro_rules! define_handler {
($connection:ident, $router: ident.$method:ident, $route:expr,
$handler:path) => {
clone_pass_bound!(
$connection,
$router.$method(
$route,
move |req: &mut Request|
$handler(&*$connection, req)));
}
}
Здесь в первую очередь стоит ещё раз подчеркнуть сходство вызова макроса с обычным сопоставлением с образцом. Как видите, разделителем аргументов макроса не обязательно должна быть запятая — router и его метод мы разделили точкой, для большего сходства с обычным кодом.
Затем тупо заменяем все конкретные имена на мета-переменные макроса и вызываем наш предыдущий макрос — не так уж страшно и сложно. Оба этих макроса я вообще написал с первой попытки.
Теперь мы написали два десятка строк безумных макросов, и код, который мы хотели сократить, наконец, начал уменьшаться:
define_handler!(sdb, router.get, "/api/v1/records", handlers::get_records);
define_handler!(sdb, router.get, "/api/v1/records/:id",
handlers::get_record);
define_handler!(sdb, router.post, "/api/v1/records", handlers::add_record);
define_handler!(sdb, router.put, "/api/v1/records/:id",
handlers::update_record);
define_handler!(sdb, router.delete, "/api/v1/records/:id",
handlers::delete_record);
Это не предел — сейчас мы определим последний макрос, который сделает наше определение очень компактным и довольно понятным. Теперь изменяющиеся части кода совсем очевидны, и сделать код совсем DRY ничто не помешает.
В нашем последнем макросе будет ровно 1 (один) нетривиальный момент.
Макросом погоняет
Вот как выглядит последний макрос:
macro_rules! define_handlers_family {
($connection:ident, $router:ident,
$( [$method:ident, $route:expr, $handler:path]),+ ) => {
$( define_handler!($connection, $router.$method, $route, $handler); )+
}
}
Он довольно небольшой. Нетривиальный момент — это то, что мы ввели повторяемость в аргументах:
($connection:ident, $router:ident,
$( [$method:ident, $route:expr, $handler:path]),+ ) => {
$( … ),+
означает, что заключённая в скобках группа должна повторяться один или более раз при вызове этого макроса. Похоже на регулярные выражения.Далее — тело нашего макроса-монстра. Сначала я написал так:
define_handler!($connection, $router.$method, $route, $handler);
На что компилятор возразил:
main.rs:134:46: 134:53 error: variable 'method' is still repeating at this depth
main.rs:134 define_handler!($connection, $router.$method, $route, $handler);
^~~~~~~
Как я говорил, часть вызова, определяющая $method, $route и $handler может повторяться. В макросах Rust действует правило, что мета-переменная, находящаяся на определённом «уровне» повторений в вызове, должна находится на том же уровне повторений при использовании.
Об этом можно думать так — кортежи параметров макро-вызовов перебираются одновременно с соответствующими телами. Т.е. одному набору параметров должно соответствовать одно тело. Таким образом становится легче понять структуру макроса — тело становится похоже на вызов.
А сейчас у нас макрос записан так, будто у него всего одно тело — получается, что параметры вызова повторяются, а тела повторить нельзя. Тогда, какой же именно $method должен быть в теле? Непонятно. Вот для избежания таких ситуаций и придумано правило перебора параметров «в ногу» с телами.
Для нас это всё значит, что нужно обернуть тело в такой же модификатор повторяемости, как у параметров:
$( define_handler!($connection, $router.$method, $route, $handler); )+
Теперь $method, $route и $handler соответствуют повторяющимся параметрам. А $connection и $router «глобальны» — они не находятся ни под одним модификатором повторяемости, поэтому они будут дублироваться в каждом теле.
В награду за этот мозговынос мы получаем красивое определение всех путей в нашем API:
define_handlers_family!(
sdb, router,
[get, "/api/v1/records", handlers::get_records],
[get, "/api/v1/records/:id", handlers::get_record],
[post, "/api/v1/records", handlers::add_record],
[put, "/api/v1/records/:id", handlers::update_record],
[delete, "/api/v1/records/:id", handlers::delete_record]);
Никакого лишнего дублирования, и в окончательном варианте выглядит даже относительно понятно для непосвящённого.
Хочу отметить, что макросы в Rust гигиеничны — столкновения имён внутри макроса с именами снаружи исключены.
Ах да, чуть не забыл — в отладке макросов очень помогает опция компилятора --pretty-print=expand. Так он напечатает код после раскрытия макросов в поток стандартного вывода. Похоже на опцию -E у компиляторов Си и C++.
До новых встреч!
На этом всё. Теперь уже совсем всё — я думаю, этот цикл статей рассказал достаточно, чтобы вы сами могли начать строить свой код на Rust, в том числе для веба.
Если вы уже начали делать что-то на Rust — пишите об этом в комментариях. А также заходите к нам в чат с вопросами, возникающими по ходу дела — вам там рады помочь.
Комментарии (3)
frol
03.11.2015 05:25+1Просто оставлю это здесь: есть удобный сайт со статусом готовности Rust к Web — arewewebyet.com:
You can use Rust for web stuff, but the ecosystem isn’t mature yet.
tgz
Great!