Освоение мастерства

Почему был создан этот фреймворк

В то время, будучи новичком, я понял, что мне сложно освоить существующие фреймворки, такие как actix-web и Rocket. Когда я захотел переписать свой веб-сервис с Go на Rust, оказалось, что каждый фреймворк выглядит сложнее, чем аналоги в Go. Учитывая, что Rust сам по себе имеет крутую кривую обучения, зачем делать веб-фреймворки ещё сложнее?

Когда Tokio представил фреймворк Axum, я обрадовался, думая, что больше не придётся поддерживать свой собственный веб-фреймворк. Однако на практике Axum оказался обманчиво простым: избыточная работа с типами и дженериками требовала глубокого понимания Rust и написания множества сложных шаблонных конструкций даже для простого middleware.

Поэтому я решил продолжить развивать свой особенный (удобный, функциональный и подходящий для новичков) веб-фреймворк.

Подходит ли вам Salvo (Сайфен)?

Несмотря на простоту, Salvo обладает полным набором мощных функций и может считаться одним из сильнейших в экосистеме Rust. При этом его изучение и использование интуитивно понятны — никакой болезненной адаптации.

  • Идеален для начинающих изучать Rust: CRUD-операции должны быть простыми и привычными. С Salvo такие задачи решаются так же легко, как и в других языках (например, Express, Koa, Gin, Flask), а в чём-то даже проще и элегантнее.

  • Подходит для production-сред, где требуется надёжный и быстрый сервер. Хотя Salvo ещё не достиг версии 1.0, его основные компоненты прошли многолетнюю итерационную доработку, стабильны, а проблемы оперативно исправляются.

  • Создан для вас, даже если ваши волосы уже не так густы, но продолжают редеть с каждым днём.

Как достигается простота

Большинство низкоуровневых задач уже решены в Hyper, поэтому для стандартных потребностей разумно строить фреймворк на его основе. Salvo — не исключение. Его ядро — это мощная и гибкая система маршрутизации, дополненная часто используемыми функциями, такими как Acme, OpenAPI, JWT-аутентификация и другие.

В Salvo унифицированы обработчики (Handler) и промежуточное ПО (Middleware). Middleware — это тоже Handler, который добавляется в Router через метод hoop. По сути, и Middleware, и Handler обрабатывают запросы (Request) и могут записывать данные в ответ (Response). Handler принимает три параметра: Request, Depot (для временных данных обработки) и Response.

Для удобства можно опускать неиспользуемые параметры или менять их порядок:

use salvo::prelude::*;

#[handler]
async fn hello_world(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    res.render("Hello world");
}
#[handler]
async fn hello_world(res: &mut Response) {
    res.render("Hello world");
}

API системы маршрутизации также прост, но функционален. В большинстве случаев достаточно работать только с типом Router.
Кроме того, если структура реализует определённые трейты, Salvo автоматически генерирует документацию OpenAPI, извлекает параметры, обрабатывает ошибки и возвращает понятные сообщения. Это делает написание обработчиков таким же простым, как и обычных функций. В следующих уроках мы подробно разберём эти возможности. Вот пример:

#[endpoint(tags("Логи сообщений"))]
pub async fn create_message_log_handler(
    input: JsonBody<CreateOrUpdateMessageLog>,
    depot: &mut Depot,
) -> APPResult<Json<MessageLog>> {
    let db = utils::get_db(depot)?;
    let log = create_message_log(&input, db).await?;
    Ok(Json(log))
}

Здесь JsonBody<CreateOrUpdateMessageLog> автоматически парсит JSON из тела запроса (поддерживаются несколько источников и вложенные типы), а макрос #[endpoint] генерирует документацию OpenAPI, упрощая извлечение параметров и обработку ошибок.

Система маршрутизации

На мой взгляд, система маршрутизации Salvo отличается от других. Router можно организовать линейно или в виде дерева. Различают дерево бизнес-логики и дерево URL-путей. Первое отражает структуру бизнес-процессов и не всегда совпадает со вторым.

Обычно маршруты пишут так:

Router::new().path("articles").get(list_articles).post(create_article);
Router::new()
    .path("articles/{id}")
    .get(show_article)
    .patch(edit_article)
    .delete(delete_article);

Часто просмотр списка статей и отдельной статьи не требует авторизации, а создание, редактирование и удаление — да. Вложенная маршрутизация Salvo идеально подходит для таких сценариев. Можно сгруппировать публичные маршруты:

Router::new()
    .path("articles")
    .get(list_articles)
    .push(Router::new().path("{id}").get(show_article));

А защищённые маршруты объединить с middleware для проверки аутентификации:

Router::new()
    .path("articles")
    .hoop(auth_check)
    .post(list_articles)
    .push(Router::new().path("{id}").patch(edit_article).delete(delete_article));

Хотя оба Router имеют одинаковый path("articles"), их можно добавить к одному родительскому маршруту. Итоговая структура будет выглядеть так:

Router::new()
    .push(
        Router::new()
            .path("articles")
            .get(list_articles)
            .push(Router::new().path("{id}").get(show_article)),
    )
    .push(
        Router::new()
            .path("articles")
            .hoop(auth_check)
            .post(list_articles)
            .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)),
    );

{id} соответствует сегменту пути. Если ID статьи — это число, можно ограничить шаблон регулярным выражением: r"{id:/\d+/}".