Concluindo o serviço
Falta pouco para termos nosso serviço pronto, pois precisamos implementar um get
por id e um update
. O get
por id não é muito diferente da rota index
, a única diferença é que vamos passar um parâmetro id
e chamaremos a rota de show
e será um método GET
também. Já o update
é um pouco diferente pois vamos enviar um corpo Json com as informações para atualizar em uma rota update
com o método PUT
. Assim, os endpoints que vamos implementar são:
- HTTP autenticado em
show/{id}
com o métodoGET
. - HTTP autenticado em
update/{id}
com o métodoPUT
e um body do tipo Json.
Show por ID
Como já falamos anteriormente, nosso objetivo agora é recuperar um TodoCard
com base em seu id
de inserção no banco de dados. Faremos isso utilizando a mesma função que utilizamos na rota index
, scan
. Para isso, sabemos que vamos precisar da rota show/{id}
, como já mencionamos, e vamos precisar retornar um TodoCard
. Assim, imagino que um bom teste para este cenário seria o seguinte:
#![allow(unused)] fn main() { #[cfg(test)] mod show_by_id { use actix_web::{test, App}; use dotenv::dotenv; use todo_server::todo_api_web::model::{ http::Clients, todo::TodoCard, }; use todo_server::todo_api_web::routes::app_routes; use serde_json::from_str; use crate::helpers::{mock_get_todos}; #[actix_rt::test] async fn test_todo_card_by_id() { dotenv().ok(); let mut app = test::init_service(App::new().data(Clients::new()).configure(app_routes)).await; let req = test::TestRequest::with_uri("/api/show/544e3675-19f5-4455-9ed9-9ccc577f70fe").to_request(); let resp = test::read_response(&mut app, req).await; let todo_card: TodoCard = from_str(&String::from_utf8(resp.to_vec()).unwrap()).unwrap(); assert_eq!(&todo_card, mock_get_todos().get(0usize).unwrap()); } } }
Este teste consiste em definir um request com um uuid, neste caso aleatório, para a rota show
com test::TestRequest::with_uri("/api/show/544e3675-19f5-4455-9ed9-9ccc577f70fe").to_request()
. Com o request em mão, chamamos o serviço para obter uma respose com test::read_response(&mut app, req).await
e convertemos esta response em um TodoCard
, let todo_card: TodoCard = from_str(&String::from_utf8(resp.to_vec()).unwrap()).unwrap()
. Como vamos mockar a resposta de TodoCard
com o primeiro valor de mock_get_todos
, basta comparar os dois com assert_eq!(&todo_card, mock_get_todos().get(0usize).unwrap())
.
O primeiro passo para resolver este teste é adicionar a rota a função app_routes
:
#![allow(unused)] fn main() { // src/todo_api_web/routes.rs // ... pub fn app_routes(config: &mut web::ServiceConfig) { config.service( web::scope("/") .service( web::scope("api/") .route("create", web::post().to(create_todo)) .route("index", web::get().to(show_all_todo)) .route("show/{id}", web::get().to(show_by_id)), ) .service( web::scope("auth/") .route("signup", web::post().to(signup_user)) .route("login", web::post().to(login)) .route("logout", web::delete().to(logout)), ) .route("ping", web::get().to(pong)) .route("~/ready", web::get().to(readiness)) .route("", web::get().to(|| HttpResponse::NotFound())), ); } }
Para recebermos o ID como argumento de rota precisamos definir-lo como {id}
, depois disso fazemos um GET
redirecionando o request para o controller show_by_id
:
#![allow(unused)] fn main() { // src/todo_api_web/controller/todo.rs // ... pub async fn show_by_id(id: web::Path<String>, state: web::Data<Clients>) -> impl Responder { let uuid = id.to_string(); match get_todo_by_id(uuid, state.dynamo.clone()) { None => { error!("Failed to read todo cards"); HttpResponse::NotFound().finish() } Some(todo_id) => HttpResponse::Ok().content_type("application/json") .json(todo_id) } } }
Na função show_by_id
vemos um ítem novo logo de cara, web::Path<String>
, a função deste ítem é extrair o conteúdo dos argumentos presentes na url do request, ou seja, todas as chaves encontradas entres os símbolos {
e }
, no nosso caso {id}
. Para o caso de um único argumento a estrutura de web::Path
é como estamos utilizando, mas para o caso de mais argumentos se utiliza tuplas para definir a sequencia de argumentos, por exemplo /api/show/{id}/task/{title}
, uma rota para obter o status de uma task
de um TodoCard
de id
específico, obteriamos os valores com web::Path<(String,String)>
. Valores diferentes de string podem ser passados desde que sejam serializáveis pelo serviço, por exemplo o código que escrevemos poderia substituir String
por Uuid
, caso fossemos utiliza-la:
#![allow(unused)] fn main() { pub async fn show_by_id(id: web::Path<uuid::Uuid>, state: web::Data<Clients>) -> impl Responder { let uuid = id.into_inner().to_string(); // ... } }
Não vamos utilizar o web::Path
com Uuid
pois, no futuro, vamos querer enviar um response BadRequest
caso o campo id
não seja um Uuid
. Se deixassemos assim o response seria InternalServerError
, que não é um status muito indicativo. Mantendo o web::Path
como String
passamos ao próximo ítem, uma funcnao de todo_api/db/todo.rs
que recupera um TodoCard
com base em seu id
, get_todo_by_id
. Os argumentos passados a get_todo_by_id
são uma String
contendo o id
e o cliente para dynamo
. Essa função retorna o tipo Option<TodoCard>
, que para o padrão None
vai retornar um status NotFound
, indicando que este elemento não foi encontrado e para o caso Some
vai retornar um Ok
com um corpo contendo um Json com o valor do TodoCard
encontrado.
A função get_todo_by_id
é semelhante a função get_todos
, mas com uma pequerna diferença, a struct ScanInput
utilizanda para fazer a busca no banco possui dois campos extras filter_expression
e expression_attribute_values
. filter_expression
é responsável por definir qual vai ser o filtro aplicado a este scan
, por exemplo =, >=, <
. No nosso caso, nossa filter_expression
será Some("id = :id".into())
, ou seja, vamos procurar um id
que seja igual ao argumento :id
. Poderiamos ter mais filtros em filter_expression
, mas usaremos somente esse. Agora precisamos definir o argumento :id
para aplicar em filter_expression
. Este argumento é adicionado a query através de expression_attribute_values
, que recebe um HashMap
contendo o nome das chaves, :id
no nosso caso, e um AttributeValue
com a informação de id
:
#![allow(unused)] fn main() { use std::collections::HashMap; use rusoto_dynamodb::{AttributeValue, DynamoDb}; let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(id); _map.insert(String::from(":id"), attr); let scan_item = ScanInput { // ... filter_expression: Some("id = :id".into()), expression_attribute_values: Some(_map), ..ScanInput::default() } }
Filter Expression
A lista de possíveis operadores para
filter_expression
é a seguinte:
- Funções:
attribute_exists | attribute_not_exists | attribute_type | contains | begins_with | size
, todas sensitivas a letras maísculas.- Operadores de comparação:
= | <> | < | > | <= | >= | BETWEEN | IN
- Operadores lógicos:
AND | OR | NOT
Com a Struct ScanInput
definida podemos executar a query em si com client.scan(scan_item).sync()
e aplicar um match
a resposta de scan
. Existem dois padrões possíveis Ok
e Err
, como nosso controller espera um Option<TodoCard>
retornamos um None
no caso de Err
. E no caso de Ok
ainda temos que cuidar o caso de a resposta de Ok
vir vazia:
#![allow(unused)] fn main() { match client.scan(scan_item).sync() { Ok(resp) => { let todo_id = adapter::scanoutput_to_todocards(resp); if todo_id.first().is_some() { debug!("Scanned {:?} todo cards", todo_id); Some(todo_id.first().unwrap().to_owned()) } else { error!("Could find todocard with ID."); None } } Err(e) => { error!("Could not scan todocard due to error {:?}", e); None } } }
Como a estrutura de resp
é um ScanOutput
, como em get_todos
, podemos aplicar o mesmo adapter adapter::scanoutput_to_todocards
a resp
, porém a resposta deste adapter será um vetor de TodoCard
. Como queremos somente um único elemento na resposta dessa query, aplicamos a função first
e validamos o caso de ela não retornar Some
, indicando com uma respostas None
. Para o caso de retornar sim, retornamos um Option
com o primeiro TodoCard
com Some(todo_id.first().unwrap().to_owned())
. A função completa ficou como a seguir, funcnao de teste esta logo depois retornando apenas Some(TodoCard{...})
:
#![allow(unused)] fn main() { #[cfg(not(feature = "dbtest"))] pub fn get_todo_by_id(id: String, client: DynamoDbClient) -> Option<TodoCard> { use rusoto_dynamodb::{AttributeValue, DynamoDb}; use std::collections::HashMap; let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(id); _map.insert(String::from(":id"), attr); let scan_item = ScanInput { limit: Some(100i64), table_name: TODO_CARD_TABLE.to_string(), filter_expression: Some("id = :id".into()), expression_attribute_values: Some(_map), ..ScanInput::default() }; match client.scan(scan_item).sync() { Ok(resp) => { let todo_id = adapter::scanoutput_to_todocards(resp); if todo_id.first().is_some() { debug!("Scanned {:?} todo cards", todo_id); Some(todo_id.first().unwrap().to_owned()) } else { error!("Could find todocard with ID."); None } } Err(e) => { error!("Could not scan todocard due to error {:?}", e); None } } } #[cfg(feature = "dbtest")] pub fn get_todo_by_id(id: String, client: DynamoDbClient) -> Option<TodoCard> { use rusoto_dynamodb::{AttributeValue, DynamoDb}; use std::collections::HashMap; use crate::todo_api_web::model::todo::{State, Task}; let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(id); _map.insert(String::from(":id"), attr); let scan_item = ScanInput { limit: Some(100i64), table_name: TODO_CARD_TABLE.to_string(), filter_expression: Some("id = :id".into()), expression_attribute_values: Some(_map), ..ScanInput::default() }; Some( TodoCard { id: Some(uuid::Uuid::parse_str("be75c4d8-5241-4f1c-8e85-ff380c041664").unwrap()), title: String::from("This is a card"), description: String::from("This is the description of the card"), owner: uuid::Uuid::parse_str("ae75c4d8-5241-4f1c-8e85-ff380c041442").unwrap(), tasks: vec![ Task { title: String::from("title 1"), is_done: true, }, Task { title: String::from("title 2"), is_done: true, }, Task { title: String::from("title 3"), is_done: false, }, ], state: State::Doing, } ) } }
Validando o Uuid
Nosso próximo passo é validar que o formato enviado é um Uuid
. Para isso criaremos um teste que faz um request com um formato aleatório de dado e retorna BadRequest
com a mesagem que "id deve ser um Uuid".
#![allow(unused)] fn main() { #[actix_rt::test] async fn test_todo_card_without_uuid() { dotenv().ok(); let mut app = test::init_service(App::new().data(Clients::new()).configure(app_routes)).await; let req = test::TestRequest::with_uri("/api/show/fake-uuid").to_request(); let resp = test::read_response(&mut app, req).await; let message = String::from_utf8(resp.to_vec()).unwrap(); assert_eq!(&message, "Id must be a Uuid::V4"); } }
Para resolver este teste a implementação de código é bastante simples, basta adicioanrmos um if
que verifica se o parse_str
é do tipo Err
e em caso de true
retornar HttpResponse::BadRequest().body("Id must be a Uuid::V4")
. Assim, nossa função ficou da seguinte forma:
#![allow(unused)] fn main() { pub async fn show_by_id(id: web::Path<String>, state: web::Data<Clients>) -> impl Responder { let uuid = id.to_string(); if uuid::Uuid::parse_str(&uuid).is_err() { return HttpResponse::BadRequest().body("Id must be a Uuid::V4"); } match get_todo_by_id(uuid, state.dynamo.clone()) { None => { error!("Failed to read todo cards"); HttpResponse::NotFound().finish() } Some(todo_id) => HttpResponse::Ok().content_type("application/json") .json(todo_id) } } }
Atualizando TodoCards
Agora vamos aprender como atualizar as informações de uma TodoCard
no DynamoDB. Vamos focar em atualizar somente dois atributos description
e state
, depois discutiremos estratégias para implementar updates em tasks
, pois os outros argumentos são essencialmente iguais a description
e state
. Agora precisamos definir como será nosso endpoint de atualização, para isso podemos definir sua rota como /api/update/{id}
e responderá via método PUT
. Assim, nosso body conterá os campos state
e/ou description
, como no exemplo de put_todo.json
:
{
"state": "Doing",
"description": "dfwgferf"
}
Um teste para esse cenário seria o seguinte:
#![allow(unused)] fn main() { // tests/test_api_web/controller.rs // ... #[cfg(test)] mod update { use actix_web::{test, App, http::StatusCode}; use dotenv::dotenv; use todo_server::todo_api_web::model::{ http::Clients, }; use todo_server::todo_api_web::routes::app_routes; use crate::helpers::{read_json}; #[actix_rt::test] async fn test_todo_card_by_id() { dotenv().ok(); let mut app = test::init_service(App::new().data(Clients::new()).configure(app_routes)).await; let req = test::TestRequest::put() .uri("/api/update/544e3675-19f5-4455-9ed9-9ccc577f70fe") .header("Content-Type", "application/json") .set_payload(read_json("put_todo.json").as_bytes().to_owned()) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::OK); } } }
Criando a Rota
Temos nosso teste, mas agora precisamos criar a rota em src/todo_api_web/routes.rs
seguindo o padrão PUT
na rota /api/update/{id}
:
#![allow(unused)] fn main() { use crate::todo_api_web::controller::{ // ... todo::{create_todo, show_all_todo, show_by_id, update_todo}, }; pub fn app_routes(config: &mut web::ServiceConfig) { config.service( web::scope("/") .service( web::scope("api/") .route("create", web::post().to(create_todo)) .route("index", web::get().to(show_all_todo)) .route("show/{id}", web::get().to(show_by_id)) .route("update/{id}", web::put().to(update_todo)), ) // ... ); } }
Agora, precisamos implementar o controller update_todo
em src/todo_api_web/controller/todo.rs
:
#![allow(unused)] fn main() { pub async fn update_todo( id: web::Path<String>, info: web::Json<TodoCardUpdate>, state: web::Data<Clients>) -> impl Responder { let uuid = id.to_string(); if uuid::Uuid::parse_str(&uuid).is_err() { return HttpResponse::BadRequest().body("Id must be a Uuid::V4"); } match update_todo_info(uuid, info.into_inner(), state.dynamo.clone()) { true => HttpResponse::Ok().finish(), false => HttpResponse::NotFound().finish() } } }
Os argumentos para a função update_todo
são id
que vem da rota da url {id}
com web::Path<String>
, info
que corresponde ao corpo do PUT
do tipo web::Json<TodoCardUpdate>
e o state
que vem do estao da aplicação com web::Data<Clients>
. Primeiro passo é converter o campo id
em String
com to_string
para validar se essa string é um Uuid
com uuid::Uuid::parse_str(&uuid)
e retornar um HttpResponse::BadRequest().body("Id must be a Uuid::V4")
caso o resultado de parse_str
seja do tipo Err
:
#![allow(unused)] fn main() { let uuid = id.to_string(); if uuid::Uuid::parse_str(&uuid).is_err() { return HttpResponse::BadRequest().body("Id must be a Uuid::V4"); } }
Depois disso, chamamos a função update_todo_info
que retorna um booleano para aplicarmos pattern matching em true
, retornando HttpResponse::Ok().finish()
, ou em false
, retornando HttpResponse::NotFound().finish()
. A função update_todo_info
está localizada em src/todo_api/db/todo.rs
e é bastante extensa:
#![allow(unused)] fn main() { #[cfg(not(feature = "dbtest"))] pub fn update_todo_info(id: String, info: TodoCardUpdate, client: DynamoDbClient) -> bool { use rusoto_dynamodb::{AttributeValue, DynamoDb}; use std::collections::HashMap; let expression = adapter::update_expression(&info); let attribute_values = adapter::expression_attribute_values(&info); let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(id); _map.insert(String::from("id"), attr); let update = UpdateItemInput { table_name: TODO_CARD_TABLE.to_string(), key: _map, update_expression: expression, expression_attribute_values: attribute_values, ..UpdateItemInput::default() }; match client.update_item(update).sync() { Ok(_) => true, Err(e) => { error!("failed due to {:?}", e); false } } } }
A primeira coisa que precisamos ressaltar neste código é o UpdateItemInput
, que é a struct responsável por executar a atualização da todo
com o id
enviado na rota. Os campos necessários são table_name
, que é o nome da tabela, key
que é um AttributeValue
com todos os valores de key
, no nosso caso é somente id
, update_expression
que define quais argumentos serão atualizados através do adapter adapter::update_expression
, expression_attribute_values
que contém os argumentos para atualizar as informações através do adapter::expression_attribute_values
que transforma os valores de TodoCardUpdate
em um HashMap<String, AttributeValue>
. Assim, para transformar o id
em um HashMap<String, AttributeValue>
podemos utilizar a seguinte lógica:
#![allow(unused)] fn main() { let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(id); _map.insert(String::from("id"), attr); }
A função para executar a atualização no Dynamo é update_item
, lembre-se que após o sync
o resultado é do tipo Result
, por isso do match
. Já os adapter são os seguintes:
#![allow(unused)] fn main() { // src/todo_api/adapter/mod.rs // ... pub fn update_expression(info: &TodoCardUpdate) -> Option<String> { let data = info.clone(); match (data.description, data.state) { (Some(_), Some(_)) => Some(String::from("SET description = :d, state_db = :s")), (_, Some(_)) => Some(String::from("SET state_db = :s")), (Some(_), _) => Some(String::from("SET description = :d")), _ => None } } pub fn expression_attribute_values(info: &TodoCardUpdate) -> Option<HashMap<String, AttributeValue>> { let data = info.clone(); match (data.description, data.state) { (Some(desc), Some(state)) => { let mut _map = HashMap::new(); let mut attr_d = AttributeValue::default(); attr_d.s = Some(String::from(desc)); let mut attr_s = AttributeValue::default(); attr_s.s = Some(String::from(state.to_string())); _map.insert(String::from(":d"), attr_d); _map.insert(String::from(":s"), attr_s); Some(_map) }, (_, Some(state)) => { let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(String::from(state.to_string())); _map.insert(String::from(":s"), attr); Some(_map) }, (Some(desc), _) => { let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(String::from(desc)); _map.insert(String::from(":d"), attr); Some(_map) }, _ => None } } }
update_expression
é responsável pro criar a expressão que vai determinar o que será atualizado. Como recebemos 2 campos Optional
, description
e state
, temos 4 possibilidades:
- Ambos existem retorna
"SET description = :d, state_db = :s")
. - Somente
state
existe retorna"SET state_db = :s"
. - Somente
description
existe retorna"SET description = :d"
. - Nenhum retorna um
None
.
Os testes para update_expression
são os seguintes:
#![allow(unused)] fn main() { #[cfg(test)] mod update_expression_test { use super::update_expression; use crate::todo_api_web::model::todo::{State, TodoCardUpdate}; #[test] fn description_and_state() { let todo_update = TodoCardUpdate {description: Some("haiushdusd".to_string()), state: Some(State::Doing)}; let expected = Some(String::from("SET description = :d, state_db = :s")); assert_eq!(expected, update_expression(&todo_update)); } #[test] fn description() { let todo_update = TodoCardUpdate {description: Some("haiushdusd".to_string()), state: None}; let expected = Some(String::from("SET description = :d")); assert_eq!(expected, update_expression(&todo_update)); } #[test] fn state() { let todo_update = TodoCardUpdate {description: None, state: Some(State::Doing)}; let expected = Some(String::from("SET state_db = :s")); assert_eq!(expected, update_expression(&todo_update)); } #[test] fn none() { let todo_update = TodoCardUpdate {description: None, state: None}; let expected = None; assert_eq!(expected, update_expression(&todo_update)); } } }
Já expression_attribute_values
é um pouco mais complicada pois deve retornar um Option<HashMap<String, AttributeValue>>
, mas as regras de pattern matching são as mesmas. Assim vamos entender o caso que existe tanto description
quanto state
. Para update_expression
não nos interessava o conteúdo da expression, assim utilizavamos Some(_)
para fazer pattern matching, porém em expression_attribute_values
eles interessam já que será inseridos dentro do HashMap
. A primeira cosia que devemos fazer é criar um HashMap
com let mut _map = HashMap::new();
e determinar os AttributeValue
para state
e para description
, let mut attr_s = AttributeValue::default();
e let mut attr_d = AttributeValue::default();
respectivamente. Depois disso, inserimos o conteúdo de state
e de description
no campo s
, de String, através de attr_d.s
, attr_s.s = Some(String::from(state.to_string()));
e attr_d.s = Some(String::from(desc));
. Inserimos estes valores no mapa com _map.insert(String::from(":d"), attr_d); _map.insert(String::from(":s"), attr_s);
e retornamos seu valor em Some(_map)
. A função para teste é a seguinte:
#![allow(unused)] fn main() { #[cfg(feature = "dbtest")] pub fn update_todo_info(id: String, info: TodoCardUpdate, client: DynamoDbClient) -> bool { use rusoto_dynamodb::{AttributeValue, DynamoDb}; use std::collections::HashMap; let expression = adapter::update_expression(&info); let attribute_values = adapter::expression_attribute_values(&info); let mut _map = HashMap::new(); let mut attr = AttributeValue::default(); attr.s = Some(id); _map.insert(String::from("id"), attr); let update = UpdateItemInput { table_name: TODO_CARD_TABLE.to_string(), key: _map, update_expression: expression, expression_attribute_values: attribute_values, ..UpdateItemInput::default() }; true } }
Agora vamos entender como nosso código mudaria para incluir os outros campos de atualização.
Atualizando outros campos
Considerando que a struct que temos no banco de dados é a seguinte e que o campo id
não será atualizado, podemos discutir como adicionar title
, owner
e tasks
:
#![allow(unused)] fn main() { pub struct TodoCard { pub id: Option<Uuid>, pub title: String, pub description: String, pub owner: Uuid, pub tasks: Vec<Task>, pub state: State, } }
Bom, title
e owner
são bastante triviais, pois bastaria expandir nossos adapters para lidarem com mais duas strings, modificando nossa struct TodoCardUpdate
para:
#![allow(unused)] fn main() { pub struct TodoCardUpdate { pub description: Option<String>, pub state: Option<State>, pub title: Option<String>, pub owner: Option<Uuid> } }
Já o adapter update_expression
ficaria semelhante ao seguinte:
#![allow(unused)] fn main() { pub fn update_expression(info: &TodoCardUpdate) -> Option<String> { let data = info.clone(); match (data.description, data.state, data.title, data.owner) { (Some(_), Some(_), Some(_), Some(_)) => Some(String::from("SET description = :d, state_db = :s, title = :t, owner = :o")), ... (Some(_), Some(_), _, _) => Some(String::from("SET description = :d, state_db = :s")), (_, Some(_), Some(_), _) => Some(String::from("SET title = :t, state_db = :s")), (_, _, Some(_), Some(_)) => Some(String::from("SET title = :t, owner = :o")), (Some(_), _, _, Some(_)) => Some(String::from("SET description = :d, owner = :o")), ... (_, Some(_), _, _) => Some(String::from("SET state_db = :s")), (Some(_), _, _, _) => Some(String::from("SET description = :d")), (_, _, Some(_), _) => Some(String::from("SET title = :t")), (_, _, _, Some(_)) => Some(String::from("SET owner = :o")), _ => None } } }
Acredito que esta solução pode ficar um pouco verbosa, assim, uma ideia seria transformar esses 4 campos em um vetor e iterar nele de forma posicional, o que não geraria uma solução muito elegante também, mas seria muito útil para o caso de expression_attribute_values
, como o pseudo código a seguir:
#![allow(unused)] fn main() { // pseudo código pub fn expression_attribute_values(info: &TodoCardUpdate) -> Option<HashMap<String, AttributeValue>> { let data = info.clone(); let mut _map = HashMap::new(); let data_vec = vec![data.description, data.state, data.title, data.owner]; data_vec.iter() .map(|i| if i.is_some() { let mut attr = AttributeValue::default(); attr.s = Some(String::from(i)); attr } else { None }) .enumerate(|(idx, item)| match idx { 0 => (":d".to_string(), item), 1 => (":s".to_string(), item), 2 => (":t".to_string(), item), 3 => (":o".to_string(), item), _ => ("".to_string(), None) }) .fold(_map,|acc, i| if i.is_some() { acc.insert(i.0, i.1) }; acc); Some(_map) } }
Tasks
Agora precisamos discutir tasks
, elas são mais complicadas pois não criamos o conceito de id
nelas, assim a solução que eu creio ser mais simples para lidar com elas é criar uma struct que contém três argumentos is_bool
, previous_text
, new_text
. O campo is_bool
é equivalente ao da struct Task
, já o argumento previous_text
é o argumento que identifica qual o texto existente de Task
no banco, e new_text
é o texto que queremos atualizar. Para entender como ficaria a adição, a atualização e o remoção teremos o seguinte:
- Adicão:
previous_text = None
,new_text = Some
. - Atualização:
previous_text = Some
,new_text = Some
. - Remoção:
previous_text = Some
,new_text = None
.
#![allow(unused)] fn main() { pub struct TaskUpdate { pub is_bool: bool, pub previous_text: Option<String>, pub new_text: Option<String>, } }
Portanto, quando identificarmos que previous_text
não existe, criamos uma nova task
, e quando identificarmos que new_text
não existe, deletamos a task
com o texto da previous_text
. Já a atualização filtramos todas as tasks que contém o previous_text
com new_text
, assim se ambos são iguais atualizamos somente is_bool
e em caso de não existir uma task com previous_text
, simplesmente criamos uma nova new_text
. Isso poderia ser feito em endpoint que responde a um POST
em /api/update/{id}/tasks
.
Fica como um bom desafio fazer estas mudanças que discutimos aqui antes de seguir para a próxima parte, assim como criar um endpoint de DELETE
. Nesta parte aprendemos a criar um serviço REST
com actix que cria e gerencia tarefas via create
, update
, show
e index
, salvando estas informações em um DynamoDB. Além disso, criamos um middleware de autenticação e endpoints de autenticação, via diesel. Outros middlewares que utilizamos foi o Logger
, que infelizmente não funciona com dotenv
, necessária para o Logger
, e um middleware que cria o header x-request-id
. Aprendemos a gerenciar o estado da aplicação com .data()
e a configurar rotas com .configure()
. Por último, aprendemos a tornar nosso sistema tolerante a falhas e a configurar o docker com todas as dependências.
Agora vamos aprender a utilizar graphql com Actix para fazer um sistema de busca de rotas de voos.