Создаём REST-сервис на Rust. Часть 5: обработчики, рефакторинг, и макросы

от автора

Всем привет!

Мы продолжаем писать веб-сервис на 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. Но не беда: метод .as_ref() превращает Option в Option<&T> — таким образом, мы получаем Option<&String>.

К сожалению, т.к. &String завёрнут в Option, он не преобразуется в &str автоматически. Поэтому мы используем вышеупомянутый синтаксис срезов в анонимной функции:

                                                   .map(|s| &s[..])

.map применяет функцию к содержимому Option и преобразует T из Option в некоторый другой тип. В данном случае, мы преобразуем &String в &str. Это похоже на хаскельный fmap :: Functor f => (a -> b) -> f a -> f b.

Есть тонкость: мы не могли вызвать .map сразу на name: Option, т.к. тогда ссылка будет валидна только в области видимости параметров функции при вызове. В таком случае мы получим ссылку внутри замыкания, и жить она будет только столько, сколько живёт замыкание. А оно нигде не сохраняется и будет уничтожено после передачи параметра в функцию. Такая ссылка будет временным объектом:

 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 реализует Deref.

impl<T> Deref for Arc<T> where T: ?Sized   type Target = T   fn deref(&self) -> &T

Таким образом, Arc будет автоматически преобразовано в &T, если это необходимо. Это даст нам &Mutex.

Дальше в дело вступает авто-разыменование при вызове методов. Вкратце, компилятор вставит столько разыменований при вызове метода, сколько надо.

Вот простой пример:

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>. Result позволяет нам обработать ошибку, а MutexGuard — это RAII-значение, которое автоматически откроет мьютекс, как только мы перестанем с ним работать.

То самое &* преобразует MutexGuard в &T – сначала мы его разыменовываем и получаем T, а затем берём адрес чтобы получить обычную ссылку, &T.

Почему lock() может напрямую работать с Arc<Mutex>, а 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 везде — в обработчиках и в функциях работы с БД. Всё, двойное клонирование больше не нужно:

                    {                         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 — пишите об этом в комментариях. А также заходите к нам в чат с вопросами, возникающими по ходу дела — вам там рады помочь.

ссылка на оригинал статьи http://habrahabr.ru/post/269903/


Комментарии

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

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