Implement the http variant of the bank server.

During that process, many shortcomings with the socket server and the
bank lib were fixed.

I am aware a massive commit like this is not ideal.
This commit is contained in:
Sebastian Hugentobler 2022-03-18 19:35:34 +01:00
parent c69654a924
commit dac95b7dae
Signed by: shu
GPG Key ID: BB32CF3CA052C2F0
34 changed files with 1797 additions and 140 deletions

View File

@ -3,6 +3,7 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/bank/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/bank/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/http-server/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/socket-server/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/socket-server/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>

1074
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,5 +2,6 @@
members = [ members = [
"bank", "bank",
"socket-server" "http-server",
"socket-server",
] ]

78
bank/Cargo.lock generated
View File

@ -8,7 +8,7 @@ version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]] [[package]]
@ -23,20 +23,20 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi",
"libc", "libc",
"winapi", "winapi",
] ]
[[package]] [[package]]
name = "bank-server" name = "bank-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
"thiserror", "thiserror",
"uuid", "uuid",
] ]
[[package]] [[package]]
@ -51,11 +51,11 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [ dependencies = [
"atty", "atty",
"humantime", "humantime",
"log", "log",
"regex", "regex",
"termcolor", "termcolor",
] ]
[[package]] [[package]]
@ -64,9 +64,9 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi",
] ]
[[package]] [[package]]
@ -75,7 +75,7 @@ version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
@ -84,7 +84,7 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
dependencies = [ dependencies = [
"quick-error", "quick-error",
] ]
[[package]] [[package]]
@ -99,7 +99,7 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]] [[package]]
@ -114,8 +114,8 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [ dependencies = [
"env_logger", "env_logger",
"log", "log",
] ]
[[package]] [[package]]
@ -124,7 +124,7 @@ version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]] [[package]]
@ -139,7 +139,7 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
@ -148,9 +148,9 @@ version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax", "regex-syntax",
] ]
[[package]] [[package]]
@ -165,9 +165,9 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-xid", "unicode-xid",
] ]
[[package]] [[package]]
@ -176,7 +176,7 @@ version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]] [[package]]
@ -185,7 +185,7 @@ version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
@ -194,9 +194,9 @@ version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
] ]
[[package]] [[package]]
@ -211,7 +211,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]
[[package]] [[package]]
@ -226,8 +226,8 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [ dependencies = [
"winapi-i686-pc-windows-gnu", "winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu",
] ]
[[package]] [[package]]
@ -242,7 +242,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
[[package]] [[package]]

View File

@ -1,19 +1,20 @@
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use anyhow::{bail, Result};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::account::AccountError::{Inactive, InvalidAmount, Overdraw};
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum AccountError { pub enum AccountError {
#[error("can not overdraw account")] #[error("can not overdraw account")]
Overdraw(), Overdraw,
#[error("account is inactive")] #[error("account is inactive")]
Inactive(), Inactive,
#[error("amount must be > 0")] #[error("amount must be > 0")]
InvalidAmount(), InvalidAmount,
#[error("account does not exist")]
NotFound,
#[error("account still has a balance")]
AccountNotZero,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -52,33 +53,35 @@ impl Hash for Account {
impl Account { impl Account {
#[cfg(test)] #[cfg(test)]
pub fn new() -> Self { pub fn new() -> Self {
Self { ..Default::default() } Self {
..Default::default()
}
} }
pub fn deposit(&mut self, amount: f64) -> Result<()> { pub fn deposit(&mut self, amount: f64) -> Result<f64, AccountError> {
self.check_account(amount)?; self.check_account(amount)?;
self.balance += amount; self.balance += amount;
Ok(()) Ok(self.balance)
} }
pub fn withdraw(&mut self, amount: f64) -> Result<()> { pub fn withdraw(&mut self, amount: f64) -> Result<f64, AccountError> {
self.check_account(amount)?; self.check_account(amount)?;
if self.balance - amount < 0 as f64 { if self.balance - amount < 0 as f64 {
bail!(Overdraw()); return Err(AccountError::Overdraw);
} }
self.balance -= amount; self.balance -= amount;
Ok(()) Ok(self.balance)
} }
fn check_account(&self, amount: f64) -> Result<()> { fn check_account(&self, amount: f64) -> Result<(), AccountError> {
if !self.is_active { if !self.is_active {
bail!(Inactive()); return Err(AccountError::Inactive);
} }
if amount < 0 as f64 { if amount < 0 as f64 {
bail!(InvalidAmount()); return Err(AccountError::InvalidAmount);
} }
Ok(()) Ok(())

View File

@ -1,38 +1,61 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::RwLock; use std::sync::{RwLock, RwLockReadGuard};
#[cfg(test)] #[cfg(test)]
use anyhow::Result; use anyhow::Result;
use crate::account::Account; use crate::account::{Account, AccountError};
#[derive(Default)]
pub struct Bank { pub struct Bank {
pub accounts: HashMap<String, RwLock<Account>>, pub accounts: HashMap<String, RwLock<Account>>,
} }
impl Bank { impl Bank {
pub fn new() -> Self { pub fn new() -> Self {
Self { accounts: HashMap::new() } Default::default()
}
pub fn account_action<F: Fn(&mut Account) -> Result<f64, AccountError>>(
bank: RwLockReadGuard<'_, Bank>,
nr: &str,
action: F,
) -> Result<f64, AccountError> {
match bank.accounts.get(nr) {
None => Err(AccountError::NotFound),
Some(account) => {
let mut account = account.write().unwrap();
action(&mut account)
}
}
} }
pub fn account_numbers(&self) -> HashSet<String> { pub fn account_numbers(&self) -> HashSet<String> {
self.accounts.iter() self.accounts
.iter()
.filter(|(_, acc)| acc.read().unwrap().is_active) .filter(|(_, acc)| acc.read().unwrap().is_active)
.map(|(_, acc)| acc.read().unwrap().number.clone()) .map(|(_, acc)| acc.read().unwrap().number.clone())
.collect() .collect()
} }
pub fn create_account(&mut self, owner: String) -> String { pub fn create_account(&mut self, owner: String) -> String {
let acc = Account { owner, ..Default::default() }; let acc = Account {
let number = acc.number.clone(); owner,
..Default::default()
};
let nr = acc.number.clone();
self.accounts.insert(acc.number.clone(), RwLock::new(acc)); self.accounts.insert(acc.number.clone(), RwLock::new(acc));
number nr
} }
#[cfg(test)] pub fn transfer(
pub fn transfer(&self, from: &mut Account, to: &mut Account, amount: f64) -> Result<()> { &self,
from: &mut Account,
to: &mut Account,
amount: f64,
) -> Result<(), AccountError> {
from.withdraw(amount)?; from.withdraw(amount)?;
to.deposit(amount)?; to.deposit(amount)?;
@ -83,4 +106,4 @@ mod tests {
assert_eq!(1, bank.account_numbers().len()); assert_eq!(1, bank.account_numbers().len());
} }
} }

View File

@ -1,2 +1,2 @@
pub mod account; pub mod account;
pub mod bank; pub mod bank;

16
http-server/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "http-server"
version = "0.1.0"
edition = "2021"
[dependencies]
bank = { path = "../bank" }
actix-web = "4.0.1"
anyhow = "1.0.55"
thiserror = "1.0.30"
log = "0.4.14"
pretty_env_logger = "0.4.0"
serde = { version = "1.0.136", features = ["derive"] }
[dev-dependencies]
serde_json = "1.0.79"

View File

@ -0,0 +1,29 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use bank::account::AccountError;
use bank::bank::Bank;
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[post("/{nr}/close")]
pub async fn route(info: web::Path<String>, data: web::Data<AppState>) -> Result<impl Responder> {
let nr = info.into_inner();
info!("closing account {}...", nr);
let bank = data.bank.read().unwrap();
match Bank::account_action(bank, &nr, |account| {
// TODO: make the error handling part of the passivate method
if account.balance > 0_f64 {
Err(AccountError::AccountNotZero)
} else if account.passivate() {
Ok(0_f64)
} else {
Err(AccountError::Inactive)
}
}) {
Err(e) => Err(HttpAccountError(e).into()),
Ok(_) => Ok(HttpResponse::Ok().finish()),
}
}

View File

@ -0,0 +1,29 @@
use actix_web::{post, web, Responder, Result};
use serde::{Deserialize, Serialize};
use crate::AppState;
#[derive(Serialize, Deserialize)]
pub struct OwnerData {
pub(crate) owner: String,
}
#[derive(Serialize)]
struct AccountCreated {
nr: String,
}
#[post("")]
pub async fn route(
form: web::Form<OwnerData>,
data: web::Data<AppState>,
) -> Result<impl Responder> {
let owner = &form.owner;
info!("creating new account with owner {}...", owner);
let mut bank = data.bank.write().unwrap();
let nr = bank.create_account(owner.clone());
info!("created account {}", nr);
Ok(web::Json(AccountCreated { nr }))
}

View File

@ -0,0 +1,31 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use serde::Serialize;
use bank::bank::Bank;
use crate::handlers::error::HttpAccountError;
use crate::handlers::AmountData;
use crate::AppState;
#[derive(Serialize)]
struct AccountCreated {
nr: String,
}
#[post("/{nr}/deposit")]
pub async fn route(
info: web::Path<String>,
form: web::Form<AmountData>,
data: web::Data<AppState>,
) -> Result<impl Responder> {
let nr = info.into_inner();
let amount = form.amount;
info!("depositing {} into account {}...", amount, nr);
let bank = data.bank.read().unwrap();
match Bank::account_action(bank, &nr, |account| account.deposit(amount)) {
Err(e) => Err(HttpAccountError(e).into()),
Ok(_) => Ok(HttpResponse::Ok().finish()),
}
}

View File

@ -0,0 +1,53 @@
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use serde::Serialize;
use thiserror::Error;
use bank::account::AccountError;
#[derive(Serialize)]
struct ErrorResponse {
code: u16,
message: String,
}
#[derive(Debug, Error)]
pub struct HttpAccountError(pub AccountError);
impl Display for HttpAccountError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.deref())
}
}
impl Deref for HttpAccountError {
type Target = AccountError;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ResponseError for HttpAccountError {
fn status_code(&self) -> StatusCode {
match self.deref() {
AccountError::NotFound => StatusCode::NOT_FOUND,
AccountError::InvalidAmount => StatusCode::BAD_REQUEST,
AccountError::Inactive => StatusCode::BAD_REQUEST,
AccountError::Overdraw => StatusCode::BAD_REQUEST,
AccountError::AccountNotZero => StatusCode::EXPECTATION_FAILED,
}
}
fn error_response(&self) -> HttpResponse {
let status_code = self.status_code();
let error_response = ErrorResponse {
code: status_code.as_u16(),
message: self.to_string(),
};
HttpResponse::build(status_code).json(error_response)
}
}

View File

@ -0,0 +1,44 @@
use std::ops::Deref;
use actix_web::{get, web, Responder, Result};
use serde::Serialize;
use bank::account::{Account, AccountError};
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[derive(Serialize)]
pub struct JsonAccount {
pub number: String,
pub owner: String,
pub balance: f64,
pub is_active: bool,
}
impl From<&Account> for JsonAccount {
fn from(a: &Account) -> Self {
JsonAccount {
number: a.number.clone(),
owner: a.owner.clone(),
balance: a.balance,
is_active: a.is_active,
}
}
}
#[get("/{nr}")]
pub async fn route(info: web::Path<String>, data: web::Data<AppState>) -> Result<impl Responder> {
let nr = info.into_inner();
info!("getting account {}...", nr);
let bank = data.bank.read().unwrap();
match bank.accounts.get(&nr) {
None => Err(HttpAccountError(AccountError::NotFound).into()),
Some(account) => {
let account = account.read().unwrap();
let account: JsonAccount = account.deref().into();
Ok(web::Json(account))
}
}
}

View File

@ -0,0 +1,11 @@
use actix_web::{get, web, Responder, Result};
use crate::AppState;
#[get("")]
pub async fn route(data: web::Data<AppState>) -> Result<impl Responder> {
info!("getting account numbers...");
let bank = data.bank.read().unwrap();
Ok(web::Json(bank.account_numbers()))
}

View File

@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
pub mod close_account;
pub mod create_account;
pub mod deposit;
pub mod error;
pub mod get_account;
pub mod get_account_nrs;
pub mod pong;
pub mod transfer;
pub mod withdraw;
#[derive(Deserialize, Serialize)]
pub struct AmountData {
pub(crate) amount: f64,
}

View File

@ -0,0 +1,6 @@
use actix_web::{get, HttpResponse};
#[get("/ping")]
pub async fn route() -> HttpResponse {
HttpResponse::Ok().finish()
}

View File

@ -0,0 +1,44 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use serde::{Deserialize, Serialize};
use bank::account::AccountError;
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[derive(Deserialize, Serialize)]
pub struct TransferData {
pub(crate) from: String,
pub(crate) to: String,
pub(crate) amount: f64,
}
#[post("/transfer")]
pub async fn route(
form: web::Form<TransferData>,
data: web::Data<AppState>,
) -> Result<impl Responder> {
let from = form.from.clone();
let to = form.to.clone();
let amount = form.amount;
info!("transfering {} from {} to {}...", from, to, amount);
let bank = data.bank.read().unwrap();
let from_acc = bank
.accounts
.get(&from)
.ok_or(HttpAccountError(AccountError::NotFound))?;
let mut from_acc = from_acc.write().unwrap();
let to_acc = bank
.accounts
.get(&to)
.ok_or(HttpAccountError(AccountError::NotFound))?;
let mut to_acc = to_acc.write().unwrap();
match bank.transfer(&mut from_acc, &mut to_acc, amount) {
Err(e) => Err(HttpAccountError(e).into()),
Ok(_) => Ok(HttpResponse::Ok().finish()),
}
}

View File

@ -0,0 +1,31 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use serde::Serialize;
use bank::bank::Bank;
use crate::handlers::error::HttpAccountError;
use crate::handlers::AmountData;
use crate::AppState;
#[derive(Serialize)]
struct AccountCreated {
nr: String,
}
#[post("/{nr}/withdraw")]
pub async fn route(
info: web::Path<String>,
form: web::Form<AmountData>,
data: web::Data<AppState>,
) -> Result<impl Responder> {
let nr = info.into_inner();
let amount = form.amount;
info!("withdrawing {} from account {}...", amount, nr);
let bank = data.bank.read().unwrap();
match Bank::account_action(bank, &nr, |account| account.withdraw(amount)) {
Err(e) => Err(HttpAccountError(e).into()),
Ok(_) => Ok(HttpResponse::Ok().finish()),
}
}

204
http-server/src/main.rs Normal file
View File

@ -0,0 +1,204 @@
#[macro_use]
extern crate log;
use std::sync::RwLock;
use actix_web::middleware::TrailingSlash;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{middleware, web, App, HttpServer};
use bank::bank::Bank;
use crate::handlers::close_account;
use crate::handlers::create_account;
use crate::handlers::deposit;
use crate::handlers::get_account;
use crate::handlers::get_account_nrs;
use crate::handlers::pong;
use crate::handlers::transfer;
use crate::handlers::withdraw;
pub struct AppState {
bank: RwLock<Bank>,
}
mod handlers;
fn config_app(app_data: Data<AppState>) -> Box<dyn Fn(&mut ServiceConfig)> {
Box::new(move |cfg: &mut ServiceConfig| {
cfg.app_data(app_data.clone()).service(pong::route).service(
web::scope("/accounts")
.service(create_account::route)
.service(get_account::route)
.service(deposit::route)
.service(close_account::route)
.service(get_account_nrs::route)
.service(withdraw::route)
.service(transfer::route),
);
})
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
pretty_env_logger::init();
let app_data = web::Data::new(AppState {
bank: RwLock::new(Bank::new()),
});
HttpServer::new(move || {
App::new()
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
.configure(config_app(app_data.clone()))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
#[cfg(test)]
mod tests {
use actix_web::body::to_bytes;
use actix_web::{test, App};
use serde_json::Value;
use crate::create_account::OwnerData;
use crate::handlers::AmountData;
use crate::transfer::TransferData;
use super::*;
#[actix_web::test]
async fn test_routes() {
// TODO: split up into separate tests
let app_data = web::Data::new(AppState {
bank: RwLock::new(Bank::new()),
});
let app = test::init_service(
App::new()
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
.configure(config_app(app_data)),
)
.await;
let payload = OwnerData {
owner: "aaa".to_string(),
};
let req = test::TestRequest::post()
.uri("/accounts")
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let nr = response_json["nr"].as_str().unwrap();
assert!(!nr.is_empty());
let payload = AmountData { amount: 10_f64 };
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/withdraw", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(!resp.status().is_success());
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/deposit", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::get()
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let balance = response_json["balance"].as_f64().unwrap();
assert_eq!(10_f64, balance);
let payload = OwnerData {
owner: "bbb".to_string(),
};
let req = test::TestRequest::post()
.uri("/accounts")
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let nr2 = response_json["nr"].as_str().unwrap();
let payload = TransferData {
from: nr.to_string(),
to: nr2.to_string(),
amount: 5_f64,
};
let req = test::TestRequest::post()
.uri("/accounts/transfer")
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::get()
.uri(&format!("/accounts/{}", nr2))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let balance = response_json["balance"].as_f64().unwrap();
assert_eq!(5_f64, balance);
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/close", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(!resp.status().is_success());
let payload = AmountData { amount: 5_f64 };
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/withdraw", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::get().uri("/accounts").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let nrs = response_json.as_array().unwrap();
assert_eq!(2, nrs.len());
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/close", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::get().uri("/accounts").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
let nrs = response_json.as_array().unwrap();
assert_eq!(1, nrs.len());
}
}

View File

@ -7,4 +7,5 @@ edition = "2021"
bank = { path = "../bank" } bank = { path = "../bank" }
anyhow = "1.0.55" anyhow = "1.0.55"
log = "0.4.14" log = "0.4.14"
pretty_env_logger = "0.4.0" pretty_env_logger = "0.4.0"
thiserror = "1.0.30"

View File

@ -12,7 +12,12 @@ use crate::protocol;
pub struct CloseAccount; pub struct CloseAccount;
impl Command for CloseAccount { impl Command for CloseAccount {
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> { fn execute(
&self,
bank: Arc<RwLock<Bank>>,
data: &[u8],
mut stream: &TcpStream,
) -> Result<usize> {
debug!("account nr bytes {:?}", data); debug!("account nr bytes {:?}", data);
let nr = String::from_utf8_lossy(data).to_string(); let nr = String::from_utf8_lossy(data).to_string();
@ -25,9 +30,9 @@ impl Command for CloseAccount {
let mut acc = acc.write().unwrap(); let mut acc = acc.write().unwrap();
stream.write(&protocol::account_passivate(acc.passivate()))? stream.write(&protocol::account_passivate(acc.passivate()))?
} }
None => stream.write(&protocol::account_passivate(false))? None => stream.write(&protocol::account_passivate(false))?,
}; };
Ok(written) Ok(written)
} }
} }

View File

@ -12,7 +12,12 @@ use crate::protocol;
pub struct CreateAccount; pub struct CreateAccount;
impl Command for CreateAccount { impl Command for CreateAccount {
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> { fn execute(
&self,
bank: Arc<RwLock<Bank>>,
data: &[u8],
mut stream: &TcpStream,
) -> Result<usize> {
debug!("owner nr bytes {:?}", data); debug!("owner nr bytes {:?}", data);
let owner = String::from_utf8_lossy(data); let owner = String::from_utf8_lossy(data);
@ -25,4 +30,4 @@ impl Command for CreateAccount {
let written = stream.write(&protocol::account_nr(&nr))?; let written = stream.write(&protocol::account_nr(&nr))?;
Ok(written) Ok(written)
} }
} }

View File

@ -12,7 +12,12 @@ use crate::protocol;
pub struct Deposit; pub struct Deposit;
impl Command for Deposit { impl Command for Deposit {
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> { fn execute(
&self,
bank: Arc<RwLock<Bank>>,
data: &[u8],
mut stream: &TcpStream,
) -> Result<usize> {
let value_bytes: [u8; 8] = <[u8; 8]>::try_from(data[..8].to_vec().as_slice())?; let value_bytes: [u8; 8] = <[u8; 8]>::try_from(data[..8].to_vec().as_slice())?;
debug!("value bytes {:?}", value_bytes); debug!("value bytes {:?}", value_bytes);
@ -30,9 +35,9 @@ impl Command for Deposit {
let mut acc = acc.write().unwrap(); let mut acc = acc.write().unwrap();
stream.write(&protocol::deposit(acc.deposit(amount)))? stream.write(&protocol::deposit(acc.deposit(amount)))?
} }
None => stream.write(&protocol::error(2))? None => stream.write(&protocol::error(2))?,
}; };
Ok(written) Ok(written)
} }
} }

View File

@ -0,0 +1,35 @@
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use thiserror::Error;
use bank::account::AccountError;
#[derive(Debug, Error)]
pub struct SocketAccountError(pub AccountError);
impl Display for SocketAccountError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.deref())
}
}
impl Deref for SocketAccountError {
type Target = AccountError;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<SocketAccountError> for u8 {
fn from(e: SocketAccountError) -> Self {
match e.deref() {
AccountError::Overdraw => 11,
AccountError::Inactive => 12,
AccountError::InvalidAmount => 13,
AccountError::NotFound => 14,
AccountError::AccountNotZero => 15,
}
}
}

View File

@ -12,9 +12,14 @@ use crate::protocol;
pub struct Fail; pub struct Fail;
impl Command for Fail { impl Command for Fail {
fn execute(&self, _: Arc<RwLock<Bank>>, error_code: &[u8], mut stream: &TcpStream) -> Result<usize> { fn execute(
&self,
_: Arc<RwLock<Bank>>,
error_code: &[u8],
mut stream: &TcpStream,
) -> Result<usize> {
error!("sending error code {}", error_code[0]); error!("sending error code {}", error_code[0]);
let written = stream.write(&protocol::error(error_code[0]))?; let written = stream.write(&protocol::error(error_code[0]))?;
Ok(written) Ok(written)
} }
} }

View File

@ -12,7 +12,12 @@ use crate::protocol;
pub struct GetAccount; pub struct GetAccount;
impl Command for GetAccount { impl Command for GetAccount {
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> { fn execute(
&self,
bank: Arc<RwLock<Bank>>,
data: &[u8],
mut stream: &TcpStream,
) -> Result<usize> {
debug!("account nr bytes {:?}", data); debug!("account nr bytes {:?}", data);
let nr = String::from_utf8_lossy(data).to_string(); let nr = String::from_utf8_lossy(data).to_string();
@ -25,9 +30,9 @@ impl Command for GetAccount {
let acc = acc.read().unwrap(); let acc = acc.read().unwrap();
stream.write(&protocol::account(&acc))? stream.write(&protocol::account(&acc))?
} }
None => stream.write(&protocol::error(2))? None => stream.write(&protocol::error(2))?,
}; };
Ok(written) Ok(written)
} }
} }

View File

@ -16,12 +16,10 @@ impl Command for GetAccountNrs {
info!("getting account numbers..."); info!("getting account numbers...");
let bank = bank.read().unwrap(); let bank = bank.read().unwrap();
let nrs: Vec<String> = bank.account_numbers() let nrs: Vec<String> = bank.account_numbers().into_iter().collect();
.into_iter()
.collect();
let written = stream.write(&protocol::account_nrs(&nrs))?; let written = stream.write(&protocol::account_nrs(&nrs))?;
Ok(written) Ok(written)
} }
} }

View File

@ -4,7 +4,6 @@ use std::sync::{Arc, RwLock};
use anyhow::Result; use anyhow::Result;
use bank::account::AccountError;
use bank::bank::Bank; use bank::bank::Bank;
use crate::commands::close_account::CloseAccount; use crate::commands::close_account::CloseAccount;
@ -16,13 +15,14 @@ use crate::commands::get_account_nrs::GetAccountNrs;
use crate::commands::pong::Pong; use crate::commands::pong::Pong;
use crate::commands::withdraw::Withdraw; use crate::commands::withdraw::Withdraw;
mod pong;
mod fail;
mod close_account; mod close_account;
mod create_account; mod create_account;
mod deposit; mod deposit;
pub mod error;
mod fail;
mod get_account; mod get_account;
mod get_account_nrs; mod get_account_nrs;
mod pong;
mod withdraw; mod withdraw;
pub trait Command: Sync + Send { pub trait Command: Sync + Send {
@ -33,26 +33,6 @@ pub struct Commands {
cmds: HashMap<u8, Box<dyn Command>>, cmds: HashMap<u8, Box<dyn Command>>,
} }
struct WebAccountError(AccountError);
impl From<&WebAccountError> for u8 {
fn from(error: &WebAccountError) -> Self {
match error.0 {
AccountError::Overdraw() => 11,
AccountError::Inactive() => 12,
AccountError::InvalidAmount() => 13,
}
}
}
pub fn error_to_code(error: &AccountError) -> u8 {
match error {
AccountError::Overdraw() => 11,
AccountError::Inactive() => 12,
AccountError::InvalidAmount() => 13,
}
}
impl Commands { impl Commands {
pub fn new() -> Self { pub fn new() -> Self {
let mut cmds = HashMap::new(); let mut cmds = HashMap::new();
@ -64,15 +44,19 @@ impl Commands {
cmds.insert(6_u8, Box::new(Deposit) as Box<dyn Command>); cmds.insert(6_u8, Box::new(Deposit) as Box<dyn Command>);
cmds.insert(7_u8, Box::new(Withdraw) as Box<dyn Command>); cmds.insert(7_u8, Box::new(Withdraw) as Box<dyn Command>);
Self { Self { cmds }
cmds
}
} }
pub fn run(&self, bank: Arc<RwLock<Bank>>, cmd: u8, data: &[u8], stream: &TcpStream) -> Result<usize> { pub fn run(
&self,
bank: Arc<RwLock<Bank>>,
cmd: u8,
data: &[u8],
stream: &TcpStream,
) -> Result<usize> {
match self.cmds.get(&cmd) { match self.cmds.get(&cmd) {
None => Fail.execute(bank, &[1_u8], stream), None => Fail.execute(bank, &[1_u8], stream),
Some(cmd) => cmd.execute(bank, data, stream) Some(cmd) => cmd.execute(bank, data, stream),
} }
} }
} }

View File

@ -17,4 +17,4 @@ impl Command for Pong {
let written = stream.write(&[protocol::PONG])?; let written = stream.write(&[protocol::PONG])?;
Ok(written) Ok(written)
} }
} }

View File

@ -12,7 +12,12 @@ use crate::protocol;
pub struct Withdraw; pub struct Withdraw;
impl Command for Withdraw { impl Command for Withdraw {
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> { fn execute(
&self,
bank: Arc<RwLock<Bank>>,
data: &[u8],
mut stream: &TcpStream,
) -> Result<usize> {
let value_bytes: [u8; 8] = <[u8; 8]>::try_from(data[..8].to_vec().as_slice())?; let value_bytes: [u8; 8] = <[u8; 8]>::try_from(data[..8].to_vec().as_slice())?;
debug!("value bytes {:?}", value_bytes); debug!("value bytes {:?}", value_bytes);
@ -24,15 +29,14 @@ impl Command for Withdraw {
info!("withdrawing {} from {}...", amount, nr); info!("withdrawing {} from {}...", amount, nr);
let bank = bank.read().unwrap(); let bank = bank.read().unwrap();
let written = match bank.accounts.get(&nr) { let written = match bank.accounts.get(&nr) {
Some(acc) => { Some(acc) => {
let mut acc = acc.write().unwrap(); let mut acc = acc.write().unwrap();
stream.write(&protocol::withdraw(acc.withdraw(amount)))? stream.write(&protocol::withdraw(acc.withdraw(amount)))?
} }
None => stream.write(&protocol::error(2))? None => stream.write(&protocol::error(2))?,
}; };
Ok(written) Ok(written)
} }
} }

View File

@ -1,10 +1,10 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
mod commands;
mod protocol; mod protocol;
mod server; mod server;
mod threadpool; mod threadpool;
mod commands;
fn main() { fn main() {
pretty_env_logger::init(); pretty_env_logger::init();

View File

@ -2,18 +2,11 @@ use anyhow::Result;
use bank::account::{Account, AccountError}; use bank::account::{Account, AccountError};
use crate::commands::error_to_code; use crate::commands::error::SocketAccountError;
pub const START: [u8; 2] = [0xde, 0xad]; pub const START: [u8; 2] = [0xde, 0xad];
pub const PONG: u8 = 0b0010_0000; pub const PONG: u8 = 0b0010_0000;
fn to_error_code(error: anyhow::Error, default: u8) -> u8 {
match error.root_cause().downcast_ref::<AccountError>() {
Some(e) => error_to_code(e),
None => default,
}
}
pub fn account_nr(nr: &str) -> Vec<u8> { pub fn account_nr(nr: &str) -> Vec<u8> {
let mut response = vec![PONG]; let mut response = vec![PONG];
response.append(&mut nr.as_bytes().to_vec()); response.append(&mut nr.as_bytes().to_vec());
@ -26,17 +19,17 @@ pub fn account_passivate(was_passivated: bool) -> Vec<u8> {
vec![PONG | is_active_byte] vec![PONG | is_active_byte]
} }
pub fn deposit(result: Result<()>) -> Vec<u8> { pub fn deposit(result: Result<f64, AccountError>) -> Vec<u8> {
match result { match result {
Err(e) => error(to_error_code(e, 10)).to_vec(), Err(e) => error(SocketAccountError(e).into()).to_vec(),
Ok(_) => vec![PONG] Ok(_) => vec![PONG],
} }
} }
pub fn withdraw(result: Result<()>) -> Vec<u8> { pub fn withdraw(result: Result<f64, AccountError>) -> Vec<u8> {
match result { match result {
Err(e) => error(to_error_code(e, 10)).to_vec(), Err(e) => error(SocketAccountError(e).into()).to_vec(),
Ok(_) => vec![PONG] Ok(_) => vec![PONG],
} }
} }
@ -85,4 +78,4 @@ pub fn account(account: &Account) -> Vec<u8> {
pub fn error(code: u8) -> [u8; 1] { pub fn error(code: u8) -> [u8; 1] {
[0b0100_0000 | code] [0b0100_0000 | code]
} }

View File

@ -33,7 +33,11 @@ pub fn run(host: &str, threads: usize) -> Result<()> {
Ok(()) Ok(())
} }
fn handle_connection(bank: Arc<RwLock<Bank>>, cmds: &Commands, mut stream: TcpStream) -> Result<()> { fn handle_connection(
bank: Arc<RwLock<Bank>>,
cmds: &Commands,
mut stream: TcpStream,
) -> Result<()> {
const BUF_SIZE: usize = 64; const BUF_SIZE: usize = 64;
let mut data: Vec<u8> = vec![]; let mut data: Vec<u8> = vec![];
@ -58,4 +62,3 @@ fn handle_connection(bank: Arc<RwLock<Bank>>, cmds: &Commands, mut stream: TcpSt
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
use std::sync::Arc;
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex; use std::sync::Mutex;
use std::thread; use std::thread;
@ -33,8 +33,8 @@ impl ThreadPool {
} }
pub fn execute<F>(&self, f: F) pub fn execute<F>(&self, f: F)
where where
F: FnOnce() + Send + 'static, F: FnOnce() + Send + 'static,
{ {
let job = Box::new(f); let job = Box::new(f);
@ -91,4 +91,4 @@ impl Worker {
thread: Some(thread), thread: Some(thread),
} }
} }
} }