Roteador

O que é Roteamento

Router define quais middlewares e Handlers processarão uma requisição HTTP. Esta é a funcionalidade mais básica e central do Salvo.

Internamente, o Router consiste em uma série de filtros (Filters). Quando uma requisição chega, o roteador testa sequencialmente, de cima para baixo, se ele mesmo e seus descendentes podem corresponder à requisição. Se houver correspondência, os middlewares ao longo de toda a cadeia formada pelo roteador e seus descendentes são executados em ordem. Se durante o processamento o status da Response for definido como erro (4XX, 5XX) ou redirecionamento (3XX), os middlewares e Handlers subsequentes serão ignorados. Você também pode chamar manualmente ctrl.skip_rest() para pular os middlewares e Handlers seguintes.

Durante o processo de correspondência, existe uma informação de caminho URL que pode ser considerada como um objeto que precisa ser completamente consumido pelos Filters. Se todos os Filters em um Router corresponderem com sucesso e essa informação de caminho URL tiver sido completamente consumida, considera-se que houve "correspondência bem-sucedida".

Por exemplo:

Router::with_path("articles").get(list_articles).post(create_article);

É equivalente a:

Router::new()
    // O PathFilter pode filtrar o caminho da requisição, só corresponderá se o caminho contiver o segmento 'articles',
    // caso contrário, falhará. Por exemplo: /articles/123 corresponde, mas /articles_list/123
    // embora contenha 'articles', não corresponde devido ao '_list' seguinte.
    .filter(PathFilter::new("articles"))

    // Se o root corresponder e o método da requisição for GET, o roteador interno corresponderá
    // e a requisição será processada por list_articles.
    .push(Router::new().filter(filters::get()).handle(list_articles))

    // Se o root corresponder e o método da requisição for POST, o roteador interno corresponderá
    // e a requisição será processada por create_article.
    .push(Router::new().filter(filters::post()).handle(create_article));

Se acessar GET /articles/, considera-se correspondência bem-sucedida e executa list_articles. Mas se acessar GET /articles/123, a correspondência falhará e retornará erro 404, porque Router::with_path("articles") só consumiu /articles do caminho URL, restando /123 não consumido, portanto a correspondência falha. Para que corresponda, o roteador pode ser alterado para:

Router::with_path("articles/{**}").get(list_articles).post(create_article);

Aqui, {**} corresponderá a qualquer caminho adicional, permitindo que GET /articles/123 execute list_articles.

Definição Plana

Podemos definir rotas em estilo plano:

Router::with_path("writers").get(list_writers).post(create_writer);
Router::with_path("writers/{id}").get(show_writer).patch(edit_writer).delete(delete_writer);
Router::with_path("writers/{id}/articles").get(list_writer_articles);

Definição em Árvore

Também podemos definir rotas em forma de árvore, o que é recomendado:

Router::with_path("writers")
    .get(list_writers)
    .post(create_writer)
    .push(
        Router::with_path("{id}")
            .get(show_writer)
            .patch(edit_writer)
            .delete(delete_writer)
            .push(Router::with_path("articles").get(list_writer_articles)),
    );

Esta forma de definição torna a organização do Router clara e simples para projetos complexos.

Muitos métodos no Router retornam a si mesmos (Self), permitindo encadeamento. Às vezes, você precisa decidir como rotear com base em certas condições. O sistema de roteamento também fornece a função then, fácil de usar:

Router::new()
    .push(
        Router::with_path("articles")
            .get(list_articles)
            .push(Router::with_path("{id}").get(show_article))
            .then(|router|{
                if admin_mode() {
                    router.post(create_article).push(
                        Router::with_path("{id}").patch(update_article).delete(delete_writer)
                    )
                } else {
                    router
                }
            }),
    );

Este exemplo mostra que apenas quando o servidor está em admin_mode, as rotas para criar, editar e excluir artigos são adicionadas.

Obtendo Parâmetros das Rotas

No código acima, {id} define um parâmetro. Podemos obter seu valor através da instância Request:

#[handler]
async fn show_writer(req: &mut Request) {
    let id = req.param::<i64>("id").unwrap();
}

{id} corresponde a um segmento do caminho. Normalmente, o id de um artigo é apenas um número, então podemos usar expressões regulares para restringir a correspondência, como r"{id|\d+}".

Para tipos numéricos, há uma maneira mais simples usando <id:num>:

  • {id:num}: corresponde a qualquer número de dígitos;
  • {id:num[10]}: corresponde exatamente a 10 dígitos;
  • {id:num(..10)}: corresponde de 1 a 9 dígitos;
  • {id:num(3..10)}: corresponde de 3 a 9 dígitos;
  • {id:num(..=10)}: corresponde de 1 a 10 dígitos;
  • {id:num(3..=10)}: corresponde de 3 a 10 dígitos;
  • {id:num(10..)}: corresponde a pelo menos 10 dígitos.

Também é possível usar {**}, {*+} ou {*?} para corresponder a todos os segmentos restantes do caminho. Para melhor legibilidade, pode-se adicionar nomes descritivos, como {**file_path}.

  • {**}: corresponde a strings vazias, como /files, /files/abc.txt, /files/dir/abc.txt;
  • {*+}: exige que o segmento não seja vazio, como /files/abc.txt, mas não /files;
  • {*?}: permite strings vazias, mas apenas um segmento, como /files, /files/abc.txt, mas não /files/dir/abc.txt.

É possível combinar múltiplas expressões para corresponder ao mesmo segmento, como /articles/article_{id:num}/, /images/{name}.{ext}.

Adicionando Middlewares

Middlewares podem ser adicionados através da função hoop:

Router::new()
    .hoop(check_authed)
    .path("writers")
    .get(list_writers)
    .post(create_writer)
    .push(
        Router::with_path("{id}")
            .get(show_writer)
            .patch(edit_writer)
            .delete(delete_writer)
            .push(Router::with_path("articles").get(list_writer_articles)),
    );

Neste exemplo, o roteador raiz usa check_authed para verificar se o usuário está logado. Todos os roteadores descendentes são afetados por este middleware.

Se os usuários podem apenas visualizar informações de writer e artigos sem login, podemos definir as rotas assim:

Router::new()
    .push(
        Router::new()
            .hoop(check_authed)
            .path("writers")
            .post(create_writer)
            .push(Router::with_path("{id}").patch(edit_writer).delete(delete_writer)),
    )
    .push(
        Router::with_path("writers").get(list_writers).push(
            Router::with_path("{id}")
                .get(show_writer)
                .push(Router::with_path("articles").get(list_writer_articles)),
        ),
    );

Mesmo com duas rotas definindo o mesmo caminho path("articles"), elas ainda podem ser adicionadas ao mesmo roteador pai.

Filtros

O Router usa filtros internamente para determinar a correspondência. Filtros suportam operações lógicas básicas como or e and. Um roteador pode conter múltiplos filtros, e só corresponde se todos os filtros corresponderem.

A estrutura de caminhos de um site é hierárquica, mas não necessariamente igual à estrutura do roteador. Um caminho pode corresponder a múltiplos nós do roteador. Por exemplo, alguns conteúdos em articles/ podem requerer login, enquanto outros não. Podemos organizar os subcaminhos que requerem login em um roteador com middleware de autenticação, e os que não requerem em outro:

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

O roteador usa filtros para direcionar requisições aos middlewares e Handlers apropriados.

path e method são dois filtros comuns. path corresponde ao caminho; method corresponde ao método HTTP, como GET, POST, PATCH, etc.

Podemos conectar filtros com and e or:

Router::with_filter(filters::path("hello").and(filters::get()));

Filtros de Caminho

Filtros baseados em caminho são os mais usados. Podem incluir parâmetros:

Router::with_path("articles/{id}").get(show_article);
Router::with_path("files/{**rest_path}").get(serve_file)

No Handler, os parâmetros podem ser obtidos através do objeto Request:

#[handler]
pub async fn show_article(req: &mut Request) {
    let article_id = req.param::<i64>("id");
}

#[handler]
pub async fn serve_file(req: &mut Request) {
    let rest_path = req.param::<i64>("rest_path");
}

Filtros de Método

Filtram requisições pelo método HTTP:

Router::new().get(show_article).patch(update_article).delete(delete_article);

Aqui, get, patch, delete são filtros de método, equivalentes a:

use salvo::routing::filter;

let mut root_router = Router::new();
let show_router = Router::with_filter(filters::get()).handle(show_article);
let update_router = Router::with_filter(filters::patch()).handle(update_article);
let delete_router = Router::with_filter(filters::get()).handle(delete_article);
Router::new().push(show_router).push(update_router).push(delete_router);

Wisp Personalizado

Para expressões de correspondência frequentes, podemos usar PathFilter::register_wisp_regex ou PathFilter::register_wisp_builder para definir um nome curto. Por exemplo, o formato GUID aparece frequentemente em caminhos. Normalmente, seria necessário escrever:

Router::with_path("/articles/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");
Router::with_path("/users/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");

Escrever essa expressão regular complexa repetidamente é propenso a erros. Em vez disso, podemos fazer:

use salvo::routing::filter::PathFilter;

#[tokio::main]
async fn main() {
    let guid = regex::Regex::new("[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}").unwrap();
    PathFilter::register_wisp_regex("guid", guid);
    Router::new()
        .push(Router::with_path("/articles/{id:guid}").get(show_article))
        .push(Router::with_path("/users/{id:guid}").get(show_user));
}

Após registrar uma vez, podemos usar {id:guid} para corresponder a GUIDs, simplificando o código.

Como Entender o Router se Você Aprendeu Frameworks Web Baseados em Controller?

As principais diferenças entre frameworks baseados em roteamento (como Salvo) e os baseados em MVC ou Controller são:

  • Flexibilidade: O roteamento permite definir o fluxo de processamento de requisições com mais precisão. Por exemplo, no Salvo você pode definir diretamente:

    Router::with_path("articles").get(list_articles).post(create_article);

    Enquanto em um Controller, você precisaria definir uma classe e métodos:

    @Controller
    public class ArticleController {
        @GetMapping("/articles")
        public List<Article> listArticles() { /* ... */ }
        
        @PostMapping("/articles")
        public Article createArticle(@RequestBody Article article) { /* ... */ }
    }
  • Integração de Middleware: Frameworks de roteamento oferecem integração mais simples de middlewares, aplicáveis a rotas específicas:

    Router::new()
        .push(
            Router::with_path("admin/articles")
                .hoop(admin_auth_middleware)  // Middleware apenas para rotas de admin
                .get(list_all_articles)
                .post(create_article),
        )
        .push(
            Router::with_path("articles")  // Rotas públicas sem autenticação
                .get(list_public_articles),
        );
  • Organização do Código: O roteamento organiza o código por funcionalidade ou endpoints de API, em vez de seguir a divisão MVC:

    // user_routes.rs - Rotas e lógica relacionada a usuários
    pub fn user_routes() -> Router {
        Router::with_path("users")
            .get(list_users)
            .post(create_user)
            .push(Router::with_path("{id}").get(get_user).delete(delete_user))
    }
    
    // article_routes.rs - Rotas e lógica relacionada a artigos
    pub fn article_routes() -> Router {
        Router::with_path("articles")
            .get(list_articles)
            .post(create_article)
    }
    
    // Combinando rotas na aplicação principal
    let router = Router::new()
        .push(user_routes())
        .push(article_routes());
  • Leveza: O roteamento geralmente é mais leve, com menos conceitos e restrições impostos pelo framework. Você pode incluir apenas o necessário, sem seguir estruturas rígidas.

O roteamento torna o desenvolvimento de APIs mais intuitivo, especialmente para microsserviços e APIs RESTful modernas. Em frameworks como Salvo, o roteamento é o conceito central, refletindo diretamente a estrutura e comportamento da API, tornando o código mais fácil de entender e manter. Em contraste, designs baseados em Controller geralmente requerem mais configuração e convenções para alcançar a mesma funcionalidade.

Visão Geral dos Métodos da Estrutura Router

CategoriaMétodoDescrição
Criação/Acessonew()Cria novo roteador
routers()/routers_mut()Obtém referência/referência mutável aos roteadores filhos
hoops()/hoops_mut()Obtém referência/referência mutável aos middlewares
filters()/filters_mut()Obtém referência/referência mutável aos filtros
Organização de Rotasunshift()Insere roteador filho no início
insert()Insere roteador filho em posição específica
push()Adiciona roteador filho
append()Adiciona múltiplos roteadores filhos
then()Configuração personalizada da cadeia de roteadores
Middlewarewith_hoop()/hoop()Cria/adiciona middleware
with_hoop_when()/hoop_when()Cria/adiciona middleware condicional
Filtros de Caminhowith_path()/path()Cria/adiciona filtro de caminho
with_filter()/filter()Cria/adiciona filtro
with_filter_fn()/filter_fn()Cria/adiciona filtro de função
Filtros de Redescheme()Adiciona filtro de protocolo
host()/with_host()Adiciona/cria filtro de host
port()/with_port()Adiciona/cria filtro de porta
Métodos HTTPget()/post()/put()Cria rota para método