diff --git a/bank/src/account.rs b/bank/src/account.rs index 80dce03..10c3cd2 100644 --- a/bank/src/account.rs +++ b/bank/src/account.rs @@ -1,3 +1,4 @@ +use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use thiserror::Error; @@ -25,6 +26,23 @@ pub struct Account { pub is_active: bool, } +impl Account { + pub fn hash_string(&self) -> String { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + format!("{:x}", hasher.finish()) + } +} + +impl Hash for Account { + fn hash(&self, state: &mut H) { + self.number.hash(state); + self.owner.hash(state); + self.balance.to_string().hash(state); + self.is_active.hash(state); + } +} + impl Default for Account { fn default() -> Self { Account { @@ -42,12 +60,6 @@ impl PartialEq for Account { } } -impl Hash for Account { - fn hash(&self, state: &mut H) { - self.number.hash(state); - } -} - impl Account { #[cfg(test)] pub fn new() -> Self { diff --git a/bank/src/bank.rs b/bank/src/bank.rs index 556acc0..b3e62d8 100644 --- a/bank/src/bank.rs +++ b/bank/src/bank.rs @@ -13,11 +13,11 @@ impl Bank { Default::default() } - pub fn account_action Result>( + pub fn account_action Result, R>( bank: RwLockReadGuard<'_, Bank>, nr: &str, action: F, - ) -> Result { + ) -> Result { match bank.accounts.get(nr) { None => Err(AccountError::NotFound), Some(account) => { diff --git a/http-client/src/api.rs b/http-client/src/api.rs index e6347d5..523afd7 100644 --- a/http-client/src/api.rs +++ b/http-client/src/api.rs @@ -27,8 +27,14 @@ pub async fn fetch_account(host: &str, nr: &str) -> Result Result<(), FetchError> { +pub async fn set_balance( + host: &str, + nr: &str, + balance: f64, + etag: String, +) -> Result<(), FetchError> { let data = JsValue::from_str(&format!("amount={}", balance)); - client::put(&format! {"{}/accounts/{}", host, nr}, data).await?; + client::put(&format! {"{}/accounts/{}", host, nr}, data, Some(etag)).await?; + Ok(()) } diff --git a/http-client/src/client.rs b/http-client/src/client.rs index bc88630..7ab4729 100644 --- a/http-client/src/client.rs +++ b/http-client/src/client.rs @@ -53,7 +53,7 @@ pub async fn get_json(url: &str) -> Result, FetchError> { Ok(text.as_string()) } -pub async fn put(url: &str, data: JsValue) -> Result<(), FetchError> { +pub async fn put(url: &str, data: JsValue, if_match: Option) -> Result<(), FetchError> { let mut opts = RequestInit::new(); opts.method("PUT"); opts.mode(RequestMode::Cors); @@ -63,6 +63,11 @@ pub async fn put(url: &str, data: JsValue) -> Result<(), FetchError> { request .headers() .set("Content-Type", "application/x-www-form-urlencoded")?; + + if let Some(etag) = if_match { + request.headers().set("If-match", &etag)?; + } + self::send_request(request).await?; Ok(()) diff --git a/http-client/src/components/main.rs b/http-client/src/components/main.rs index 6b73c3a..b226be5 100644 --- a/http-client/src/components/main.rs +++ b/http-client/src/components/main.rs @@ -77,8 +77,17 @@ impl Component for Main { true } Event::SetBalance(balance, nr) => { + let acc = bank::account::Account { + number: nr.clone(), + owner: self.selected_owner.clone(), + balance: self.selected_balance, + is_active: true, + }; + + let hash = acc.hash_string(); + ctx.link().send_future(async move { - match api::set_balance("http://localhost:8000", &nr, balance).await { + match api::set_balance("http://localhost:8000", &nr, balance, hash).await { Err(e) => Event::ShowError(e.to_string()), Ok(_) => Event::SelectAccountNr(nr), } diff --git a/http-server/src/handlers/update_account.rs b/http-server/src/handlers/update_account.rs index a0cbaf6..76cd9c8 100644 --- a/http-server/src/handlers/update_account.rs +++ b/http-server/src/handlers/update_account.rs @@ -1,9 +1,7 @@ -use actix_web::{put, web, HttpResponse, Responder, Result}; +use actix_web::{put, web, HttpRequest, 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; @@ -18,6 +16,7 @@ pub async fn route( info: web::Path, form: web::Form, data: web::Data, + req: HttpRequest, ) -> Result { let nr = info.into_inner(); let amount = form.amount; @@ -29,11 +28,22 @@ pub async fn route( 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()), + match bank.accounts.get(&nr) { + None => Err(HttpAccountError(AccountError::NotFound).into()), + Some(account) => { + let mut account = account.write().unwrap(); + let hash = account.hash_string(); + if let Some(etag) = req.headers().get("If-Match") { + let etag = etag.to_str().unwrap_or(""); + if etag != hash { + Ok(HttpResponse::PreconditionFailed().finish()) + } else { + account.balance = amount; + Ok(HttpResponse::Ok().finish()) + } + } else { + Ok(HttpResponse::PreconditionRequired().finish()) + } + } } } diff --git a/http-server/src/main.rs b/http-server/src/main.rs index 0e41684..cbf07ae 100644 --- a/http-server/src/main.rs +++ b/http-server/src/main.rs @@ -155,6 +155,30 @@ mod tests { .set_form(&payload) .to_request(); let resp = test::call_service(&app, req).await; + assert_eq!(resp.status(), StatusCode::PRECONDITION_REQUIRED); + + let payload = AmountData { amount: 10_f64 }; + let req = test::TestRequest::put() + .uri(&format!("/accounts/{}", nr)) + .append_header((actix_web::http::header::IF_MATCH, "I aspire to be an etag")) + .set_form(&payload) + .to_request(); + let resp = test::call_service(&app, req).await; + assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED); + + let acc = bank::account::Account { + number: nr.to_string(), + owner: "aaa".to_string(), + balance: 5_f64, + is_active: true + }; + let payload = AmountData { amount: 10_f64 }; + let req = test::TestRequest::put() + .uri(&format!("/accounts/{}", nr)) + .append_header((actix_web::http::header::IF_MATCH, acc.hash_string())) + .set_form(&payload) + .to_request(); + let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); let req = test::TestRequest::put()