add http-client

This commit is contained in:
Sebastian Hugentobler 2022-03-25 19:18:55 +01:00
parent ac2904588c
commit 8bcd555d71
36 changed files with 1978 additions and 49 deletions

View file

@ -5,6 +5,8 @@ edition = "2021"
[dependencies]
bank = { path = "../bank" }
http-lib = { path = "../http-lib" }
actix-cors = "0.6.1"
actix-web = "4.0.1"
thiserror = "1.0.30"
log = "0.4.14"

View file

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{delete, web, HttpResponse, Responder, Result};
use bank::account::AccountError;
use bank::bank::Bank;
@ -6,7 +6,7 @@ use bank::bank::Bank;
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[post("/{nr}/close")]
#[delete("/{nr}")]
pub async fn route(info: web::Path<String>, data: web::Data<AppState>) -> Result<impl Responder> {
let nr = info.into_inner();

View file

@ -1,4 +1,4 @@
use actix_web::{post, web, Responder, Result};
use actix_web::{post, web, HttpResponse, Responder, Result};
use serde::{Deserialize, Serialize};
use crate::AppState;
@ -25,5 +25,5 @@ pub async fn route(
let nr = bank.create_account(owner.clone());
info!("created account {}", nr);
Ok(web::Json(AccountCreated { nr }))
Ok(HttpResponse::Created().json(AccountCreated { nr }))
}

View file

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{patch, web, HttpResponse, Responder, Result};
use serde::Serialize;
use bank::bank::Bank;
@ -12,7 +12,7 @@ struct AccountCreated {
nr: String,
}
#[post("/{nr}/deposit")]
#[patch("/{nr}/deposit")]
pub async fn route(
info: web::Path<String>,
form: web::Form<AmountData>,

View file

@ -36,9 +36,9 @@ impl ResponseError for HttpAccountError {
match self.deref() {
AccountError::NotFound => StatusCode::NOT_FOUND,
AccountError::InvalidAmount => StatusCode::BAD_REQUEST,
AccountError::Inactive => StatusCode::BAD_REQUEST,
AccountError::Inactive => StatusCode::GONE,
AccountError::Overdraw => StatusCode::BAD_REQUEST,
AccountError::AccountNotZero => StatusCode::EXPECTATION_FAILED,
AccountError::AccountNotZero => StatusCode::BAD_REQUEST,
}
}

View file

@ -1,32 +1,13 @@
use std::ops::Deref;
use actix_web::{get, web, Responder, Result};
use serde::Serialize;
use bank::account::{Account, AccountError};
use bank::account::AccountError;
use http_lib::json_account::JsonAccount;
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();

View file

@ -0,0 +1,31 @@
use actix_web::{head, web, HttpResponse, Responder, Result};
use bank::account::AccountError;
use serde::Serialize;
use crate::handlers::error::HttpAccountError;
use crate::AppState;
#[derive(Serialize)]
struct AccountCreated {
nr: String,
}
#[head("/{nr}")]
pub async fn route(info: web::Path<String>, data: web::Data<AppState>) -> Result<impl Responder> {
let nr = info.into_inner();
info!("checking 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();
if account.is_active {
Ok(HttpResponse::Ok().finish())
} else {
Err(HttpAccountError(AccountError::Inactive).into())
}
}
}
}

View file

@ -6,8 +6,10 @@ pub mod deposit;
pub mod error;
pub mod get_account;
pub mod get_account_nrs;
pub mod is_active;
pub mod pong;
pub mod transfer;
pub mod update_account;
pub mod withdraw;
#[derive(Deserialize, Serialize)]

View file

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{patch, web, HttpResponse, Responder, Result};
use serde::{Deserialize, Serialize};
use bank::account::AccountError;
@ -13,7 +13,7 @@ pub struct TransferData {
pub(crate) amount: f64,
}
#[post("/transfer")]
#[patch("/transfer")]
pub async fn route(
form: web::Form<TransferData>,
data: web::Data<AppState>,

View file

@ -0,0 +1,39 @@
use actix_web::{put, web, HttpResponse, Responder, Result};
use bank::account::AccountError;
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,
}
#[put("/{nr}")]
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;
if amount <= 0_f64 {
return Err(HttpAccountError(AccountError::InvalidAmount).into());
}
info!("updating account {}...", nr);
let bank = data.bank.read().unwrap();
match Bank::account_action(bank, &nr, |account| {
account.balance = amount;
Ok(amount)
}) {
Err(e) => Err(HttpAccountError(e).into()),
Ok(_) => Ok(HttpResponse::Ok().finish()),
}
}

View file

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse, Responder, Result};
use actix_web::{patch, web, HttpResponse, Responder, Result};
use serde::Serialize;
use bank::bank::Bank;
@ -12,7 +12,7 @@ struct AccountCreated {
nr: String,
}
#[post("/{nr}/withdraw")]
#[patch("/{nr}/withdraw")]
pub async fn route(
info: web::Path<String>,
form: web::Form<AmountData>,

View file

@ -3,6 +3,7 @@ extern crate log;
use std::sync::RwLock;
use actix_cors::Cors;
use actix_web::middleware::TrailingSlash;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{middleware, web, App, HttpServer};
@ -14,8 +15,10 @@ use crate::handlers::create_account;
use crate::handlers::deposit;
use crate::handlers::get_account;
use crate::handlers::get_account_nrs;
use crate::handlers::is_active;
use crate::handlers::pong;
use crate::handlers::transfer;
use crate::handlers::update_account;
use crate::handlers::withdraw;
pub struct AppState {
@ -34,7 +37,9 @@ fn config_app(app_data: Data<AppState>) -> Box<dyn Fn(&mut ServiceConfig)> {
.service(close_account::route)
.service(get_account_nrs::route)
.service(withdraw::route)
.service(transfer::route),
.service(transfer::route)
.service(is_active::route)
.service(update_account::route),
);
})
}
@ -48,11 +53,14 @@ async fn main() -> std::io::Result<()> {
});
HttpServer::new(move || {
let cors = Cors::permissive();
App::new()
.wrap(cors)
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim))
.configure(config_app(app_data.clone()))
})
.bind(("127.0.0.1", 8080))?
.bind(("127.0.0.1", 8000))?
.run()
.await
}
@ -60,6 +68,8 @@ async fn main() -> std::io::Result<()> {
#[cfg(test)]
mod tests {
use actix_web::body::to_bytes;
use actix_web::dev::Service;
use actix_web::http::{Method as HttpMethod, StatusCode};
use actix_web::{test, App};
use serde_json::Value;
@ -92,7 +102,7 @@ mod tests {
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
assert_eq!(resp.status(), StatusCode::CREATED);
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
@ -100,15 +110,29 @@ mod tests {
assert!(!nr.is_empty());
let payload = AmountData { amount: 10_f64 };
let req = test::TestRequest::post()
let req = test::TestRequest::default()
.method(HttpMethod::HEAD)
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::default()
.method(HttpMethod::HEAD)
.uri("/accounts/muh")
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let payload = AmountData { amount: 5_f64 };
let req = test::TestRequest::patch()
.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()
let req = test::TestRequest::patch()
.uri(&format!("/accounts/{}/deposit", nr))
.set_form(&payload)
.to_request();
@ -121,6 +145,40 @@ mod tests {
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 payload = AmountData { amount: 10_f64 };
let req = test::TestRequest::put()
.uri(&format!("/accounts/{}", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::put()
.uri("/accounts/muh")
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_client_error());
let payload = AmountData { amount: -10_f64 };
let req = test::TestRequest::put()
.uri(&format!("/accounts/{}", nr))
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_client_error());
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();
@ -134,7 +192,7 @@ mod tests {
.set_form(&payload)
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
assert_eq!(resp.status(), StatusCode::CREATED);
let body_bytes = to_bytes(resp.into_body()).await.unwrap();
let response_json: Value = serde_json::from_slice(&body_bytes).unwrap();
@ -145,7 +203,7 @@ mod tests {
to: nr2.to_string(),
amount: 5_f64,
};
let req = test::TestRequest::post()
let req = test::TestRequest::patch()
.uri("/accounts/transfer")
.set_form(&payload)
.to_request();
@ -163,14 +221,14 @@ mod tests {
let balance = response_json["balance"].as_f64().unwrap();
assert_eq!(5_f64, balance);
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/close", nr))
let req = test::TestRequest::delete()
.uri(&format!("/accounts/{}", 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()
let req = test::TestRequest::patch()
.uri(&format!("/accounts/{}/withdraw", nr))
.set_form(&payload)
.to_request();
@ -186,12 +244,19 @@ mod tests {
let nrs = response_json.as_array().unwrap();
assert_eq!(2, nrs.len());
let req = test::TestRequest::post()
.uri(&format!("/accounts/{}/close", nr))
let req = test::TestRequest::delete()
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let req = test::TestRequest::default()
.method(HttpMethod::HEAD)
.uri(&format!("/accounts/{}", nr))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::GONE);
let req = test::TestRequest::get().uri("/accounts").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());