diff --git a/Cargo.lock b/Cargo.lock index e21b4b5..19c3576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4877,7 +4877,10 @@ version = "1.0.0" dependencies = [ "actix-web", "chrono", + "derive_more", "dotenv", + "env_logger", + "log", "mysql 24.0.0", "serde", ] diff --git a/databases/mysql/Cargo.toml b/databases/mysql/Cargo.toml index cc6a203..a05b633 100644 --- a/databases/mysql/Cargo.toml +++ b/databases/mysql/Cargo.toml @@ -5,7 +5,10 @@ edition = "2021" [dependencies] actix-web = "4" -chrono = { version = "0.4.26", default-features = false, features = ["clock"] } +chrono = { version = "0.4.26", default-features = false, features = ["std", "clock"] } +derive_more = "0.99.7" dotenv = "0.15" +env_logger = "0.10" +log = "0.4" mysql = "24" serde = { version = "1", features = ["derive"] } diff --git a/databases/mysql/README.md b/databases/mysql/README.md index 48dc2b4..c790025 100644 --- a/databases/mysql/README.md +++ b/databases/mysql/README.md @@ -38,7 +38,7 @@ You may need to ensure that you are running the commands with the correct MySQL 1. Create tables in the database: - Directory "mysql\sql" contains below listed ".sql" files: + The `sql` directory contains below listed ".sql" files: - `bankdetails.sql` - `branch_details.sql` @@ -69,7 +69,7 @@ You may need to ensure that you are running the commands with the correct MySQL 1. Using a different terminal send an HTTP GET/POST requests to the running server: - Directory "mysql/apis" contains below listed API's files: + The `apis` directory contains below listed API's files: - `bank.txt` - `branch.txt` diff --git a/databases/mysql/src/main.rs b/databases/mysql/src/main.rs index bb8f2fd..f1ccf03 100644 --- a/databases/mysql/src/main.rs +++ b/databases/mysql/src/main.rs @@ -1,178 +1,10 @@ -mod db_layer; +use std::{env, io}; -use std::{env, str}; +use actix_web::{web, App, HttpServer}; -use actix_web::{get, post, web, App, HttpServer, Responder}; -use dotenv::dotenv; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize)] -struct BankData { - bank_name: String, - country: String, -} - -#[derive(Deserialize)] -struct BranchData { - branch_name: String, - location: String, -} - -#[derive(Deserialize)] -struct TellerData { - teller_name: String, - branch_name: String, -} - -#[derive(Deserialize)] -struct CustomerData { - customer_name: String, - branch_name: String, -} - -// output -#[derive(Serialize)] -pub struct ResponseStatus { - pub status_code: u8, - pub status_description: String, -} - -#[derive(Serialize)] -pub struct BankDetails { - pub bank_name: String, - pub country: String, -} - -#[derive(Serialize)] -pub struct BankResponseData { - pub status_code: u8, - pub status_description: String, - pub bank_data: Vec, -} - -#[derive(Serialize)] -pub struct BranchDetails { - pub branch_name: String, - pub location: String, -} - -#[derive(Serialize)] -pub struct BranchResponseData { - pub status_code: u8, - pub status_description: String, - pub branch_data: Vec, -} - -#[derive(Serialize)] -pub struct TellerDetails { - pub teller_name: String, - pub branch_name: String, -} - -#[derive(Serialize)] -pub struct TellerResponseData { - pub status_code: u8, - pub status_description: String, - pub teller_data: Vec, -} - -#[derive(Serialize)] -pub struct CustomerDetails { - pub customer_name: String, - pub branch_name: String, -} - -#[derive(Serialize)] -pub struct CustomerResponseData { - pub status_code: u8, - pub status_description: String, - pub customer_data: Vec, -} - -#[get("/")] -async fn index() -> impl Responder { - String::new() -} - -#[post("/bank")] -async fn add_bank(bank_data: web::Json, data: web::Data) -> impl Responder { - let bank_name = &bank_data.bank_name; - let _country = &bank_data.country; - - let response_data = db_layer::create_bank(&data, bank_name.to_string(), _country.to_string()); - - web::Json(response_data) -} - -#[post("/branch")] -async fn add_branch( - branch_data: web::Json, - data: web::Data, -) -> impl Responder { - let branch_name = &branch_data.branch_name; - let _location = &branch_data.location; - - let response_data = - db_layer::create_branch(&data, branch_name.to_string(), _location.to_string()); - - web::Json(response_data) -} - -#[post("/teller")] -async fn add_teller( - teller_data: web::Json, - data: web::Data, -) -> impl Responder { - let teller_name = &teller_data.teller_name; - let branch_name = &teller_data.branch_name; - - let response_data = - db_layer::create_teller(&data, teller_name.to_string(), branch_name.to_string()); - - web::Json(response_data) -} - -#[post("/customer")] -async fn add_customer( - customer_data: web::Json, - data: web::Data, -) -> impl Responder { - let customer_name = &customer_data.customer_name; - let branch_name = &customer_data.branch_name; - - let response_data = - db_layer::create_customer(&data, customer_name.to_string(), branch_name.to_string()); - - web::Json(response_data) -} - -#[get("/bank")] -async fn get_bank(data: web::Data) -> impl Responder { - let bank_response_data = db_layer::get_bank_data(&data); - - web::Json(bank_response_data) -} - -#[get("/branch")] -async fn get_branch(data: web::Data) -> impl Responder { - let branch_response_data = db_layer::get_branch_data(&data); - - web::Json(branch_response_data) -} - -#[get("/teller")] -async fn get_teller(data: web::Data) -> impl Responder { - let teller_response_data = db_layer::get_teller_data(&data); - - web::Json(teller_response_data) -} - -#[get("/customer")] -async fn get_customer(data: web::Data) -> impl Responder { - let customer_response_data = db_layer::get_customer_data(&data); - - web::Json(customer_response_data) -} +mod models; +mod persistence; +mod routes; fn get_conn_builder( db_user: String, @@ -190,62 +22,46 @@ fn get_conn_builder( } #[actix_web::main] -async fn main() { - // get env vars - dotenv().ok(); - let server_addr = env::var("SERVER_ADDR").expect("SERVER_ADDR is not set in .env file"); +async fn main() -> io::Result<()> { + // initialize environment + dotenv::dotenv().ok(); + + // initialize logger + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + log::info!("setting up app from environment"); + let db_user = env::var("MYSQL_USER").expect("MYSQL_USER is not set in .env file"); let db_password = env::var("MYSQL_PASSWORD").expect("MYSQL_PASSWORD is not set in .env file"); let db_host = env::var("MYSQL_HOST").expect("MYSQL_HOST is not set in .env file"); - let my_db_port = env::var("MYSQL_PORT").expect("MYSQL_PORT is not set in .env file"); + let db_port = env::var("MYSQL_PORT").expect("MYSQL_PORT is not set in .env file"); let db_name = env::var("MYSQL_DBNAME").expect("MYSQL_DBNAME is not set in .env file"); - let mut http_server_status = String::from("[info] ActixWebHttpServer - Listening for HTTP on "); - let db_port: u16 = match my_db_port.parse::() { - Ok(a) => a, - Err(_err) => 0, - }; + let db_port = db_port.parse().unwrap(); - http_server_status.push_str(&server_addr); + let builder = get_conn_builder(db_user, db_password, db_host, db_port, db_name); - let builder: mysql::OptsBuilder = - get_conn_builder(db_user, db_password, db_host, db_port, db_name); - let pool = match mysql::Pool::new(builder) { - Ok(pool) => pool, - Err(e) => { - println!("Failed to open DB connection. {:?}", e); - return; - } - }; + log::info!("initializing database connection"); + + let pool = mysql::Pool::new(builder).unwrap(); let shared_data = web::Data::new(pool); - let server = match HttpServer::new(move || { + log::info!("starting HTTP server at http://localhost:8080"); + + HttpServer::new(move || { App::new() .app_data(shared_data.clone()) - .service(index) - .service(add_bank) - .service(add_branch) - .service(add_teller) - .service(add_customer) - .service(get_bank) - .service(get_branch) - .service(get_teller) - .service(get_customer) + .service(routes::index) + .service(routes::add_bank) + .service(routes::add_branch) + .service(routes::add_teller) + .service(routes::add_customer) + .service(routes::get_bank) + .service(routes::get_branch) + .service(routes::get_teller) + .service(routes::get_customer) }) - .bind(server_addr) - { - Ok(s) => { - println!("{:?}", http_server_status); - s - } - Err(e) => { - println!("Failed to bind port. {:?}", e); - return; - } - }; - - match server.run().await { - Ok(_) => println!("Server exited normally."), - Err(e) => println!("Server exited with error: {:?}", e), - }; + .bind(("127.0.0.1", 8080))? + .run() + .await } diff --git a/databases/mysql/src/models.rs b/databases/mysql/src/models.rs new file mode 100644 index 0000000..155b78c --- /dev/null +++ b/databases/mysql/src/models.rs @@ -0,0 +1,83 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +pub struct BankData { + pub bank_name: String, + pub country: String, +} + +#[derive(Debug, Deserialize)] +pub struct BranchData { + pub branch_name: String, + pub location: String, +} + +#[derive(Debug, Deserialize)] +pub struct TellerData { + pub teller_name: String, + pub branch_name: String, +} + +#[derive(Debug, Deserialize)] +pub struct CustomerData { + pub customer_name: String, + pub branch_name: String, +} + +#[derive(Debug, Serialize)] +pub struct ResponseStatus { + pub status_code: u8, + pub status_description: String, +} + +#[derive(Debug, Serialize)] +pub struct BankDetails { + pub bank_name: String, + pub country: String, +} + +#[derive(Debug, Serialize)] +pub struct BankResponseData { + pub status_code: u8, + pub status_description: String, + pub bank_data: Vec, +} + +#[derive(Debug, Serialize)] +pub struct BranchDetails { + pub branch_name: String, + pub location: String, +} + +#[derive(Debug, Serialize)] +pub struct BranchResponseData { + pub status_code: u8, + pub status_description: String, + pub branch_data: Vec, +} + +#[derive(Debug, Serialize)] +pub struct TellerDetails { + pub teller_name: String, + pub branch_name: String, +} + +#[derive(Debug, Serialize)] +pub struct TellerResponseData { + pub status_code: u8, + pub status_description: String, + pub teller_data: Vec, +} + +#[derive(Debug, Serialize)] +pub struct CustomerDetails { + pub customer_name: String, + pub branch_name: String, +} + +#[derive(Debug, Serialize)] +pub struct CustomerResponseData { + pub status_code: u8, + pub status_description: String, + pub customer_data: Vec, +} diff --git a/databases/mysql/src/db_layer.rs b/databases/mysql/src/persistence.rs similarity index 89% rename from databases/mysql/src/db_layer.rs rename to databases/mysql/src/persistence.rs index 0f463b5..6153e77 100644 --- a/databases/mysql/src/db_layer.rs +++ b/databases/mysql/src/persistence.rs @@ -1,47 +1,54 @@ +use derive_more::{Display, Error, From}; use mysql::{params, prelude::*}; -use crate::{ +use crate::models::{ BankDetails, BankResponseData, BranchDetails, BranchResponseData, CustomerDetails, CustomerResponseData, ResponseStatus, TellerDetails, TellerResponseData, }; const ERROR_MESSAGE: &str = "Error occurred during processing, please try again."; -pub fn create_bank(pool: &mysql::Pool, bank_name: String, _country: String) -> ResponseStatus { - let my_status_code: u8 = 1; - let my_status_description = ERROR_MESSAGE.to_owned(); +#[derive(Debug, Display, Error, From)] +pub enum PersistenceError { + EmptyBankName, + EmptyCountry, + EmptyBranch, + EmptyLocation, + EmptyTellerName, + EmptyCustomerName, + MysqlError(mysql::Error), - let mut response_status = ResponseStatus { - status_code: my_status_code, - status_description: my_status_description, - }; - - if bank_name.replace(' ', "").trim().is_empty() { - response_status.status_description = String::from("Bank name is empty!"); - return response_status; - } - - if _country.replace(' ', "").trim().is_empty() { - response_status.status_description = String::from("Country is empty!"); - return response_status; - } - - match pool.get_conn().and_then(|mut conn| { - insert_bank_data(&mut conn, bank_name.to_lowercase(), _country.to_lowercase()) - }) { - Ok(x) => { - if x > 0 { - response_status.status_code = 0; - response_status.status_description = "Successful".to_owned(); - } - } - Err(err) => println!("Failed to open DB connection. create_bank {err:?}"), - } - - response_status + Unknown, } -pub fn create_branch(pool: &mysql::Pool, branch_name: String, _location: String) -> ResponseStatus { +impl actix_web::ResponseError for PersistenceError {} + +pub fn create_bank( + pool: &mysql::Pool, + bank_name: String, + country: String, +) -> Result<(), PersistenceError> { + if bank_name.replace(' ', "").trim().is_empty() { + return Err(PersistenceError::EmptyBankName); + } + + if country.replace(' ', "").trim().is_empty() { + return Err(PersistenceError::EmptyCountry); + } + + let mut conn = pool.get_conn()?; + + let rows_inserted = + insert_bank_data(&mut conn, bank_name.to_lowercase(), country.to_lowercase())?; + + if rows_inserted > 0 { + return Ok(()); + } else { + Err(PersistenceError::Unknown) + } +} + +pub fn create_branch(pool: &mysql::Pool, branch_name: String, location: String) -> ResponseStatus { let my_status_code: u8 = 1; let my_status_description = ERROR_MESSAGE.to_owned(); @@ -55,7 +62,7 @@ pub fn create_branch(pool: &mysql::Pool, branch_name: String, _location: String) return response_status; } - if _location.replace(' ', "").trim().is_empty() { + if location.replace(' ', "").trim().is_empty() { response_status.status_description = String::from("Location is empty!"); return response_status; } @@ -64,7 +71,7 @@ pub fn create_branch(pool: &mysql::Pool, branch_name: String, _location: String) insert_branch_data( &mut conn, branch_name.to_lowercase(), - _location.to_lowercase(), + location.to_lowercase(), ) }) { Ok(x) => { diff --git a/databases/mysql/src/routes.rs b/databases/mysql/src/routes.rs new file mode 100644 index 0000000..c3429e7 --- /dev/null +++ b/databases/mysql/src/routes.rs @@ -0,0 +1,92 @@ +use actix_web::{get, post, web, HttpResponse, Responder}; + +use crate::{ + models::{BankData, BranchData, CustomerData, TellerData}, + persistence::{ + create_bank, create_branch, create_customer, create_teller, get_bank_data, get_branch_data, + get_customer_data, get_teller_data, + }, +}; + +#[get("/")] +pub(crate) async fn index() -> impl Responder { + String::new() +} + +#[post("/bank")] +pub(crate) async fn add_bank( + web::Json(bank_data): web::Json, + data: web::Data, +) -> actix_web::Result { + let bank_name = bank_data.bank_name; + let _country = bank_data.country; + + create_bank(&data, bank_name, _country)?; + + Ok(HttpResponse::NoContent()) +} + +#[post("/branch")] +pub(crate) async fn add_branch( + web::Json(branch_data): web::Json, + data: web::Data, +) -> actix_web::Result { + let branch_name = branch_data.branch_name; + let _location = branch_data.location; + + let response_data = create_branch(&data, branch_name, _location); + + Ok(web::Json(response_data)) +} + +#[post("/teller")] +pub(crate) async fn add_teller( + web::Json(teller_data): web::Json, + data: web::Data, +) -> actix_web::Result { + let teller_name = teller_data.teller_name; + let branch_name = teller_data.branch_name; + + let response_data = create_teller(&data, teller_name, branch_name); + + Ok(web::Json(response_data)) +} + +#[post("/customer")] +pub(crate) async fn add_customer( + web::Json(customer_data): web::Json, + data: web::Data, +) -> actix_web::Result { + let customer_name = customer_data.customer_name; + let branch_name = customer_data.branch_name; + + let response_data = create_customer(&data, customer_name, branch_name); + + Ok(web::Json(response_data)) +} + +#[get("/bank")] +pub(crate) async fn get_bank(data: web::Data) -> actix_web::Result { + let bank_response_data = get_bank_data(&data); + Ok(web::Json(bank_response_data)) +} + +#[get("/branch")] +pub(crate) async fn get_branch(data: web::Data) -> actix_web::Result { + let branch_response_data = get_branch_data(&data); + Ok(web::Json(branch_response_data)) +} + +#[get("/teller")] +pub(crate) async fn get_teller(data: web::Data) -> actix_web::Result { + let teller_response_data = get_teller_data(&data); + Ok(web::Json(teller_response_data)) +} + +#[get("/customer")] +pub(crate) async fn get_customer( + data: web::Data, +) -> actix_web::Result { + let customer_response_data = get_customer_data(&data); + Ok(web::Json(customer_response_data)) +}