Створюємо REST-сервіс на Rust. Частина 3: оновлюємо базу з консолі

Всім привіт!

попередній частині ми розібрали конфігураційний файл бази даних, щоб рахувати з нього параметри з'єднання.

Тепер давайте реалізуємо безпосередньо операції оновлення БД: створення, оновлення, видалення наших записів і відповідний їм інтерфейс командного рядка.



Для початку, давайте розберемо аргументи програми. Її інтерфейс буде виглядати так:

const HELP: &'static str = "Usage: phonebook COMMAND [ARG]...
Commands:
add NAME PHONE - create new record;
del ID1 ID2... - delete record;
edit ID - record edit;
show - display all records;
show STRING - display records which contain a given substring in the name;
help - display this help.";

Тут вже є пара цікавих моментів. const оголошує постійну, причому таку, що вона просто вбудовується в місце використання. Таким чином, у неї немає своєї адреси в пам'яті — схоже на #define в C. Тип постійної треба вказувати завжди — і в даному випадку він може виглядати трохи моторошно. &'static str? Що це?

Якщо мені не зраджує пам'ять, явно зазначених часів життя ми ще не бачили. Так от, це — посилання, &str, і її можна по-іншому записати як &'foo str. Зазвичай нам не доводиться явно вказувати час життя, оскільки компілятор може сам вивести його — тобто 'foo просто опускається.

Зазначу також, що 'foo могло б бути 'bar або чим завгодно ще — це просто ім'я змінної. В нашому випадку, можна думати так: посилання HELP: &str має час життя, зване 'foo, і воно одно 'static.

Тепер про 'static. Це час життя, що дорівнює часу життя програми. Наша рядок безпосередньо вбудована в образ програми, і їй не потрібна яка-небудь ініціалізація або явне знищення. Тому вона завжди доступна, поки програма виконується. Детальніше про 'static можна прочитати тут.

Таким чином, ми оголосили строкову постійну, яка завжди доступна.

А ось код розбору аргументів — як завжди, спочатку цілком. Потім ми розглянемо його докладніше.

Код розбору командного рядка
let args: Vec<String> = std::env::args().collect();
match args.get(1) {
Some(text) => {
match text.as_ref() {
"add" => {
if args.len() != 4 {
panic!("Usage: phonebook add NAME PHONE");
}
let r = db::insert(db &args[2], &args[3])
.unwrap();
println!("{} rows affected", r);
},
"del" => {
if args.len() < 3 {
panic!("Usage: phonebook del ID...");
}
let ids: Vec<i32> = args[2..].iter()
.map(|s| s.parse().unwrap())
.collect();

db::remove(db &ids)
.unwrap();
},
"edit" => {
if args.len() != 5 {
panic!("Usage: phonebook edit ID NAME PHONE");
}
let id = args[2].parse().unwrap();
db::update(db, id &args[3], &args[4])
.unwrap();
},
"show" => {
if args.len() > 3 {
panic!("Usage: phonebook show [SUBSTRING]");
}
let s;
if args.len() == 3 {
s = args.get(2);
} else {
s = None;
}
let r = db::show(db, s.as_ref().map(|s| &s[..])).unwrap();
db::format(&r);
},
"help" => {
println!("{}", HELP);
},
command @ _ => panic!(
format!("Invalid command: {}", command))
}
}
None => panic!("No command supplied"),
}


Подивимося на перший рядок:

let args: Vec<_> = std::env::args().collect();

std::env::args() просто повертає ітератор з аргументів командного рядка. Чому це ітератор, а не який-небудь статичний масив? Бо нам можуть і не знадобитися всі аргументи, а потенційно їх може бути багато. Тому використовується ітератор — він «ледачий». Це в дусі Rust — ви не платите за те, що вам не потрібно.

Так от, тут у нас завідомо мало аргументів і нам буде простіше мати все-таки нормальний вектор, з якого аргументи можна брати за індексами. Ми робимо .collect(), щоб обійти всі елементи і зібрати їх в певну колекцію.

Яку саме колекцію? Ось тут є тонкий момент. Насправді, .collect() викликає метод from_iter() тієї колекції, в яку кладуться елементи. Виходить, нам треба знати її тип. Саме тому ми не можемо опустити тип args і написати так:

let args = std::env::args().collect();

Ось що на це скаже компілятор:

main.rs:61:9: 61:13 error: unable to infer enough type information about `_`; type annotations or generic parameter binding required [E0282]
main.rs:61 let args = std::env::args().collect();
^~~~
main.rs:61:9: 61:13 help: run `rustc --explain E0282` to see a detailed explanation

Однак зауважте, що висновок типів робить своє діло: нам достатньо вказати в якості типу Vec<_>: який тип лежить в векторі, компілятор і так знає. Треба тільки уточнити, яку колекцію ми хочемо.

Ну і навіщо всі ці складності? Потім, що ми можемо, наприклад, зібрати аргументи в зв'язний список (або якусь іншу колекцію), якщо захочемо:

let args: std::collections::LinkedList<_> = std::env::args().collect();

Список колекцій, що реалізують from_iter, є на сторінці документації типажу.

Далі ми бачимо

match args.get(1) {

.get() повертає Ok(element), якщо елемент вектора існує, і Ні в іншому випадку. Ми користуємося цим, щоб виявити ситуацію, коли користувач не вказав команду:

}
None => panic!("No command supplied"),
}

Якщо команда не збігається ні з однією з зумовлених, ми виводимо помилку:

command @ _ => panic!(
format!("Invalid command: {}", command))

Ми хочемо потрапити в цю гілку при будь-якому значенні text — тому значення даної гілки використовується _, «будь-яке значення». Проте, ми хочемо вивести цю саму неправильну команду, тому ми пов'язуємо вираз match з ім'ям command за допомогою конструкції command @ _. Детальніше про це синтаксисі дивіться здесь і здесь.

Далі розбір виглядає так:

Some(text) => {
match text.as_ref() {
"add" => {
// handle add
},

Якщо у нас є команда, ми потрапимо в гілку Some(text). Далі ми користуємося match ще раз, щоб зіставити назва команди — як бачите, match досить універсальний.

Команди розбираються досить однотипно, тому давайте розглянемо саму цікаву: delete. Вона приймає список ідентифікаторів записів, які повинні бути видалені.

"del" => {
if args.len() < 3 {
panic!("Usage: phonebook del ID...");
}
let ids: Vec<i32> = args[2..].iter()
.map(|s| s.parse().unwrap())
.collect();

db::remove(db &ids)
.unwrap();
},

Спочатку нам потрібні ідентифікаторів: ми отримуємо їх з аргументів командного рядка наступним чином:

let ids: Vec<i32> = args[2..].iter()
.map(|s| s.parse().unwrap())
.collect();

З let foo: Vec<_> =… .collect() ми вже знайомі. Залишилося розібратися, що відбувається всередині цієї строчки.

args[2..] отримує зріз вектора — починаючи з третього елемента до кінця вектора. Схоже на зрізи в Python.

.iter() отримує ітератор з цього зрізу, до якого ми застосовуємо анонімну функцію за допомогою .map():

.map(|s| s.parse().unwrap())

Наша анонімна функція приймає єдиний аргумент — s — і розбирає його як ціле число. Звідки вона знає, що це повинно бути ціле? Звідси:

let ids: Vec<i32> = 

(Хе-хе, насправді, навіть не звідси, а з сигнатури функції db::remove — вона приймає зріз &[i32]. Висновок типів використовує цю інформацію, щоб зрозуміти, що FromStr::from_str треба викликати у i32. Тому ми могли бути і тут використовувати Vec<_> — з метою документування коду, ми вказали тип явно. Про саму db::remove — нижче).

Взагалі, застосування адаптерів ітераторів зразок .map() — це поширений шаблон у коді на Rust. Він дозволяє отримати контрольовану ленивость виконання там, де вона найчастіше потрібна — при потоковому читанні якихось даних.

Відмінно, ми впоралися зі всієї підготовчої роботою. Залишилося оновити саму базу. insert виглядає зовсім нудно. Давайте подивимося на remove.

До речі, а чому вона записана як db::remove? Тому, що вона знаходиться в окремому модулі. На рівні файлів, це означає, що вона в окремому исходнике: src/db.rs. Цей модуль включається у наш головний файл? Ось так:

mod db;

Просто! Дана інструкція еквівалента вставці всього вихідного коду модуля в те місце, де вона написана. (Але насправді цього не відбувається, це ж не сишный препроцесор. Тут компілюється весь контейнер відразу, тому компілятор може вважати модулі в пам'ять і встановлювати зв'язки на рівні проміжного представлення, а не тупо копіювати вихідний код у вигляді тексту. Варто відзначити, що компілятор буде шукати модуль у файлах src/db.rs і src/db/mod.rs — це дозволяє акуратно організувати ієрархію модулів.

Тепер код нашої функції:

pub fn remove(db: Connection, ids: &[i32]) -> ::postgres::Result<u64> {
let stmt = db.prepare("DELETE FROM phonebook WHERE id=$1").unwrap();
for id in ids {
try!(stmt.execute(&[id]));
}
Ok(0)
}

Так-так, тут ми майже все знаємо. По порядку.

pub означає, що функція доступна зовні модуля. В іншому випадку, ми б не змогли викликати її з main, т. к. за замовчуванням усі функції всередині модулів приховані:

main.rs:81:21: 81:31 error: function `remove` is private
main.rs:81 db::remove(db &ids)
^~~~~~~~~~

Тип значення, що повертається, виглядає дивно. ::postgres::Result?

Два двокрапки означають, що модуль postgres потрібно шукати від кореня нашого контейнера, і не від поточного модуля. Цей модуль автоматично оголошується в main.rs, коли ми робимо extern crate postgres. Але він не стає видно в db.rs автоматично! Тому ми ліземо в корінь простору імен за допомогою ::postgres. Ще ми могли б повторно запросити зв'язування контейнера postgres в db.rs, але це не вважається хорошою практикою — краще, якщо всі запити на зв'язування знаходяться в одному місці, а інші модулі користуються тим, що доступно в головному.

Добре, трохи розібралися з модулями. Детальніше дивіться тут.

Далі ми бачимо небачений досі макрос
try!
.

Він, як підказує його назва, намагається виконати якусь операцію. Якщо вона завершується успіхом, значенням try!() значення, вкладене в Ok(_). Якщо ні, він виконує щось схоже на return Err(error). Це альтернатива нашим постійним .unwrap() — тепер програма не завершиться панікою у разі помилки, а поверне помилку наверх для обробки викликає функцією.

Цим макросом можна користуватися у функціях, які самі повертають Result — в іншому випадку макрос не зможе повернути Err, т. к. тип повертаного значення і тип значення return не співпадуть.

З видаленням все. Далі я вибірково пройдуся по інших операціях, описуючи те, що ми поки не знаємо.

Ось, наприклад, як відбувається робота з транзакціями:

{
let tx: ::postgres::Transaction = db.transaction().unwrap();
tx.execute(
"UPDATE phonebook SET name = $1, phone = $2 WHERE id = $3",
&[&name, &phone, &id]).unwrap();
tx.set_commit();
}

Як бачите, це типове застосування RAII. Ми просто не передаємо нікуди tx, і воно знищується по виходу з блоку. Реалізація його деструктора зберігає або відкочує транзакцію в залежності від прапора успіху. Якщо б ми не зробили tx.set_commit(), деструктор tx відкотив б.

А ось як можна відформатувати рядок без печатки на екран:

Some(s) => format!("WHERE name LIKE '%{}%'", s),

Коли ми створюємо вектор, можна відразу вказати, під скільки елементів він повинен виділити пам'ять:

let mut results = Vec::with_capacity(size);

І наостанок, ще один приклад коду у функціональному стилі:

let max = rs.iter().fold(
0,
|acc, ref item|
if item.name.len() > acc { item.name.len() } else { acc });

Цей код можна було б записати простіше, якби ми порівнювали типи, для яких реалізований типаж Ord:

let max = rs.iter().max();

Або, ми можемо реалізувати цей типаж Record. Він вимагає реалізації PartialOrd і Eq, а Eq, в свою чергу — PartialEq. Тому насправді доведеться реалізувати 4 типажу. На щастя, реалізація тривіальна.

Реалізація типажів
use std::cmp::Ordering;

impl Ord for Record {
fn cmp(&self, other: &Self) -> Ordering {
self.name.len().cmp(&other.name.len())
}
}

impl PartialOrd for Record {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.name.len().cmp(&other.name.len()))
}
}

impl Eq for Record { }

impl PartialEq for Record {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.name == other.name
&& self.phone == other.phone
}
}

pub fn format(rs: &[Record]) {
let max = rs.iter().max().unwrap();
for v in rs {
println!("{:3} {:.*} {}", v.id max.name.len(), v.name, v.phone);
}
}


Варто відзначити, що осмисленість такої реалізації під питанням — все ж навряд чи варто порівнювати запису БД по довжині одного з полів.

До речі, типаж Eq — це один з прикладів типажів-маркерів: він не вимагає реалізації жодних методів, а просто говорить компілятору, що якийсь тип володіє певним властивістю. Інші приклади таких типажів — це Send і Sync, про які ми ще поговоримо.

На сьогодні всі — пост і так виявився найдовшим з серії.

Тепер наше додаток реально працює, але у нього поки немає REST-інтерфейсу. Веб-частиною ми займемося в наступний раз.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.