Anteriormente neste blog, abordamos como criar um Serviço da web CRUD com Rust usando warp e como construir um aplicativo da web de front-end com Rust usando Yew .

Neste tutorial, vamos colocar tudo junto e construir um aplicativo da web full stack simples, com um back-end REST apoiado por banco de dados e um aplicativo de página única baseado em Wasm no front-end, que chama este back-end.

Para completar, criaremos um Módulo Rust, que será usado pelo front-end e pelo back-end, para demonstrar como compartilhar código em tal configuração.

Vamos construir um aplicativo de proprietário de animal de estimação muito simples que permite ao usuário adicionar proprietários e seus animais de estimação. Nosso aplicativo apresentará uma visão detalhada para os proprietários e sua lista de animais de estimação, permitindo que eles excluam e adicionem animais de estimação conforme necessário.

Aqui está o que abordaremos:

Configurando um Rust full-stack app Funcionalidade comum Construindo a implementação de front-end do back-end REST Testando nosso aplicativo Rust full-stack

Você não precisa ter lido as postagens mencionadas para acompanhar, mas como esta postagem inclui os dois conceitos, não entraremos no mesmo nível de profundidade em relação ao básico. Se você quiser se aprofundar, sinta-se à vontade para examiná-los.

Sem mais delongas, vamos começar!

Configurando um aplicativo Rust full-stack

Para acompanhar, tudo que você precisa é de uma instalação Rust razoavelmente recente . Docker ou alguma outra forma de executando um banco de dados Postgres , também seria útil.

Nesse caso, como vamos escrever o back-end e o front-end em Rust e vamos compartilhar algum código entre eles, criaremos um projeto de espaço de trabalho com vários membros usando Cargo .

Primeiro, crie um novo projeto Rust:

cargo new rust-fullstack-example cd rust-fullstack-example

Em seguida, exclua a pasta src e edite o arquivo Cargo.toml da seguinte maneira:

[workspace] members=[“backend”,”frontend”, #Internal”common”]

Agora podemos criar nossos três projetos separados do Rust:

cargo new–lib common cargo novo back-end cargo novo–lib frontend

Navegue até o diretório comum e edite o arquivo Cargo.toml, adicionando as seguintes dependências:

[dependencies] serde={version=”=1.0.126″, features=[“derive”]}

Em seguida, edite o arquivo Cargo.toml no frontend e adicione estas dependências:

[dependencies] yew=”0.18″wasm-bindgen=”0.2.67″serde_json=”1″serde={version=”=1.0.126″, features=[“derive”]} anyhow=”1″yew-router=”0.15.0″common={version=”0.1.0″, path=”../common”}

Estamos usando Yew para construir o Interface baseada em Wasm . Adicionamos mais algumas bibliotecas de utilitários para roteamento e erro e Manuseio de JSON , bem como uma dependência interna para nossa biblioteca comum, que manterá o código compartilhado entre o front-end e o back-end.

Finalmente, edite o arquivo Cargo.toml no back-end e adicione estas dependências:

[dependencies] tokio={version=”=1.6.1″, features=[“macros”,”rt-multi-thread”]} warp=”=0.3.1″mobc=”=0.7.2″mobc-postgres={version=”=0.7.0″, features=[“with-chrono-0_4″,”with-serde_json-1″]} serde={version=”=1.0.126”, features=[“derive”]} serde_json=”=1.0.64″thiserror=”=1.0.24″common={version=”0.1.0″, path=”../common”}

Estamos usando a estrutura da web warp para construir o back-end. Como estamos usando um banco de dados Postgres para armazenar nossos dados, também adicionaremos o pool de conexão mobc para Postgres.

Além disso, como Warp é otimizado para Tokio , precisamos adicioná-lo como nosso tempo de execução assíncrono. Adicionaremos algumas bibliotecas de utilitários para tratamento de erros e JSON, bem como uma dependência interna ao nosso projeto comum.

Isso é tudo para a configuração. Vamos começar a escrever o código compartilhado para front-end e back-end em nosso projeto comum.

Funcionalidade comum

Começaremos detalhando o módulo comum, onde adicionaremos o modelos de dados compartilhados entre o front-end e o back-end. Em um aplicativo real, muito mais poderia ser compartilhado-incluindo validação, auxiliares, utilitários etc.-mas, neste caso, nos limitaremos às estruturas de dados.

Em lib.rs, adicione os modelos de dados para nossos modelos Owner e Pet:

use serde:: {Deserialize, Serialize}; # [derive (Deserialize, Clone, PartialEq, Debug)] pub struct Owner {pub id: i32, pub name: String,} # [derive (Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct OwnerRequest {pub name: String,} # [derive (Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct OwnerResponse {id do pub: i32, nome do pub: String,} impl OwnerResponse {pub fn of (owner: Owner)-> OwnerResponse {OwnerResponse { id: owner.id, name: owner.name,}}} # [derive (desserialize, clone, PartialEq, Debug)] pub struct Pet {pub id: i32, pub name: String, pub owner_id: i32, pub animal_type: String, pub color: Option ,} # [derive (Serialize, Desserialize, Clone, PartialEq, Debug)] pub struct PetRequest {pub name: String, pub animal_type: String, pub color: Option ,} # [derive (Serialize, Deserialize, Clone, PartialEq, Debug)] pub struct PetResponse {pub id: i32, pub name: String, p ub animal_type: String, pub color: Option ,} impl PetResponse {pub fn of (pet: Pet)-> PetResponse {PetResponse {id: pet.id, name: pet.name, animal_type: pet.animal_type, color: pet.color,}}}

Definimos as estruturas de domínio do banco de dados Owner e Pet, bem como os objetos de dados de solicitação e resposta, que usaremos para a comunicação entre o front-end e o back-end.

Compartilhar este código é bom porque adicionar ou remover um campo em algum lugar da API nos dará um erro de compilação no frontend se não fizermos acomodações para a mudança. Isso pode nos poupar algum tempo atrás de um erro quando uma API é atualizada.

O proprietário é muito simples, com apenas um nome e a ID do banco de dados. O tipo Pet tem um nome, um animal_type e uma cor opcional.

Também definimos alguns auxiliares para criar nossos objetos de dados para a API a partir dos objetos de domínio do banco de dados.

Este é tudo o que colocaremos no projeto comum.

Vamos continuar com a parte de back-end de nosso aplicativo.

Construindo o back-end REST

Começamos com o definição do banco de dados para o nosso modelo de dados:

CREATE TABLE IF NOT EXISTS owner (id SERIAL PRIMARY KEY NOT NULL, nome VARCHAR (255) NOT NULL); CRIAR TABELA SE NÃO EXISTIR pet (id SERIAL PRIMARY KEY NOT NULL, owner_id INT NOT NULL, nome VARCHAR (255) NOT NULL, animal_type VARCHAR (255) NOT NULL, cor VARCHAR (255), CONSTRAINT fk_pet_owner_id FOREIGN KEY (owner_id) REFERÊNCIAS pet (Eu iria) );

Isso define nossas duas tabelas de dados com seus respectivos campos.

Vamos construir o back-end de baixo para cima, começando com a camada de banco de dados e avançando até o servidor web e as definições de roteamento.

Primeiro, vamos criar um módulo db. Aqui, vamos começar com algum banco de dados e código de inicialização do pool de conexão em mod.rs:

type Result =std:: result:: Result ; const DB_POOL_MAX_OPEN: u64=32; const DB_POOL_MAX_IDLE: u64=8; const DB_POOL_TIMEOUT_SECONDS: u64=15; const INIT_SQL: & str=”./db.sql”; pub async fn init_db (db_pool: & DBPool)-> Result ()> {let init_file=fs:: read_to_string (INIT_SQL) ?; deixe con=get_db_con (db_pool).await ?; con.batch_execute (init_file.as_str ()).await.map_err (DBInitError) ?; Ok (())} pub async fn get_db_con (db_pool: & DBPool)-> Resultado {db_pool.get (). Await.map_err (DBPoolError)} pub fn create_pool ()-> std:: result:: Result > {let config=Config:: from_str (“postgres://[email protected]: 7878/postgres”) ?; let manager=PgConnectionManager:: new (config, NoTls); Ok (Pool:: builder ().max_open (DB_POOL_MAX_OPEN).max_idle (DB_POOL_MAX_IDLE).get_timeout (Some (Duration:: from_secs (DB_POOL_TIMEOUT_SECONDS))).build (manager))}

No init_db, lemos o db.sql file e execute-o para inicializar nossas tabelas.

Os helpers create_pool e get_db_con estão lá para inicializar o pool de banco de dados e obter uma nova conexão do pool.

Com essas configurações mais detalhes, vamos dar uma olhada em nosso primeiro objeto de acesso ao domínio em owner.rs.

pub const TABELA: & str=”owner”; const SELECT_FIELDS: & str=”id, nome”; pub async fn fetch (db_pool: & DBPool)-> Resultado > {let con=get_db_con (db_pool).await ?; deixe query=format! (“SELECT {} FROM {}”, SELECT_FIELDS, TABLE); let rows=con.query (query.as_str (), & []). await.map_err (DBQueryError) ?; Ok (rows.iter (). Map (| r | row_to_owner (& r)). Collect ())} pub async fn fetch_one (db_pool: & DBPool, id: i32)-> Resultado {let con=get_db_con (db_pool ).aguardam?; deixe query=format! (“SELECT {} FROM {} WHERE id=$ 1”, SELECT_FIELDS, TABLE); let row=con.query_one (query.as_str (), & [& id]).await.map_err (DBQueryError) ?; Ok (row_to_owner (& row))} pub async fn create (db_pool: & DBPool, body: OwnerRequest)-> Resultado {let con=get_db_con (db_pool).await ?; deixe consulta=formato! (“INSERT INTO {} (nome) VALORES ($ 1) RETORNANDO *”, TABELA); let row=con.query_one (query.as_str (), & [& body.name]).await.map_err (DBQueryError) ?; Ok (row_to_owner (& row))} fn row_to_owner (row: & Row)-> Owner {let id: i32=row.get (0); deixe o nome: String=row.get (1); Owner {id, name}}

Existem três operações de banco de dados para proprietários:

fetch busca todos os proprietários fetch_one busca o proprietário com um determinado ID criar cria um novo proprietário

A implementação desses métodos é bastante direta. Primeiramente, obtemos uma conexão do pool, então definimos a consulta Postgres a ser executada e a executamos com os valores dados, propagando quaisquer erros.

Finalmente, usamos o auxiliar row_to_owner para converter o dados de linha do banco de dados para uma estrutura de Proprietário real.

O objeto de acesso a dados pet.rs é bastante semelhante:

pub const TABLE: & str=”pet”; const SELECT_FIELDS: & str=”id, owner_id, name, animal_type, color”; pub async fn fetch (db_pool: & DBPool, owner_id: i32)-> Resultado > {let con=get_db_con (db_pool).await ?; deixe query=format! (“SELECT {} FROM {} WHERE owner_id=$ 1”, SELECT_FIELDS, TABLE); let rows=con.query (query.as_str (), & [& owner_id]).await.map_err (DBQueryError) ?; Ok (rows.iter (). Map (| r | row_to_pet (& r)). Collect ())} pub async fn create (db_pool: & DBPool, owner_id: i32, body: PetRequest)-> Result {let con=get_db_con (db_pool).await ?; deixe query=format! (“INSERT INTO {} (nome, id_proprietário, tipo_animal, cor) VALORES ($ 1, $ 2, $ 3, $ 4) RETORNANDO *”, TABELA); let row=con.query_one (query.as_str (), & [& body.name, & owner_id, & body.animal_type, & body.color],).await.map_err (DBQueryError) ?; Ok (row_to_pet (& row))} pub assíncrono fn delete (db_pool: & DBPool, owner_id: i32, id: i32)-> Resultado {let con=get_db_con (db_pool).await ?; deixe query=format! (“DELETE FROM {} WHERE id=$ 1 AND owner_id=$ 2”, TABLE); con.execute (query.as_str (), & [& id, & owner_id]).await.map_err (DBQueryError)} fn row_to_pet (row: & Row)-> Pet {let id: i32=row.get (0); deixe owner_id: i32=row.get (1); deixe o nome: String=row.get (2); deixe animal_type: String=row.get (3); deixe a cor: Opção =row.get (4); Pet {id, name, owner_id, animal_type, color,}}

Aqui temos os três métodos a seguir:

fetch busca todos os animais de estimação pertencentes a um determinado owner_id create cria um novo animal para o determinado owner_id delete exclui o pet com o id e owner_id fornecidos

Em termos de implementação, segue exatamente o mesmo conceito do owner.rs acima.

Isso conclui a camada do banco de dados. Vamos avançar um passo e implementar handler.rs em src.

pub async fn list_pets_handler (owner_id: i32, db_pool: DBPool)-> Result {let pets=db:: pet:: fetch (& db_pool , owner_id).await.map_err (rejeitar:: custom) ?; Ok (json:: _>> (& pets.into_iter (). Map (PetResponse:: of).collect (),))} pub async fn create_pet_handler (owner_id: i32, body: PetRequest, db_pool: DBPool, )-> Resultado {Ok (json (& PetResponse:: of (db:: pet:: create (& db_pool, owner_id, body).await.map_err (rejeitar:: custom) ?,)))} pub assíncrono fn delete_pet_handler (owner_id: i32, id: i32, db_pool: DBPool)-> Resultado {db:: pet:: delete (& db_pool, owner_id, id).await.map_err (rejeitar:: custom) ?; Ok (StatusCode:: OK)} pub async fn list_owners_handler (db_pool: DBPool)-> Resultado {let owners=db:: owner:: fetch (& db_pool).await.map_err (rejeitar:: custom) ?; Ok (json:: _>> (& owners.into_iter (). Map (OwnerResponse:: of).collect (),))} pub async fn fetch_owner_handler (id: i32, db_pool: DBPool)-> Resultado {let owner=db:: owner:: fetch_one (& db_pool, id).await.map_err (rejeitar:: custom) ?; Ok (json (& OwnerResponse:: of (owner)))} pub async fn create_owner_handler (body: OwnerRequest, db_pool: DBPool)-> Result {Ok (json (& OwnerResponse:: of (db:: owner:: create (& db_pool, body).await.map_err (rejeitar:: custom) ?,)))}

A superfície API consiste em seis operações:

Listar proprietários Buscar proprietário para um determinado ID Criar proprietário Criar animal de estimação Excluir pet List pets para um determinado dono

Em cada caso, simplesmente chamamos a operação correspondente em nossa camada de banco de dados e convertemos o Owner retornado, ou Pet em OwnerResponse ou PetResponse, respectivamente, retornando quaisquer erros diretamente para o chamador.

Finalmente, avançando mais um passo, implementamos o servidor web real apontando para esses manipuladores em main.rs.

mod db; erro de mod; manipulador de mod; tipo Resultado =padrão:: resultado:: Resultado ; tipo DBCon=Conexão >; tipo DBPool=Pool >; # [tokio:: main] async fn main () {let db_pool=db:: create_pool (). expect (“banco de dados pode ser criado”); db:: init_db (& db_pool).await.expect (“banco de dados pode ser inicializado”); let pet=warp:: path! (“dono”/i32/”pet”); let pet_param=warp:: path! (“proprietário”/i32/”pet”/i32); let owner=warp:: path (“proprietário”); deixe pet_routes=pet.and (warp:: get ()).and (with_db (db_pool.clone ())).and_then (handler:: list_pets_handler).or (pet.and (warp:: post ()).and (warp:: body:: json ()).and (with_db (db_pool.clone ())).and_then (handler:: create_pet_handler)).or (pet_param.and (warp:: delete ()).and (with_db (db_pool.clone ())).and_then (handler:: delete_pet_handler)); deixe owner_routes=owner.and (warp:: get ()).and (warp:: path:: param ()).and (with_db (db_pool.clone ())).and_then (handler:: fetch_owner_handler).or ( owner.and (warp:: get ()).and (with_db (db_pool.clone ())).and_then (handler:: list_owners_handler)).or (owner.and (warp:: post ()).and (warp:: body:: json ()).and (with_db (db_pool.clone ())).and_then (handler:: create_owner_handler)); let routes=pet_routes.or (owner_routes).recover (error:: handle_rejection).with (warp:: cors ().allow_credentials (true).allow_methods (& [Method:: OPTIONS, Method:: GET, Method:: POST , Method:: DELETE, Method:: PUT,]).allow_headers (vec! [Header:: CONTENT_TYPE, header:: ACCEPT]).expose_headers (vec! [Header:: LINK]).max_age (300).allow_any_origin ( ),); warp:: serve (routes).run (([127, 0, 0, 1], 8000)). await; } fn with_db (db_pool: DBPool)-> impl Filter + Clone {warp:: any (). map (move || db_pool.clone ())}

Existe um pouco para descompactar, então vamos analisá-lo.

Inicialmente, definimos os módulos e alguns tipos para economizar tempo de digitação. Em seguida, na função principal (ou tokio:: main, o ponto de entrada assíncrono de nosso aplicativo), primeiro inicializamos o banco de dados e o banco de dados.

Na parte inferior, há um filtro with_db, que é a forma preferida no warp de passar dados para um manipulador-neste caso, o pool de conexão.

Em seguida, definimos várias bases de roteamento para pet, que tem a forma/owner/$ ownerId/pet; pet_param, que adiciona um/$ petId no final; e owner, que simplesmente contém/owner.

Com essas bases, podemos definir nossas rotas, levando aos diferentes manipuladores:

GET/owner lista todos os proprietários GET/owner/$ ownerId retorna owner com o ID fornecido POST/proprietário cria um proprietário GET/owner/$ ownerid/pet lista todos os animais de estimação do determinado proprietário POST/owner/$ ownerId/pet cria um animal de estimação para o determinado proprietário DELETE/owner/$ ownerId/pet/$ petId exclui o animal de estimação com o ID fornecido e o ID do proprietário

Em seguida, conectamos tudo com um configuração do CORS e execute o servidor na porta 8000.

Isso conclui o back-end. Você pode executá-lo simplesmente executando cargo run e, desde que tenha um banco de dados Postgres em execução na porta 7878 (por exemplo, usando Docker), você terá a API REST em execução em http://localhost: 8000 .

Você pode testá-lo usando cURL executando comandos como este:

curl-X POST http://localhost: 8000/owner-d'{“name”:”mario”}’-H’content-type: application/json’curl-v-X POST http://localhost: 8000/owner/1/pet-d'{“nome”:”minka”,”animal_type”:”cat”,”color”:”black-brown-white”}’-H’content-type: application/json’

A implementação do frontend

Agora que temos um back-end totalmente funcional, precisamos de uma maneira de interagir com ele.

No caso do front-end, vamos começar do início em lib.rs e avançar até os componentes porque é mais natural percorrer a árvore de componentes passo a passo.

Usaremos yew_router para roteamento. Caso contrário, usaremos a mesma configuração que a documentação oficial do Yew sugere , usando trunk para construir e servir o aplicativo da web.

Em nosso aplicativo, há são dois módulos, animal de estimação e proprietário. No entanto, antes de começarmos a escrever qualquer código Rust, precisamos criar nosso arquivo index.html na raiz do nosso projeto de front-end, incluindo os estilos que usaremos:

Exemplo Rust Fullstack

Este arquivo HTML será usado como um ponto de partida e o tronco adicionará os trechos correspondentes para fazer nosso aplicativo funcionar com ele na pasta dist quando construirmos o aplicativo.

Comece na raiz

Vamos começar no topo com lib.rs.

Primeiro definimos alguns módulos e uma estrutura para conter nosso componente raiz, como bem como algumas rotas.

proprietário do mod; mod pet; tipo de pub Anchor=RouterAnchor ; struct FullStackApp {} pub enum Msg {} # [derive (Switch, Clone, Debug)] pub enum AppRoute {# [to=”/app/create-owner”] CreateOwner, # [to=”/app/create-pet/{id}”] CreatePet (i32), # [to=”/app/{id}”] Detalhe (i32), # [to=”/”] Home,}

Nosso aplicativo tem rotas para Home ( por exemplo, proprietários de lista), para ver uma página de detalhes do proprietário e para criar proprietários e animais de estimação.

Em seguida, implementamos o atributo Component para nosso FullStackApp para que possamos usá-lo como um ponto de entrada.

Componente impl para FullStackApp {type Message=Msg; tipo Propriedades=(); fn create (_: Self:: Properties, _link: ComponentLink )-> Self {Self {}} fn update (& mut self, _msg: Self:: Message)-> ShouldRender {true} fn change (& mut self, _props: Self:: Properties)-> ShouldRender {true} visão fn (& self)-> Html {html! {

{“Home”}
render=Router:: render (mover | switch: AppRoute | {match switch {AppRoute:: CreateOwner=> {html! {

}} AppRoute:: CreatePet (owner_id)=> {html! {

} } AppRoute:: Detail (owner_id)=> {html! {

}} AppRoute:: Home=> {html! {


{“Criar novo proprietário”}

}}}})/>

}}}

Nosso componente raiz realmente não faz muito; contém apenas um menu simples com um link Home, que está sempre visível, e depois inclui o roteador, que, para cada uma de nossas rotas, configura qual componente deve ser mostrado e o que é apenas uma marcação extra.

Por exemplo, para AppRoute:: Home, nossa rota inicial padrão, mostramos uma lista de proprietários e um link para o formulário Criar novo proprietário.

Finalmente, precisamos do seguinte trecho para fazer o Wasm-magic funciona e assim obtemos um aplicativo da web real do tronco:

# [wasm_bindgen (start)] pub fn run_app () {App:: :: new (). mount_to_body (); }

Lista de proprietários

Vamos começar com a lista de proprietários mostrada na página inicial porque é o componente mais simples.

No módulo proprietário, criamos um arquivo mod.rs, um create.rs, um arquivo detail.rs e list.rs.

No mod.rs, simplesmente exportamos estes módulos:

pub mod create; detalhe do mod do pub; lista de mod de pub;

Em seguida, começamos a implementar list.rs.

O objetivo é obter a lista de proprietários do back-end e exibir cada proprietário com um link para sua página de detalhes.

Nós primeiro defina a estrutura de Lista, que é a base de nosso componente:

pub struct Lista {fetch_task: Opção , proprietários: Opção >, link: ComponentLink ,}

O ComponentLink é o método de Yew de enviar mensagens dentro do componente para, por exemplo, acionar efeitos colaterais, como uma solicitação da web.

Como estamos usando o FetchService de Yew, também precisamos salvar o fetch_task que estamos vai usar para buscar os proprietários do back-end.

A lista de proprietários é None no início e será preenchida assim que a solicitação para o back-end (espero) retornar uma lista de proprietários.

Então, definimos nosso Msg enum, definindo as mensagens que são tratadas pelo componente.

pub enum Msg {MakeReq, Resp (Result , anyhow:: Error>),}

Simplesmente criamos uma ação para tornar o pedido e um para receber o resultado do backend.

Com isso, podemos implementar o componente da seguinte maneira:

impl Component for List {type Properties=(); tipo Message=Msg; fn create (_props: Self:: Properties, link: ComponentLink )-> Self {link.send_message (Msg:: MakeReq); Próprio {fetch_task: Nenhum, link, proprietários: Nenhum,}} visualização fn (& self)-> Html {html! {

{self.render_list ()}

}} fn update (& mut self, msg: Self:: Message)-> ShouldRender {match msg {Msg:: MakeReq=> {self.owners=None; let req=Request:: get (“http://localhost: 8000/owner”).body (Nothing).expect (“can make req to backend”); let cb=self.link.callback (| response: Response , de qualquer forma:: Error >>> | {let Json (data)=response.into_body (); Msg:: Resp (data )},); let task=FetchService:: fetch (req, cb).expect (“pode ​​criar tarefa”); self.fetch_task=Alguns (tarefa); ()} Msg:: Resp (resp)=> {se deixar Ok (dados)=resp {self.owners=Some (data); }}} true} fn change (& mut self, _props: Self:: Properties)-> ShouldRender {true}}

Quando o componente é criado, usamos o link do componente para acionar MakeReq, enviando uma solicitação aos proprietários para o back-end. Em seguida, inicializamos o componente.

Na atualização, lidamos com as mensagens de solicitação e resposta, usando o FetchService para enviar uma solicitação para http://localhost: 8000/owner , onde nosso backend nos fornece a lista de proprietários.

Em seguida, analisamos a resposta no callback e chamamos Msg:: Resp (data), que, se nenhum erro ocorreu, irá definir os dados em nosso componente.

Na função render, nós simplesmente chamamos render_list, que implementamos em List como segue:

impl List {fn render_list (& self)-> Html {if let Some (t)=& self.owners {html! {

{t.iter (). map (| name | self.view_owner (name)). collect::

}} else { html! {

{“loading…”}

}}} fn view_owner (& self, owner: & OwnerResponse)-> Html {html! {

{& owner.name}

}}}

Basicamente, se tivermos self.owners definidos, iteramos sobre a lista e renderizamos view_owner para cada um deles. Isso cria um link para AppRoute:: Detail com o ID do proprietário, que é um link para a página de detalhes.

Se não tivermos dados, mostramos uma mensagem carregando….

Isso é tudo para proprietários de listas. Vamos continuar com a página de detalhes em detail.rs.

Criando uma página de detalhes para proprietários

A página de detalhes do proprietário é um pouco mais complicada. Aqui, precisamos fazer duas solicitações: uma para buscar o proprietário com o ID de proprietário fornecido (para que também possamos atualizar a página e usar a rota diretamente), bem como a lista de animais de estimação do proprietário. Além disso, temos que implementar a funcionalidade para excluir animais de estimação aqui.

A ideia geral é a mesma:

# [derive (Propriedades, Clone, PartialEq)] pub struct Props {pub owner_id: i32 ,} pub struct Detail {props: Props, link: ComponentLink , pets: Option >, owner: Option , fetch_pets_task: Option , fetch_owner_task: Option , delete_pet_task: Opção ,} pub enum Msg {MakePetsReq (i32), MakeOwnerReq (i32), MakeDeletePetReq (i32, i32), RespPets (Result , anyhow:: Error>), RespOwner (Result ), RespDeletePet (Response (), de qualquer maneira:: Error >>>, i32),}

Definimos os props para o componente com o qual é chamado-neste caso, o proprietário id do caminho da rota.

Em seguida, definimos a estrutura Detalhe, que contém os dados do nosso componente, incluindo os animais de estimação e o proprietário que iremos buscar, bem como o link do componente a encontre os adereços e FetchTasks para buscar animais de estimação, buscar um proprietário e excluir um animal de estimação.

Vamos dar uma olhada na implementação do componente:

impl Component for Detail {type Properties=Props; tipo Message=Msg; fn create(props: Self::Properties, link: ComponentLink)-> Self { link.send_message(Msg::MakePetsReq(props.owner_id)); link.send_message(Msg::MakeOwnerReq(props.owner_id)); Self { props, link, owner: None, pets: None, fetch_pets_task: None, fetch_owner_task: None, delete_pet_task: None, } } fn view(&self)-> Html { html! {

{ self.render_detail(&self.owner, &self.pets)}

} } fn update(&mut self, msg: Self::Message)-> ShouldRender { match msg { Msg::MakePetsReq(id)=> { let req=Request::get(&format!(“http://localhost:8000/owner/{}/pet”, id)) .body(Nothing) .expect(“can make req to backend”); let cb=self.link.callback( |response: Response, anyhow::Error>>>| { let Json(data)=response.into_body(); Msg::RespPets(data) }, ); let task=FetchService::fetch(req, cb).expect(“can create task”); self.fetch_pets_task=Some(task); () } Msg::MakeOwnerReq(id)=> { let req=Request::get(&format!(“http://localhost:8000/owner/{}”, id)) .body(Nothing) .expect(“can make req to backend”); let cb=self.link.callback( |response: Response>>| { let Json(data)=response.into_body(); Msg::RespOwner(data) }, ); let task=FetchService::fetch(req, cb).expect(“can create task”); self.fetch_owner_task=Some(task); () } Msg::MakeDeletePetReq(owner_id, pet_id)=> { let req=Request::delete(&format!( “http://localhost:8000/owner/{}/pet/{}”, owner_id, pet_id )) .body(Nothing) .expect(“can make req to backend”); let cb=self.link.callback( move |response: Response(), anyhow::Error>>>| { Msg::RespDeletePet(response, pet_id) }, ); let task=FetchService::fetch(req, cb).expect(“can create task”); self.delete_pet_task=Some(task); () } Msg::RespPets(resp)=> { if let Ok(data)=resp { self.pets=Some(data); } } Msg::RespOwner(resp)=> { if let Ok(data)=resp { self.owner=Some(data); } } Msg::RespDeletePet(resp, id)=> { if resp.status().is_success() { self.pets=self .pets .as_ref() .map(|pets| pets.into_iter().filter(|p| p.id !=id).cloned().collect()); } } } true } fn change(&mut self, props: Self::Properties)-> ShouldRender { self.props=props; true } }

The basics are the same, our view calls a render_detail function, which we’ll look at in a bit and in create, we also initialize our component and trigger the fetching of pets and the owner by sending the corresponding messages and the given owner_id.

In update, we need to implement the request and response handlers for fetching pets and the owner. These are almost exactly the same as in the List component, just with different URLs and different return types.

In the handler of MakeDeletePetReq, we send the DELETE request using the owner_id and pet_id given. If it works, we trigger the Msg::RespDeletePet message.

There, if the request succeeds, we simply remove the pet with the given ID from our list of pets. This is nice because it means we don’t need to refetch the whole list of pets.

Let’s look at the rendering code for the owner detail:

impl Detail { fn render_detail( &self, owner: &Option, pets: &Option>, )-> Html { match owner { Some(o)=> { html! {

{ self.view_pet_list(pets) }
{“Create New Pet”}

} } None=> { html! {

{“loading…”}

} } } } fn view_pet_list(&self, pets: &Option>)-> Html { match pets { Some(p)=> { html! { p.iter().map(|pet| self.view_pet(pet)).collect::

{“loading…”}

} } } } fn view_pet(&self, pet: &PetResponse)-> Html { let id=pet.id; let owner_id=self.props.owner_id; html! {

{ &pet.name } {“(“} {“)”}
{ &pet.animal_type }
{ &pet.color.as_ref().unwrap_or(&String::new()) }

} } }

Again, if we have data, we render it. Otherwise, we show a loading… indicator. Once we have our owner, we render its name with its ID next to it.

Below, we render the list of pets, with the actual rendering of the pets in view_pet. We also create the button for deleting pets, which has an onclick handler triggering the MsgMakeDeletePetReq message.

Below the pet list, we show a link to the Create Pet route.

We’re almost done. Now we just have to look at the components for creating owners and pets. Let’s start with owners in create.rs:

pub struct CreateForm { link: ComponentLink, fetch_task: Option, state_name: String, } pub enum Msg { MakeReq, Resp(Result), EditName(String), }

Again, we start with the Component struct and the Msg enum.

In this case, we need the data infrastructure to make a request to create an owner, but we also need a way to create and edit a form.

For this purpose, we create the state_name field on our component and the EditName(String) in Msg.

Let’s look at the Component implementation next:

impl Component for CreateForm { type Properties=(); type Message=Msg; fn create(_props: Self::Properties, link: ComponentLink)-> Self { Self { link, state_name: String::new(), fetch_task: None, } } fn view(&self)-> Html { html! {

{ self.render_form() }

} } fn update(&mut self, msg: Self::Message)-> ShouldRender { match msg { Msg::MakeReq=> { let body=OwnerRequest { name: self.state_name.clone(), }; let req=Request::post(“http://localhost:8000/owner”) .header(“Content-Type”,”application/json”) .body(Json(&body)) .expect(“can make req to backend”); let cb=self.link.callback( |response: Response>>| { let Json(data)=response.into_body(); Msg::Resp(data) }, ); let task=FetchService::fetch(req, cb).expect(“can create task”); self.fetch_task=Some(task); () } Msg::Resp(resp)=> { ConsoleService::info(&format!(“owner created: {:?}”, resp)); if let Ok(_)=resp { RouteAgent::dispatcher().send(RouteRequest::ChangeRoute(Route { route:”/”.to_string(), state: (), })); } } Msg::EditName(input)=> { self.state_name=input; } } true } fn change(&mut self, _props: Self::Properties)-> ShouldRender { true } } impl CreateForm { fn render_form(&self)-> Html { let edit_name=self .link .callback(move |e: InputData| Msg::EditName(e.value)); html! {

} } }

As you can see, in render_form inside the CreateForm implementation, we create a simple form input field, which takes self.state_name as a value. This means it’s directly connected to our state.

We use the oninput event handler to, each time someone writes text into the input field, call the Msg::EditName message with the value of the input field.

When you look at the update function in the Component implementation, the handler for Msg::EditName simply sets self.state_name to the given value in the input. This ensures that we always have the value that’s in the form field inside our component.

This is important once we click on the Submit button, which triggers Msg::MakeReq. There, we create a JSON payload for creating an owner using self.state_name as the value for the name.

Then we send this payload to the backend endpoint for creating an owner and, if everything is successful, use yew_router‘s RouteAgent and dispatcher to manually change the route back to”/”, our Home route.

Detail page for pets

That’s how easy form handling is with Yew! Let’s look at the final part of the puzzle by creating a pet module with a mod.rs and create.rs inside.

In the mod.rs, we again just export create:

pub mod create;

In create.rs, we implement the component for adding new pets, which will be very similar to the owner’s CreateForm we just implemented.

#[derive(Properties, Clone, PartialEq)] pub struct Props { pub owner_id: i32, } pub struct CreateForm { props: Props, link: ComponentLink, fetch_task: Option, state_pet_name: String, state_animal_type: String, state_color: Option, } pub enum Msg { MakeReq(i32), Resp(Result), EditName(String), EditAnimalType(String), EditColor(String), }

The CreatePet form takes as a prop the owner_id of the owner for whom we want to create a pet.

Then, we define state_pet_name, state_animal_type, and state_color to keep the state of our three form fields, same as we did for the owner.

For Msg, it’s the same: we need handlers for each of our form fields, as well as for making the create pet request and handling its response.

Let’s look a t the implementation of Component and the rendering logic:

impl Component for CreateForm { type Properties=Props; type Message=Msg; fn create(props: Self::Properties, link: ComponentLink)-> Self { Self { props, link, state_pet_name: String::new(), state_animal_type: String::from(“cat”), state_color: Some(String::from(“black”)), fetch_task: None, } } fn view(&self)-> Html { html! {

{ self.render_form(self.props.owner_id) }

} } fn update(&mut self, msg: Self::Message)-> ShouldRender { match msg { Msg::MakeReq(id)=> { let body=PetRequest { name: self.state_pet_name.clone(), animal_type: self.state_animal_type.clone(), color: self.state_color.clone(), }; let req=Request::post(&format!(“http://localhost:8000/owner/{}/pet”, id)) .header(“Content-Type”,”application/json”) .body(Json(&body)) .expect(“can make req to backend”); let cb=self.link.callback( |response: Response>>| { let Json(data)=response.into_body(); Msg::Resp(data) }, ); let task=FetchService::fetch(req, cb).expect(“can create task”); self.fetch_task=Some(task); () } Msg::Resp(resp)=> { ConsoleService::info(&format!(“pet created: {:?}”, resp)); if let Ok(_)=resp { RouteAgent::dispatcher().send(RouteRequest::ChangeRoute(Route { route: format!(“/app/{}”, self.props.owner_id), state: (), })); } } Msg::EditName(input)=> { self.state_pet_name=input; } Msg::EditAnimalType(input)=> { ConsoleService::info(&format!(“input: {:?}”, input)); self.state_animal_type=input; } Msg::EditColor(input)=> { self.state_color=Some(input); } } true } fn change(&mut self, props: Self::Properties)-> ShouldRender { self.props=props; true } } impl CreateForm { fn render_form(&self, owner_id: i32)-> Html { let edit_name=self .link .callback(move |e: InputData| Msg::EditName(e.value)); let edit_animal_type=self.link.callback(move |e: ChangeData| match e { ChangeData::Select(elem)=> Msg::EditAnimalType(elem.value()), _=> unreachable!(“only used on select field”), }); let edit_color=self .link .callback(move |e: InputData| Msg::EditColor(e.value)); html! {

} } }

Let’s start with the render_form function in CreateForm. Here, we again create input fields for all the pet’s fields. However, this time with a twist: we use a select field for the animal type, since we want to limit it to cats and dogs.

That means, for the callback handler of edit_animal_type, we get a ChangeData instead of an InputData. Inside it, we need to match on the type of change. We only want to react on ChangeData::Select(elem) and take the element’s value, sending it over to be set in our component state.

For the other two fields, the process is the same as in our Create Owner component.

In terms of the Component implementation, there isn’t really anything new here, either. We implement the handler for calling the create pet endpoint on our backend and the handlers for passing the form input field values to our state, so we can create the payload for this endpoint.

With this last component out of the way, our Rust full-stack web app implementation is complete! The only thing left is to test that it actually works.

Testing our Rust full-stack app

We can run both the frontend and the backend with a Postgres database running on port 7878 (navigate to http://localhost:8080).

There, we’re greeted by an empty Home screen. We can click Create New Owner, which shows us the form:

Submitting will create the owner, which we’ll see in the list back at Home:

Next, let’s click on the new owner to see the owner detail page:

Now we can start adding some pets using Create New Pet:

Once we’re done, we’re redirected back to the Owner Detail page, which shows us our newly added list of pets:

Finally, we can try to delete a pet by clicking the Delete button next to it:

Fantastic; it works! And all of it written in Rust.

You can find the full code for this example on GitHub.

Conclusion

In this tutorial, we demonstrated how to build a simple full-stack web application fully in Rust. We covered how to create a multimodule workspace using Cargo and how to share code between the frontend and backend parts of the application.

Thus Rust web ecosystem is still maturing, so it’s quite impressive that you can already build modern full-stack web apps with too much fuzz.

I’m excited to see how the Wasm journey continues and I’m very much looking forward to seeing the Rust async web ecosystem develop even further, with improved stability, compatibility, and richness in libraries.

In any case, the future of web development in Rust looks promising!