From a226adcd0f5e3abd524575d40d510b1cac5d949a Mon Sep 17 00:00:00 2001 From: phoenix Date: Sun, 14 Jun 2026 23:25:35 -0400 Subject: [PATCH] Get contact (#10) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/10 --- .gitea/workflows/workflow.yml | 5 + Cargo.lock | 2 +- Cargo.toml | 2 +- src/caller/contact.rs | 198 ++++++++++++++++++++++++++++++++++ src/caller/mod.rs | 130 +--------------------- src/config/mod.rs | 8 +- src/repo/mod.rs | 58 ++++++++++ tests/test.rs | 79 ++++++++++++++ 8 files changed, 353 insertions(+), 129 deletions(-) create mode 100644 src/caller/contact.rs diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index c9da54e..634616a 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -5,6 +5,11 @@ on: branches: - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: check: name: Check diff --git a/Cargo.lock b/Cargo.lock index 9961d48..d9a4ff5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,7 +2293,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.5" +version = "0.1.6" dependencies = [ "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index aaa2664..9d3c478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.5" +version = "0.1.6" edition = "2024" rust-version = "1.96" diff --git a/src/caller/contact.rs b/src/caller/contact.rs new file mode 100644 index 0000000..d1d296d --- /dev/null +++ b/src/caller/contact.rs @@ -0,0 +1,198 @@ +pub mod request { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + pub struct AddContactRequest { + pub phone_number: String, + pub firstname: Option, + pub lastname: Option, + pub nickname: Option, + pub user_id: uuid::Uuid, + } + + impl AddContactRequest { + pub fn is_valid(&self) -> bool { + !self.phone_number.is_empty() || !self.user_id.is_nil() + } + } + + #[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] + pub struct GetContactParams { + pub id: Option, + pub user_id: Option, + } +} + +pub mod response { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct AddContactResponse { + pub message: String, + pub data: Vec, + } + + pub use AddContactResponse as GetContactResponse; + + /* + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct GetContactResponse { + pub message: String, + pub data: Vec, + } + */ +} + +pub mod endpoint { + // use axum::{Json, response::IntoResponse}; + + use crate::repo::contact as contact_repo; + + /// Endpoint to create Contact + #[utoipa::path( + post, + path = super::super::endpoints::ADD_CONTACT, + request_body( + content = super::request::AddContactRequest, + description = "Data needed to create a Contact", + content_type = "application/json" + ), + responses( + (status = 201, description = "Contact created", body = super::response::AddContactResponse), + (status = 400, description = "Error", body = super::response::AddContactResponse), + (status = 500, description = "Error creating Contact", body = super::response::AddContactResponse) + ) +)] + pub async fn create_contact( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::AddContactResponse::default(); + + if payload.is_valid() { + match contact_repo::get_with_user_id_and_phone( + &pool, + &payload.user_id, + &payload.phone_number, + ) + .await + { + Ok(_) => { + println!("Already created"); + response.message = String::from("Already created"); + (axum::http::StatusCode::NOT_MODIFIED, axum::Json(response)) + } + Err(err) => { + match err { + sqlx::Error::RowNotFound => { + println!("Good to create"); + // Put code here + let mut contact = textsender_models::contact::Contact { + firstname: payload.firstname, + lastname: payload.lastname, + phone_number: payload.phone_number, + nickname: payload.nickname, + user_id: Some(payload.user_id), + ..Default::default() + }; + match contact_repo::insert(&pool, &contact).await { + Ok(id) => { + contact.id = Some(id); + response.message = + String::from(super::super::response::SUCCESSFUL); + response.data.push(contact); + + (axum::http::StatusCode::CREATED, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Contact not created"); + + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } + _ => { + eprintln!("Error: {err:?}"); + response.message = String::from("Something went wrong"); + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + + // Endpoint to get songs + #[utoipa::path( + get, + path = super::super::endpoints::GET_CONTACT, + params( + ("id" = uuid::Uuid, Path, description = "Id of Contact"), + ("user_id" = uuid::Uuid, Path, description = "User Id associated with the Contact") + ), + responses( + (status = 200, description = "Songs found", body = super::response::GetContactResponse), + (status = 400, description = "Error getting songs", body = super::response::GetContactResponse) + ) + )] + pub async fn get_contacts( + axum::Extension(pool): axum::Extension, + axum::extract::Query(params): axum::extract::Query, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::GetContactResponse::default(); + + let contacts = match params.id { + Some(id) => match contact_repo::get(&pool, &id).await { + Ok(contact) => { + vec![contact] + } + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => match params.user_id { + Some(user_id) => match contact_repo::get_with_user_id(&pool, &user_id).await { + Ok(contacts) => contacts, + Err(err) => { + eprintln!("Error: {err:?}"); + return ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ); + } + }, + None => { + response.message = String::from("Invalid parameter"); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)); + } + }, + }; + + response.data = contacts; + response.message = String::from(super::super::response::SUCCESSFUL); + + (axum::http::StatusCode::OK, axum::Json(response)) + } +} diff --git a/src/caller/mod.rs b/src/caller/mod.rs index 1c04d29..d0911be 100644 --- a/src/caller/mod.rs +++ b/src/caller/mod.rs @@ -1,8 +1,12 @@ +pub mod contact; + pub mod endpoints { pub const ROOT: &str = "/"; /// Constant for adding Contact endpoint pub const ADD_CONTACT: &str = "/api/v1/contact/new"; + /// Constant for getting Contact endpoint + pub const GET_CONTACT: &str = "/api/v1/contact"; } pub mod response { @@ -13,129 +17,3 @@ pub mod response { pub async fn root() -> &'static str { "Hello, World!" } - -pub mod contact { - pub mod request { - use serde::{Deserialize, Serialize}; - use utoipa::ToSchema; - - #[derive(Debug, Deserialize, Serialize, ToSchema)] - pub struct AddContactRequest { - pub phone_number: String, - pub firstname: Option, - pub lastname: Option, - pub nickname: Option, - pub user_id: uuid::Uuid, - } - - impl AddContactRequest { - pub fn is_valid(&self) -> bool { - !self.phone_number.is_empty() || !self.user_id.is_nil() - } - } - } - - pub mod response { - use serde::{Deserialize, Serialize}; - use utoipa::ToSchema; - - #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] - pub struct AddContactResponse { - pub message: String, - pub data: Vec, - } - } - - pub mod endpoint { - // use axum::{Json, response::IntoResponse}; - - use crate::repo::contact as contact_repo; - - /// Endpoint to create Contact - #[utoipa::path( - post, - path = super::super::endpoints::ADD_CONTACT, - request_body( - content = super::request::AddContactRequest, - description = "Data needed to create a Contact", - content_type = "application/json" - ), - responses( - (status = 201, description = "Contact created", body = super::response::AddContactResponse), - (status = 400, description = "Error", body = super::response::AddContactResponse), - (status = 500, description = "Error creating Contact", body = super::response::AddContactResponse) - ) - )] - pub async fn create_contact( - axum::Extension(pool): axum::Extension, - axum::Json(payload): axum::Json, - ) -> ( - axum::http::StatusCode, - axum::Json, - ) { - let mut response = super::response::AddContactResponse::default(); - - if payload.is_valid() { - match contact_repo::get_with_user_id_and_phone( - &pool, - &payload.user_id, - &payload.phone_number, - ) - .await - { - Ok(_) => { - println!("Already created"); - response.message = String::from("Already created"); - (axum::http::StatusCode::NOT_MODIFIED, axum::Json(response)) - } - Err(err) => { - match err { - sqlx::Error::RowNotFound => { - println!("Good to create"); - // Put code here - let mut contact = textsender_models::contact::Contact { - firstname: payload.firstname, - lastname: payload.lastname, - phone_number: payload.phone_number, - nickname: payload.nickname, - user_id: Some(payload.user_id), - ..Default::default() - }; - match contact_repo::insert(&pool, &contact).await { - Ok(id) => { - contact.id = Some(id); - response.message = - String::from(super::super::response::SUCCESSFUL); - response.data.push(contact); - - (axum::http::StatusCode::CREATED, axum::Json(response)) - } - Err(err) => { - eprintln!("Error: {err:?}"); - response.message = String::from("Contact not created"); - - ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - axum::Json(response), - ) - } - } - } - _ => { - eprintln!("Error: {err:?}"); - response.message = String::from("Something went wrong"); - ( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - axum::Json(response), - ) - } - } - } - } - } else { - response.message = String::from("Request body is not valid"); - (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) - } - } - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index 1bde5a1..ebded9a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,7 +10,7 @@ pub mod host { pub mod init { use std::time::Duration; - use axum::routing::post; + use axum::routing::{get, post}; use tower_http::timeout::TimeoutLayer; use utoipa::OpenApi; @@ -89,6 +89,12 @@ pub mod init { crate::auth::auth::, )), ) + .route( + crate::caller::endpoints::GET_CONTACT, + get(contact_endpoints::get_contacts).route_layer(axum::middleware::from_fn( + crate::auth::auth::, + )), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/mod.rs b/src/repo/mod.rs index fa0acf5..6bda712 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -119,4 +119,62 @@ pub mod contact { }, } } + + pub async fn get_with_user_id( + pool: &sqlx::PgPool, + user_id: &uuid::Uuid, + ) -> Result, sqlx::Error> { + match sqlx::query( + r#" + SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts" + WHERE + user_id = $1; + "#, + ) + .bind(user_id) + .fetch_all(pool) + .await + { + Ok(rows) => { + let mut contacts: Vec = Vec::new(); + for row in rows { + let id: uuid::Uuid = row.try_get("id")?; + + let phone_number: String = row.try_get("phone_number")?; + let user_id: uuid::Uuid = row.try_get("user_id")?; + let contact = textsender_models::contact::Contact { + id: Some(id), + firstname: match row.try_get("firstname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: {err:?}"); + None + } + }, + lastname: match row.try_get("lastname") { + Ok(val) => val, + Err(err) => { + eprintln!("Error: {err:?}"); + None + } + }, + nickname: match row.try_get("nickname") { + Ok(val) => val, + Err(err) => { + eprintln!( + "Error: nickname has no value for contacts record {err:?}" + ); + None + } + }, + phone_number, + user_id: Some(user_id), + }; + contacts.push(contact); + } + Ok(contacts) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } + } } diff --git a/tests/test.rs b/tests/test.rs index 257fe59..23592f9 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -188,6 +188,30 @@ mod request { .unwrap(); app.clone().oneshot(req).await } + + pub async fn get_contact( + app: &axum::Router, + id: &uuid::Uuid, + ) -> Result { + let uri = format!( + "{}?id={}", + textsender_api::caller::endpoints::GET_CONTACT, + id + ); + + let req = axum::http::Request::builder() + .method(axum::http::Method::GET) + .uri(uri) + .header(axum::http::header::CONTENT_TYPE, "application/json") + .header( + axum::http::header::AUTHORIZATION, + super::bearer_auth().await, + ) + .body(axum::body::Body::empty()) + .unwrap(); + + app.clone().oneshot(req).await + } } /// Test contact phone number @@ -228,3 +252,58 @@ async fn test_create_contact() { let _ = db_mgr::drop_database(&tm_pool, &db_name).await; } + +#[tokio::test] +async fn test_get_contact() { + let tm_pool = db_mgr::get_pool().await.unwrap(); + let db_name = db_mgr::generate_db_name().await; + + match db_mgr::create_database(&tm_pool, &db_name).await { + Ok(_) => { + println!("Success"); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + } + + let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); + db::migrations(&pool).await; + + let app = init::app(pool).await; + + // Send request + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + let contact = &resp.data[0]; + assert_eq!(true, contact.id.is_some(), "Missing Contact Id"); + let id = contact.id.unwrap(); + match request::get_contact(&app, &id).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::GetContactResponse, + >(response) + .await; + assert_eq!( + false, + resp.data.is_empty(), + "At least Contact record should have been returned" + ); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +}