add conditional put
This commit is contained in:
parent
b4fcbdfef8
commit
547b7b375a
@ -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<H: Hasher>(&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<H: Hasher>(&self, state: &mut H) {
|
||||
self.number.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Account {
|
||||
#[cfg(test)]
|
||||
pub fn new() -> Self {
|
||||
|
@ -13,11 +13,11 @@ impl Bank {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn account_action<F: Fn(&mut Account) -> Result<f64, AccountError>>(
|
||||
pub fn account_action<F: Fn(&mut Account) -> Result<R, AccountError>, R>(
|
||||
bank: RwLockReadGuard<'_, Bank>,
|
||||
nr: &str,
|
||||
action: F,
|
||||
) -> Result<f64, AccountError> {
|
||||
) -> Result<R, AccountError> {
|
||||
match bank.accounts.get(nr) {
|
||||
None => Err(AccountError::NotFound),
|
||||
Some(account) => {
|
||||
|
@ -27,8 +27,14 @@ pub async fn fetch_account(host: &str, nr: &str) -> Result<JsonAccount, FetchErr
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_balance(host: &str, nr: &str, balance: f64) -> 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(())
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ pub async fn get_json(url: &str) -> Result<Option<String>, 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<String>) -> 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(())
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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<String>,
|
||||
form: web::Form<AmountData>,
|
||||
data: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
) -> Result<impl Responder> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user