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

@ -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()),
}
}