Rust API - Criando a rota de update by id com rust - Part VII


Não vamos perder tempo, no nosso schema.rs, vamos escrever o UpdateSchema da nossa aplicação para trabalhar o nosso update o nosso vai se chamar UpdateTaskSchema:

#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateTaskSchema {
    pub title: Option<String>,
    pub content: Option<String>,
}

Na nossa service, vamos importar pelo nosso actix_web o método path junto ao nosso actix_web:

use actix_web::{
    get,
    post,
    delete,
    patch,
    web::{
        Data,
        Json,
        scope,
        Query,
        Path,
        ServiceConfig
    },
    HttpResponse,
    Responder
};

E vamos começar a desenvolver o nosso update_task_by_id:

#[patch("/tasks/{id}")]
async fn update_task_by_id(
    path: Path<uuid::Uuid>,
    body: Json<UpdateTaskSchema>,
    data: Data<AppState>
) -> impl Responder {

}

Observe que nosso path a gente está em busca do nosso uuid, porém agora temos um body com um dado novo a ser atualizado.


#[patch("/tasks/{id}")]
async fn update_task_by_id(
    path: Path<uuid::Uuid>,
    body: Json<UpdateTaskSchema>,
    data: Data<AppState>
) -> impl Responder {
  let task_id = path.into_inner();

}

Próximo ponto é fazer a nossa query e o tratamento de erro, o que vai ser um pouco diferente frente ao que fizemos até então, pois primeiro precisamos fazer uma busca por id e depois fazer um update por id frente ao nosso content ou title.

Então o primeiro passo, criar o nosso select:

#[patch("/tasks/{id}")]
async fn update_task_by_id(
    path: Path<uuid::Uuid>,
    body: Json<UpdateTaskSchema>,
    data: Data<AppState>
) -> impl Responder {
    let task_id = path.into_inner();

    match
        sqlx
            ::query_as!(TaskModel, "SELECT * FROM tasks WHERE id = $1", task_id)
            .fetch_one(&data.db).await
    {
        Ok(task) => {
          //...calma que vamos voltar aqui
        }
        Err(err) => {
            let message = format!("Internal server error: {:?}", err);
            return HttpResponse::NotFound().json(
                serde_json::json!({"status": "fail","message": message})
            );
        }
    }
}

E agora, vamos trabalhar o retorno da nossa aplicação fazendo o update de fato, seja de title ou do nosso content:

#[patch("/tasks/{id}")]
async fn update_task_by_id(
    path: Path<uuid::Uuid>,
    body: Json<UpdateTaskSchema>,
    data: Data<AppState>
) -> impl Responder {
    let task_id = path.into_inner();

    match
        sqlx
            ::query_as!(TaskModel, "SELECT * FROM tasks WHERE id = $1", task_id)
            .fetch_one(&data.db).await
    {
        Ok(task) => {
            match
                sqlx
                    ::query_as!(
                        TaskModel,
                        "UPDATE tasks SET title = $1, content = $2 WHERE id = $3 RETURNING *",
                        body.title.to_owned().unwrap_or(task.title),
                        body.content.to_owned().unwrap_or(task.content),
                        task_id
                    )
                    .fetch_one(&data.db)
                    .await
                        {
                            Ok(task) => {
                                let task_response = json!({
                                    "status" : "success",
                                    "task" : task
                                });

                                return HttpResponse::Ok().json(task_response)
                            }
                            Err(error) => {
                                let message = format!("{:?}", error);
                                return HttpResponse::InternalServerError().json(json!({
                                    "status" : "error",
                                    "message" : message
                                }))
                            }
                        }
        }
        Err(err) => {
            let message = format!("{:?}", error);
            return HttpResponse::NotFound().json(json!({
                "status" : "not found",
                "message" :  message
            }))
        }
    }
}

Por fim, adicionar nossa nova função de update junto a nossa config

pub fn config(conf: &mut ServiceConfig) {
    let scope = scope("/api")
        .service(health_checker)
        .service(create_task)
        .service(get_all_tasks)
        .service(get_task_by_id)
        .service(delete_task_by_id)
        .service(update_task_by_id);

    conf.service(scope);
}

O nosso arquivo no final fica assim:

  src/services.rs
use actix_web::{
    web::{
        scope,
        Json,
        Path,
        Data,
        ServiceConfig,
        Query
    },
    get,
    post,
    delete,
    patch,
    HttpResponse,
    Responder,
};

use serde_json::json;
use uuid::Uuid;

use crate::{schema::{CreateTaskSchema, FilterOptions, UpdateTaskSchema}, model::TaskModel, AppState};



#[get("/healthchecker")]
async fn health_checker() -> impl Responder {
    const MESSAGE: &str = "Health check: API is up and running smoothly.";

    HttpResponse::Ok().json(json!({
        "status": "success",
        "message": MESSAGE
    }))
}

#[post("/task")]
async fn create_task(
    body: Json<CreateTaskSchema>,
    data: Data<AppState>
) -> impl Responder {

    match
        sqlx::query_as!(
            TaskModel,
            "INSERT INTO tasks (title, content) VALUES ($1, $2)
            RETURNING * ",
            body.title.to_string(),
            body.content.to_string()
        )
        .fetch_one(&data.db)
        .await {
            Ok(task) => {
                let note_response = json!({
                    "status": "success",
                    "task": json!({
                        "task": task,
                    })
                });

                return HttpResponse::Ok().json(note_response);
            }
            Err(error) => {

                return HttpResponse::InternalServerError().json(
                    json!({
                        "status": "error",
                        "message": format!("{:?}", error)
                    })
                )
            }

        }

}


#[get("/tasks")]
async fn get_all_tasks(
    opts: Query<FilterOptions>,
    data: Data<AppState>

) -> impl Responder {
    let limit = opts.limit.unwrap_or(10);
    let offset = (opts.page.unwrap_or(1)- 1) * limit;


    match
        sqlx::query_as!(
        TaskModel,
            "SELECT * FROM tasks ORDER by id LIMIT $1 OFFSET $2",
            limit as i32,
            offset as i32,
        )
        .fetch_all(&data.db)
        .await {
            Ok(tasks) => {
                let json_response = json!({
                    "status": "success",
                    "result":  tasks.len(),
                    "tasks": tasks
                });

                return HttpResponse::Ok().json(json_response);
            }
            Err(error) => {

                return HttpResponse::InternalServerError().json(
                    json!({
                        "status": "error",
                        "message": format!("{:?}", error)
                    })
                )
            }
        }


}


#[get("/tasks/{id}")]
async fn get_task_by_id(
    path: Path<Uuid>,
    data: Data<AppState>
) -> impl Responder {
    let task_id = path.into_inner();

    match
        sqlx::query_as!(
            TaskModel,
            "SELECT * FROM tasks WHERE id = $1", task_id
        )
        .fetch_one(&data.db)
        .await {
            Ok(task) => {
                let task_note = json!({
                    "status": "success",
                    "task": task
                });


               return HttpResponse::Ok().json(task_note);
            }

            Err(error) => {

                return HttpResponse::InternalServerError().json(
                    json!({
                        "status": "error",
                        "message": format!("{:?}", error)
                    })
                )
            }


        }


}

#[delete("/tasks/{id}")]
async fn delete_task_by_id(
    path: Path<Uuid>,
    data: Data<AppState>
) -> impl Responder {
    let task_id = path.into_inner();

    match
        sqlx::query_as!(
            TaskModel,
            "DELETE FROM tasks WHERE id = $1", task_id
        )
        .execute(&data.db)
        .await {
            Ok(_) => {
                return HttpResponse::NoContent().finish();
            }

            Err(error) => {

                return HttpResponse::InternalServerError().json(
                    json!({
                        "status": "error",
                        "message": format!("{:?}", error)
                    })
                )
            }
        }
}



#[patch("/tasks/{id}")]
async fn update_task_by_id(
    path: Path<Uuid>,
    body: Json<UpdateTaskSchema>,
    data: Data<AppState>
) -> impl Responder {
    let task_id = path.into_inner();

    match
        sqlx::query_as!(
            TaskModel,
            "SELECT * FROM tasks WHERE id = $1",
            task_id
        )
        .fetch_one(&data.db)
        .await {
            Ok(task) => {

                match
                    sqlx::query_as!(
                        TaskModel,
                        "UPDATE tasks SET title = $1, content = $2 WHERE id = $3 RETURNING *",
                        body.title.to_owned().unwrap_or(task.title),
                        body.content.to_owned().unwrap_or(task.content),
                        task_id
                    )
                    .fetch_one(&data.db)
                    .await {
                        Ok(task) => {
                            let task_response = json!({
                                "status" : "success",
                                "task" : task
                            });

                            return HttpResponse::Ok().json(task_response)
                        }
                        Err(error) => {
                            let message = format!("{:?}", error);
                            return HttpResponse::InternalServerError().json(json!({
                                "status" : "error",
                                "message" : message
                            }))

                        }
                    }


            }
            Err(error) => {
                let message = format!("{:?}", error);
                return HttpResponse::NotFound().json(json!({
                    "status" : "not found",
                    "message" :  message
                }))
            }
        }
}



pub fn config(conf:  &mut ServiceConfig) {
    let scope = scope("/api")
            .service(health_checker)
            .service(create_task)
            .service(get_all_tasks)
            .service(get_task_by_id)
            .service(delete_task_by_id)
            .service(update_task_by_id);



    conf.service(scope);
}

Para testar, nós iremos criar uma nova task (again):

curl --request POST \
  --url http://localhost:8080/api/task \
  --header 'Content-Type: application/json' \
  --data '{
	"title": "title test2",
	"content": "content test"
}'

Ele deve me gerar essa task onde eu irei pegar o nosso id:

{
  "status": "success",
  "task": {
    "task": {
      "content": "content test",
      "created_at": "2023-06-10T16:35:58.187917Z",
      "id": "a754b8d0-fa0e-4912-86b8-f7158d177c50",
      "title": "title test2"
    }
  }
}

E vamos através do id, atualizar o body:

curl --request PATCH \
  --url http://localhost:8080/api/tasks/a754b8d0-fa0e-4912-86b8-f7158d177c50 \
  --header 'Content-Type: application/json' \
  --data '{
	"title": "title hey jude",
	"content": "content hey jude"
}'

E concluimos:

{
   "data":{
      "task":{
         "content":"title hey jude",
         "created_at":"2023-06-10T16:35:58.187917Z",
         "id":"a754b8d0-fa0e-4912-86b8-f7158d177c50",
         "title":"title hey jude"
      }
   },
   "status":"success"
}

Próximos passos, logs…que a vida sem rumo é triste kk Grande abraço e até mais =]