Лет так много назад, если верить слухам того времени, питон был не зыбко популярен, flask был где-то в узких кругах, а за django продавцам нужно было замолвить слово. Все, конечно, понимали - за django будущее, и не только потому, что java всем поднадоела, но потому что было удобно и для бизнеса, и для кодинга. Что кривить, читая книгу zero2prod невольно вспоминаешь удовольствие от изучения django, удивления - "а что, так можно было", и пожалуй, глубину проработки деталей, которые обычный разработчик осилил бы самостоятельно, но обычно было лень.

Rust при всей своей скромности по скорости весьма удобен для day to day разработки, и книга (которая в тайтле) раскрывает детали этой парадоксальной особенности.

Хотелось бы имееть какую-никакую логику в построении приложения, но, к сожaлению или к счастью, в наше время при наличии широкого набора инструментов любой компонент будущей структуры выглядит наиболее подходящим на место первого к выполнению. Сходимость проекта, в свою очередь, будет определяться всем попало, и не последнюю роль в ней будет играть фантазия авторов и усидчивость коллег.

Начнем, пожалуй, с файлов конфигурации. В свое время Степанов говорил, что единственное применение inheritance есть в наследовании полей. У нас есть файлик базовой конфигурации base.yaml и много-много файлов конфигураций для различных сред. В коде это выглядит примерно так:

Config::builder()
    .add_source(File::new("configuration/base.yaml", FileFormat::Yaml))
    .add_source(File::new(&env_config, FileFormat::Yaml))
    .build()

На всякий случай, yaml формат - это такой удобный KV с отступами.

Миграции для базы данных, почему бы и нет. Ну то есть у нас еще нет базы данных, но есть знание, что персистентный стейт нужен, и каким-то образом нужно будет создать таблички или менять колонки. Для миграций важна очередность выполнения (к именам файлов иногда добавляют timestamp) и инструмент (command line tool). Автор zero2prod юзает миграции в баш-скрипте. Как-то так:

...
export DATABASE_URL=...
sqlx database create
sqlx migrate run

Лирическое отступление: забавно, что как киллер-фича наряду с async/await рассматривалась и rust cli. То есть как бы люди думали в 2018 куда приложить ресурсы и рассматривалось четыре направления: Embedded systems, WebAssembly, Command-line interfaces, Network services. И тут автор книги о расте на голубом глазу юзает баш.

Докер. Наверно, не очень распространенное, но очень удобное решение - two stage build. На первом шаге билдим код, на втором копируем и запускаем бинарник. Автор zero2prod придает большое значение размеру образа - там прям увлекательное чтиво. Далее автор деплоит приложение на облако, попутно описывая файл конфигурации для деплоя - где живет база, где application code, нужен ли load balancer и т.д. В целом это отдельная история, но как бы неплохо бы знать, как ваш код будет исполняться.

Пару слов об sqlx, который согласно докам "..is not an ORM, but compile-time checked queries". То есть ребята во время компиляции коннектятся к базе и проверяют структуру ваших sql запросов. Кажется, в какое-то время всех это задолбало, и ребята выпустили command line утилиту для генерации query oффлайн. Далее в книге идет не очень популярная, но весьма интересная аргументация о движке запросов. Если мне не изменяет память, суть ORM в достаточно легкой замене базы данных без изменения кода приложения (см. например django). Так вот, автор zero2prod предлагает юзать чистый sql, аргументируя это тем, что язык приложения может смениться, а sql запросы останутся эскуальными. Напомню, книга о языке программирования rust.

Лирическое отступление: популярный вопрос на интервью - какие либы вы юзали. Автор zero2prod сравнивает sqlx c двумя orm, и кажется, самый притязательный читатель найдет что-нибудь интересное для себя.

Итак, мы хотим уже написать код, но как? Автор книги предлагает через тесты, а сам проект разбить на клиентский код и библиотеку. Клиентский код - эт так, чтоб потыкаться курлом, ну или через браузер, - а библиотеку нужно покрыть тестами чуть более чем полностью - red green development, все дела. Тут, как говорится, есть нюанс. Пофиксить свои тесты это всегда ок. Пофиксить чужие тесты, если test case не больше десяти строк, тоже ок, а вот пофиксить абстракцию в тестах (потому что много тестов и нужны абстракции) - это, скорее, skip test, чем фикс. Спорить о тестах также бесполезно, как и о языках программирования (хотя все и так знают, что rust лучший :)

Все вебовские приложения в конце концов юзают "экстримли фаст" web framework, не исключая и zero2prod. Автор книги юзает actix-web с весьма интересной фичей powerful request routing. Тут проще кодом:

App::new()
    .route("/health_check", web::get().to(routes::health_check))
    .route("/subscriptions", web::post().to(routes::subscribe))    

Pучка routes::subscribe может иметь почти любую сигнатуру. По словам автора actix-web, это происходит благодаря системе типов раста, а не за счет магии макросов. Например,

pub async fn health_check() ...
pub async fn subscribe(form: web::Form<Email>, pool: web::Data<SqlitePool>) ...

// in the same time we can swap args if we want in subscribe(...)
pub async fn subscribe(pool: web::Data<SqlitePool>, form: web::Form<Email>) ...

Кажется, в динамических языках это сделать не очень тривиально.

Интересная структура кода у автора zero2prod - в одном файле и имплементация route, и запрос к базе данных. Кажется, все свалено в кучу, c другой стороны код получается очень компактным, поэтому и не хочется разделять на файлы, например:

pub async fn subscribe(...) -> Result<HttpResponse, SubscribeError> {
    let new_subscriber = form.0.try_into()?;
    let mut transaction = pool.begin().await?;
    insert_subscriber(&new_subscriber, &mut transaction).await?;
    transaction.commit().await?;
    Ok(HttpResponse::Ok().finish())
}

Вот эти знаки вопроса в коде - это про удобство (или эргономику) раста. Каждый такой знак на самом деле раскрывается в что-то типа такого:

if insert_subscriber(&new_subscriber, &mut transaction).await.is_err() {
    return HttpResponse::InternalServerError().finish();
}

Внимательный читатель тут же поинтересуется, каким образом SubscribeError связан с http ответом (ResponseError) - тут работает нативная фича раста - Trait (они же протоколы), как-то так:

impl From<sqlx::Error> for SubscribeError {
    fn from(e: sqlx::Error) -> Self {
        Self::DatabaseError(e)
    }
}

impl From<String> for SubscribeError {
    fn from(e: String) -> Self {
        Self::ValidationError(e)
    }
}

impl ResponseError for SubscribeError {
    fn status_code(&self) -> StatusCode {
        match self {
            SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
            SubscribeError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

Немного многословно, можно сделать короче с помощью библиотек обработки ошибок: anyhow и thiserror. Тут идея, как мне кажется, в том, что ошибки лежат где-то в одном месте и не мешают чтению кода, основной логики программы (только ok path - класс!).

Валидации инпута. Автор книги предлагает считать String, они же подписчики "грязными" данными, а структурку SubscriberName(String) чистыми данными, соответственно переход между состояниями строго в одном месте:

impl SubscriberName {
    pub fn parse(s: String) -> Result<SubscriberName, String> { ... }
}

В идеальном мире, наверно, сработало бы, а так кажется маловероятным, что любые String инпута будут обернуты в структурки. Опять же есть трэйт AsRef - возможно и прокатит:

impl AsRef<str> for SubscriberName {
    fn as_ref(&self) -> &str {
        &self.0
    }
}
// e.g. we can use our SubscriberName here 
fn somewhere_inside_codebase(x: impl AsRef<str>) { ... }

Пару слов про telemetry. Наверно, не стоит все бросать и заменять логи на телеметрию уже сегодня. Идея вроде путная, и вроде уже много сервисов по сбору эвентов от приложений. Но хз. Мне зашла такая обертка (см. ниже) которая вроде как форсит вас юзать короткие функции и одновременно добавляет эвент на входе функции и эвент на выходе:

#[tracing::instrument(
    name = "Adding a new subscriber",
    skip(form, pool),
    fields(
        subscriber_email = %form.email,
        subscriber_name= %form.name
    )
)]
pub async fn subscribe(...) {}

В книге еще много чего интересного, поэтому приятного чтения.

Комментарии (5)


  1. InBioReactor
    09.07.2024 16:11

    Высокий технический уровень материала


  1. Ydav359
    09.07.2024 16:11
    +3

    Смотря какой fabric, смотря сколько details


  1. selcon
    09.07.2024 16:11

    Какой-то поток сознания?


    1. InBioReactor
      09.07.2024 16:11

      Квадрат Малевича не просто квадрат, он чёрный и символизирует глубину изложения. Здесь что-то вроде...


  1. Dominux
    09.07.2024 16:11

    Когда решил сэкономить на написании статей настолько, что вместо подписки ChatGPT-4o заюзал какую-то устаревшую слабую LLM для генерации статьи целиком