initial commit
This commit is contained in:
commit
3d9c98eeca
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
6
.idea/.gitignore
vendored
Normal file
6
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
11
.idea/bank-server.iml
Normal file
11
.idea/bank-server.iml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="CPP_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/bank-server.iml" filepath="$PROJECT_DIR$/.idea/bank-server.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
252
Cargo.lock
generated
Normal file
252
Cargo.lock
generated
Normal file
@ -0,0 +1,252 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bank-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
|
||||
dependencies = [
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_env_logger"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
|
||||
dependencies = [
|
||||
"env_logger",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "bank-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.55"
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
thiserror = "1.0.30"
|
||||
uuid = { version = "0.8.2", features = ["v4"] }
|
142
src/account.rs
Normal file
142
src/account.rs
Normal file
@ -0,0 +1,142 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use thiserror::Error;
|
||||
use anyhow::{bail, Result};
|
||||
use uuid::Uuid;
|
||||
use crate::account::AccountError::{Inactive, InvalidAmount, Overdraw};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AccountError {
|
||||
#[error("can not overdraw account")]
|
||||
Overdraw(),
|
||||
#[error("account is inactive")]
|
||||
Inactive(),
|
||||
#[error("amount must be > 0")]
|
||||
InvalidAmount(),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Account {
|
||||
pub number: String,
|
||||
pub owner: String,
|
||||
pub balance: f64,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl Default for Account {
|
||||
fn default() -> Self {
|
||||
Account {
|
||||
number: Uuid::new_v4().to_string(),
|
||||
owner: "".into(),
|
||||
balance: 0_f64,
|
||||
is_active: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Account {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.number == other.number
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Account {}
|
||||
|
||||
impl Hash for Account {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.number.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Account {
|
||||
#[cfg(test)]
|
||||
pub fn new() -> Self {
|
||||
Self { ..Default::default() }
|
||||
}
|
||||
|
||||
pub fn deposit(&mut self, amount: f64) -> Result<()> {
|
||||
self.check_account(amount)?;
|
||||
self.balance += amount;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn withdraw(&mut self, amount: f64) -> Result<()> {
|
||||
self.check_account(amount)?;
|
||||
|
||||
if self.balance - amount < 0 as f64 {
|
||||
bail!(Overdraw());
|
||||
}
|
||||
self.balance -= amount;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_account(&self, amount: f64) -> Result<()> {
|
||||
if !self.is_active {
|
||||
bail!(Inactive());
|
||||
}
|
||||
|
||||
if amount < 0 as f64 {
|
||||
bail!(InvalidAmount());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn passivate(&mut self) -> bool {
|
||||
let is_passivated = self.balance <= 0 as f64 && self.is_active;
|
||||
|
||||
if is_passivated {
|
||||
self.is_active = false;
|
||||
}
|
||||
|
||||
is_passivated
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deposits() {
|
||||
let mut acc = Account::new();
|
||||
|
||||
let ok_result = acc.deposit(10.56);
|
||||
assert!(ok_result.is_ok());
|
||||
|
||||
let err_result = acc.deposit(-5.89);
|
||||
assert!(err_result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn withdrawals() {
|
||||
let mut acc = Account::new();
|
||||
|
||||
let ok_result1 = acc.deposit(10_f64);
|
||||
assert!(ok_result1.is_ok());
|
||||
|
||||
let ok_result2 = acc.withdraw(5_f64);
|
||||
assert!(ok_result2.is_ok());
|
||||
|
||||
let err_result1 = acc.withdraw(10_f64);
|
||||
assert!(err_result1.is_err());
|
||||
|
||||
let err_result2 = acc.withdraw(-10_f64);
|
||||
assert!(err_result2.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passivation() {
|
||||
let mut acc = Account::new();
|
||||
let deposit_amount = 100_f64;
|
||||
|
||||
acc.deposit(deposit_amount).unwrap();
|
||||
|
||||
assert!(!acc.passivate());
|
||||
acc.withdraw(deposit_amount).unwrap();
|
||||
|
||||
assert!(acc.passivate());
|
||||
assert!(!acc.passivate());
|
||||
}
|
||||
}
|
86
src/bank.rs
Normal file
86
src/bank.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::RwLock;
|
||||
|
||||
#[cfg(test)]
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::account::Account;
|
||||
|
||||
pub struct Bank {
|
||||
pub accounts: HashMap<String, RwLock<Account>>,
|
||||
}
|
||||
|
||||
impl Bank {
|
||||
pub fn new() -> Self {
|
||||
Self { accounts: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn account_numbers(&self) -> HashSet<String> {
|
||||
self.accounts.iter()
|
||||
.filter(|(_, acc)| acc.read().unwrap().is_active)
|
||||
.map(|(_, acc)| acc.read().unwrap().number.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn create_account(&mut self, owner: String) -> String {
|
||||
let acc = Account { owner, ..Default::default() };
|
||||
let number = acc.number.clone();
|
||||
|
||||
self.accounts.insert(acc.number.clone(), RwLock::new(acc));
|
||||
|
||||
number
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn transfer(&self, from: &mut Account, to: &mut Account, amount: f64) -> Result<()> {
|
||||
from.withdraw(amount)?;
|
||||
to.deposit(amount)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creation() {
|
||||
let mut bank = Bank::new();
|
||||
let nr = bank.create_account("Rohaja".into());
|
||||
|
||||
assert!(bank.accounts.get(&nr).is_some());
|
||||
assert!(bank.accounts.get("thisisnotanuuid").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer() {
|
||||
let mut bank = Bank::new();
|
||||
let nr1 = bank.create_account("Rohaja".into());
|
||||
let nr2 = bank.create_account("Hal".into());
|
||||
|
||||
{
|
||||
let mut acc1 = bank.accounts.get(&nr1).unwrap().write().unwrap();
|
||||
let mut acc2 = bank.accounts.get(&nr2).unwrap().write().unwrap();
|
||||
|
||||
let result = bank.transfer(&mut acc1, &mut acc2, 100_f64);
|
||||
assert!(result.is_err());
|
||||
|
||||
acc1.deposit(100_f64).unwrap();
|
||||
let result = bank.transfer(&mut acc1, &mut acc2, 50_f64);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(acc2.balance, 50_f64);
|
||||
|
||||
let result = bank.transfer(&mut acc1, &mut acc2, -50_f64);
|
||||
assert!(result.is_err());
|
||||
|
||||
acc2.withdraw(50_f64).unwrap();
|
||||
assert!(acc2.passivate());
|
||||
|
||||
let result = bank.transfer(&mut acc1, &mut acc2, 50_f64);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
assert_eq!(1, bank.account_numbers().len());
|
||||
}
|
||||
}
|
32
src/commands/close_account.rs
Normal file
32
src/commands/close_account.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct CloseAccount;
|
||||
|
||||
impl Command for CloseAccount {
|
||||
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> {
|
||||
debug!("account nr bytes {:?}", data);
|
||||
|
||||
let nr = String::from_utf8_lossy(data).to_string();
|
||||
info!("closing account {}...", nr);
|
||||
|
||||
let bank = bank.read().unwrap();
|
||||
|
||||
let written = match bank.accounts.get(&nr) {
|
||||
Some(acc) => {
|
||||
let mut acc = acc.write().unwrap();
|
||||
stream.write(&protocol::account_passivate(acc.passivate()))?
|
||||
}
|
||||
None => stream.write(&protocol::account_passivate(false))?
|
||||
};
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
}
|
27
src/commands/create_account.rs
Normal file
27
src/commands/create_account.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct CreateAccount;
|
||||
|
||||
impl Command for CreateAccount {
|
||||
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> {
|
||||
debug!("owner nr bytes {:?}", data);
|
||||
|
||||
let owner = String::from_utf8_lossy(data);
|
||||
info!("creating new account with owner {}...", owner);
|
||||
|
||||
let mut bank = bank.write().unwrap();
|
||||
let nr = bank.create_account(owner.into());
|
||||
info!("created account {}", nr);
|
||||
|
||||
let written = stream.write(&protocol::account_nr(&nr))?;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
37
src/commands/deposit.rs
Normal file
37
src/commands/deposit.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct Deposit;
|
||||
|
||||
impl Command for Deposit {
|
||||
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())?;
|
||||
debug!("value bytes {:?}", value_bytes);
|
||||
|
||||
let nr_bytes = &data[8..].to_vec();
|
||||
debug!("nr bytes {:?}", nr_bytes);
|
||||
|
||||
let amount = f64::from_be_bytes(value_bytes);
|
||||
let nr = String::from_utf8_lossy(nr_bytes).to_string();
|
||||
info!("depositing {} into {}...", amount, nr);
|
||||
|
||||
let bank = bank.read().unwrap();
|
||||
|
||||
let written = match bank.accounts.get(&nr) {
|
||||
Some(acc) => {
|
||||
let mut acc = acc.write().unwrap();
|
||||
stream.write(&protocol::deposit(acc.deposit(amount)))?
|
||||
}
|
||||
None => stream.write(&protocol::error(2))?
|
||||
};
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
}
|
19
src/commands/fail.rs
Normal file
19
src/commands/fail.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct Fail;
|
||||
|
||||
impl Command for Fail {
|
||||
fn execute(&self, _: Arc<RwLock<Bank>>, error_code: &[u8], mut stream: &TcpStream) -> Result<usize> {
|
||||
error!("sending error code {}", error_code[0]);
|
||||
let written = stream.write(&protocol::error(error_code[0]))?;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
32
src/commands/get_account.rs
Normal file
32
src/commands/get_account.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct GetAccount;
|
||||
|
||||
impl Command for GetAccount {
|
||||
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], mut stream: &TcpStream) -> Result<usize> {
|
||||
debug!("account nr bytes {:?}", data);
|
||||
let nr = String::from_utf8_lossy(data).to_string();
|
||||
|
||||
info!("getting account {:?}...", nr);
|
||||
|
||||
let bank = bank.read().unwrap();
|
||||
|
||||
let written = match bank.accounts.get(&nr) {
|
||||
Some(acc) => {
|
||||
let acc = acc.read().unwrap();
|
||||
stream.write(&protocol::account(&acc))?
|
||||
}
|
||||
None => stream.write(&protocol::error(2))?
|
||||
};
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
}
|
26
src/commands/get_account_nrs.rs
Normal file
26
src/commands/get_account_nrs.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct GetAccountNrs;
|
||||
|
||||
impl Command for GetAccountNrs {
|
||||
fn execute(&self, bank: Arc<RwLock<Bank>>, _: &[u8], mut stream: &TcpStream) -> Result<usize> {
|
||||
info!("getting account numbers...");
|
||||
|
||||
let bank = bank.read().unwrap();
|
||||
let nrs: Vec<String> = bank.account_numbers()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let written = stream.write(&protocol::account_nrs(&nrs))?;
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
}
|
67
src/commands/mod.rs
Normal file
67
src/commands/mod.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::account::AccountError;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::close_account::CloseAccount;
|
||||
use crate::commands::create_account::CreateAccount;
|
||||
use crate::commands::deposit::Deposit;
|
||||
use crate::commands::fail::Fail;
|
||||
use crate::commands::get_account::GetAccount;
|
||||
use crate::commands::get_account_nrs::GetAccountNrs;
|
||||
use crate::commands::pong::Pong;
|
||||
use crate::commands::withdraw::Withdraw;
|
||||
|
||||
mod pong;
|
||||
mod fail;
|
||||
mod close_account;
|
||||
mod create_account;
|
||||
mod deposit;
|
||||
mod get_account;
|
||||
mod get_account_nrs;
|
||||
mod withdraw;
|
||||
|
||||
pub trait Command: Sync + Send {
|
||||
fn execute(&self, bank: Arc<RwLock<Bank>>, data: &[u8], stream: &TcpStream) -> Result<usize>;
|
||||
}
|
||||
|
||||
pub struct Commands {
|
||||
cmds: HashMap<u8, Box<dyn Command>>,
|
||||
}
|
||||
|
||||
impl From<&AccountError> for u8 {
|
||||
fn from(error: &AccountError) -> Self {
|
||||
match error {
|
||||
AccountError::Overdraw() => 11,
|
||||
AccountError::Inactive() => 12,
|
||||
AccountError::InvalidAmount() => 13,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub fn new() -> Self {
|
||||
let mut cmds = HashMap::new();
|
||||
cmds.insert(1_u8, Box::new(Pong) as Box<dyn Command>);
|
||||
cmds.insert(2_u8, Box::new(CreateAccount) as Box<dyn Command>);
|
||||
cmds.insert(3_u8, Box::new(GetAccount) as Box<dyn Command>);
|
||||
cmds.insert(4_u8, Box::new(GetAccountNrs) as Box<dyn Command>);
|
||||
cmds.insert(5_u8, Box::new(CloseAccount) 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>);
|
||||
|
||||
Self {
|
||||
cmds
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self, bank: Arc<RwLock<Bank>>, cmd: u8, data: &[u8], stream: &TcpStream) -> Result<usize> {
|
||||
match self.cmds.get(&cmd) {
|
||||
None => Fail.execute(bank, &[1_u8], stream),
|
||||
Some(cmd) => cmd.execute(bank, data, stream)
|
||||
}
|
||||
}
|
||||
}
|
19
src/commands/pong.rs
Normal file
19
src/commands/pong.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct Pong;
|
||||
|
||||
impl Command for Pong {
|
||||
fn execute(&self, _: Arc<RwLock<Bank>>, _: &[u8], mut stream: &TcpStream) -> Result<usize> {
|
||||
info!("sending 'pong'");
|
||||
let written = stream.write(&[protocol::PONG])?;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
37
src/commands/withdraw.rs
Normal file
37
src/commands/withdraw.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Command;
|
||||
use crate::protocol;
|
||||
|
||||
pub struct Withdraw;
|
||||
|
||||
impl Command for Withdraw {
|
||||
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())?;
|
||||
debug!("value bytes {:?}", value_bytes);
|
||||
|
||||
let nr_bytes = &data[8..].to_vec();
|
||||
debug!("nr bytes {:?}", nr_bytes);
|
||||
|
||||
let amount = f64::from_be_bytes(value_bytes);
|
||||
let nr = String::from_utf8_lossy(nr_bytes).to_string();
|
||||
info!("withdrawing {} from {}...", amount, nr);
|
||||
|
||||
let bank = bank.read().unwrap();
|
||||
|
||||
let written = match bank.accounts.get(&nr) {
|
||||
Some(acc) => {
|
||||
let mut acc = acc.write().unwrap();
|
||||
stream.write(&protocol::withdraw(acc.withdraw(amount)))?
|
||||
}
|
||||
None => stream.write(&protocol::error(2))?
|
||||
};
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
}
|
17
src/main.rs
Normal file
17
src/main.rs
Normal file
@ -0,0 +1,17 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
mod account;
|
||||
mod bank;
|
||||
mod protocol;
|
||||
mod server;
|
||||
mod threadpool;
|
||||
mod commands;
|
||||
|
||||
fn main() {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let host = "127.0.0.1:1234";
|
||||
info!("running on {}", host);
|
||||
server::run(host, 8).expect("failed to run server");
|
||||
}
|
85
src/protocol.rs
Normal file
85
src/protocol.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use crate::account::{Account, AccountError};
|
||||
use anyhow::Result;
|
||||
|
||||
pub const START: [u8; 2] = [0xde, 0xad];
|
||||
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) => e.into(),
|
||||
None => default,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn account_nr(nr: &str) -> Vec<u8> {
|
||||
let mut response = vec![PONG];
|
||||
response.append(&mut nr.as_bytes().to_vec());
|
||||
debug!("account number bytes: {:?}", response);
|
||||
response
|
||||
}
|
||||
|
||||
pub fn account_passivate(was_passivated: bool) -> Vec<u8> {
|
||||
let is_active_byte: u8 = if was_passivated { 1 } else { 0 };
|
||||
vec![PONG | is_active_byte]
|
||||
}
|
||||
|
||||
pub fn deposit(result: Result<()>) -> Vec<u8> {
|
||||
match result {
|
||||
Err(e) => error(to_error_code(e, 10)).to_vec(),
|
||||
Ok(_) => vec![PONG]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn withdraw(result: Result<()>) -> Vec<u8> {
|
||||
match result {
|
||||
Err(e) => error(to_error_code(e, 10)).to_vec(),
|
||||
Ok(_) => vec![PONG]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn account_nrs(nrs: &[String]) -> Vec<u8> {
|
||||
let mut response = vec![PONG];
|
||||
|
||||
nrs.iter().for_each(|key| {
|
||||
let key_bytes = key.as_bytes();
|
||||
debug!("account number bytes: {:?}", key_bytes);
|
||||
|
||||
response.push(key_bytes.len() as u8);
|
||||
response.append(&mut key_bytes.to_vec());
|
||||
});
|
||||
|
||||
debug!("account numbers bytes: {:?}", response);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub fn account(account: &Account) -> Vec<u8> {
|
||||
let is_active_byte: u8 = if account.is_active { 1 } else { 0 };
|
||||
|
||||
let mut response = vec![PONG | is_active_byte];
|
||||
|
||||
let nr_bytes = account.number.as_bytes();
|
||||
let mut nr_bytes_sized = vec![nr_bytes.len() as u8];
|
||||
nr_bytes_sized.append(&mut nr_bytes.to_vec());
|
||||
debug!("nr bytes: {:?}", nr_bytes_sized);
|
||||
|
||||
let owner_bytes = account.owner.as_bytes();
|
||||
let mut owner_bytes_sized = vec![owner_bytes.len() as u8];
|
||||
owner_bytes_sized.append(&mut owner_bytes.to_vec());
|
||||
debug!("owner bytes: {:?}", owner_bytes_sized);
|
||||
|
||||
let balance: [u8; 8] = account.balance.to_be_bytes();
|
||||
debug!("balance bytes: {:?}", balance);
|
||||
|
||||
response.append(&mut nr_bytes_sized);
|
||||
response.append(&mut owner_bytes_sized);
|
||||
response.append(&mut balance.to_vec());
|
||||
|
||||
debug!("account response: {:?}", response);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub fn error(code: u8) -> [u8; 1] {
|
||||
[0b0100_0000 | code]
|
||||
}
|
60
src/server.rs
Normal file
60
src/server.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::bank::Bank;
|
||||
use crate::commands::Commands;
|
||||
use crate::protocol;
|
||||
use crate::threadpool::ThreadPool;
|
||||
|
||||
pub fn run(host: &str, threads: usize) -> Result<()> {
|
||||
let listener = TcpListener::bind(host)?;
|
||||
let pool = ThreadPool::new(threads);
|
||||
let cmds = Arc::new(Commands::new());
|
||||
|
||||
let bank = Arc::new(RwLock::new(Bank::new()));
|
||||
|
||||
for stream in listener.incoming() {
|
||||
let cmds = cmds.clone();
|
||||
let bank = bank.clone();
|
||||
|
||||
let stream = stream?;
|
||||
|
||||
pool.execute(move || {
|
||||
if let Err(e) = handle_connection(bank, &cmds, stream) {
|
||||
error!("{}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_connection(bank: Arc<RwLock<Bank>>, cmds: &Commands, mut stream: TcpStream) -> Result<()> {
|
||||
const BUF_SIZE: usize = 64;
|
||||
|
||||
let mut data: Vec<u8> = vec![];
|
||||
let mut buffer = [0; BUF_SIZE];
|
||||
|
||||
let mut read = stream.read(&mut buffer)?;
|
||||
while read > 0 {
|
||||
data.append(&mut buffer[..read].to_vec());
|
||||
read = stream.read(&mut buffer)?;
|
||||
}
|
||||
|
||||
if data[..2] != protocol::START {
|
||||
bail!("got {:?} as first bytes, not my problem", &data[..2]);
|
||||
}
|
||||
|
||||
let cmd: u8 = (data[2] & 0b1111_0000) >> 4;
|
||||
trace!("got command {}", cmd);
|
||||
|
||||
cmds.run(bank, cmd, &data[3..], &stream)?;
|
||||
|
||||
stream.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
94
src/threadpool.rs
Normal file
94
src/threadpool.rs
Normal file
@ -0,0 +1,94 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
pub struct ThreadPool {
|
||||
workers: Vec<Worker>,
|
||||
sender: mpsc::Sender<Message>,
|
||||
}
|
||||
|
||||
type Job = Box<dyn FnOnce() + Send + 'static>;
|
||||
|
||||
enum Message {
|
||||
NewJob(Job),
|
||||
Terminate,
|
||||
}
|
||||
|
||||
impl ThreadPool {
|
||||
pub fn new(size: usize) -> ThreadPool {
|
||||
assert!(size > 0);
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
|
||||
let receiver = Arc::new(Mutex::new(receiver));
|
||||
|
||||
let mut workers = Vec::with_capacity(size);
|
||||
|
||||
for id in 0..size {
|
||||
workers.push(Worker::new(id, Arc::clone(&receiver)));
|
||||
}
|
||||
|
||||
ThreadPool { workers, sender }
|
||||
}
|
||||
|
||||
pub fn execute<F>(&self, f: F)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
let job = Box::new(f);
|
||||
|
||||
self.sender.send(Message::NewJob(job)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ThreadPool {
|
||||
fn drop(&mut self) {
|
||||
trace!("Sending terminate message to all workers.");
|
||||
|
||||
for _ in &self.workers {
|
||||
self.sender.send(Message::Terminate).unwrap();
|
||||
}
|
||||
|
||||
trace!("Shutting down all workers.");
|
||||
|
||||
for worker in &mut self.workers {
|
||||
trace!("Shutting down worker {}", worker.id);
|
||||
|
||||
if let Some(thread) = worker.thread.take() {
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
id: usize,
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
|
||||
let thread = thread::spawn(move || loop {
|
||||
let message = receiver.lock().unwrap().recv().unwrap();
|
||||
|
||||
match message {
|
||||
Message::NewJob(job) => {
|
||||
trace!("Worker {} got a job; executing.", id);
|
||||
|
||||
job();
|
||||
}
|
||||
Message::Terminate => {
|
||||
trace!("Worker {} was told to terminate.", id);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Worker {
|
||||
id,
|
||||
thread: Some(thread),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user