use std::fmt;
use std::sync::Mutex;
pub trait ConsistencyTokenStore: Send + Sync + fmt::Debug {
fn get(&self) -> String;
fn set(&self, token: String);
fn replace(&self, presented: &str, answered: String) {
let _ = presented;
self.set(answered);
}
fn remove(&self, refused: &str) {
if self.get() == refused {
self.set(String::new());
}
}
}
#[derive(Default)]
pub struct ConsistencyTokenCell(Mutex<String>);
impl ConsistencyTokenCell {
pub fn new() -> Self {
Self::default()
}
fn lock(&self) -> std::sync::MutexGuard<'_, String> {
self.0.lock().unwrap_or_else(|e| e.into_inner())
}
}
impl ConsistencyTokenStore for ConsistencyTokenCell {
fn get(&self) -> String {
self.lock().clone()
}
fn set(&self, token: String) {
*self.lock() = token;
}
fn remove(&self, refused: &str) {
let mut held = self.lock();
if *held == refused {
held.clear();
}
}
}
impl fmt::Debug for ConsistencyTokenCell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConsistencyTokenCell")
.field("holds_token", &!self.lock().is_empty())
.finish()
}
}
pub(crate) const MALFORMED_TOKEN: &str =
"the Origin-Consistency-Token header must be a token a response of this API carried";
pub(crate) fn is_malformed_token(err: &crate::Error) -> bool {
err.api()
.is_some_and(|e| e.http_status == Some(400) && e.message == MALFORMED_TOKEN)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Error;
use crate::error::ApiError;
#[test]
fn a_cell_forgets_a_refused_token_only_while_it_holds_it() {
let cell = ConsistencyTokenCell::new();
assert_eq!(cell.get(), "");
cell.set("v1.a".to_owned());
cell.remove("v1.b");
assert_eq!(cell.get(), "v1.a");
cell.remove("v1.a");
assert_eq!(cell.get(), "");
assert_eq!(
format!("{cell:?}"),
"ConsistencyTokenCell { holds_token: false }"
);
}
#[test]
fn the_default_remove_forgets_only_the_refused_token() {
#[derive(Debug, Default)]
struct Plain(Mutex<String>);
impl ConsistencyTokenStore for Plain {
fn get(&self) -> String {
self.0.lock().unwrap().clone()
}
fn set(&self, token: String) {
*self.0.lock().unwrap() = token;
}
}
let store = Plain::default();
store.set("v1.a".to_owned());
store.remove("v1.b");
assert_eq!(store.get(), "v1.a");
store.remove("v1.a");
assert_eq!(store.get(), "");
}
#[test]
fn only_the_malformed_token_refusal_is_one() {
let refusal = |status: u16, message: &str| {
Error::from(ApiError::from_response(
status,
serde_json::json!({"error": {"code": status, "status": "INVALID_ARGUMENT", "message": message}})
.to_string()
.as_bytes(),
))
};
assert!(is_malformed_token(&refusal(400, MALFORMED_TOKEN)));
assert!(!is_malformed_token(&refusal(
400,
"the request body must be at most 1 bytes"
)));
assert!(!is_malformed_token(&refusal(
400,
&format!("{MALFORMED_TOKEN}.")
)));
assert!(!is_malformed_token(&refusal(409, MALFORMED_TOKEN)));
assert!(!is_malformed_token(&Error::NoCredentials));
}
}