use std::collections::HashMap;
use std::path::Path;
use std::sync::{Mutex, OnceLock};
use ikigai_core::{Error, ReprType, Representation, Result};
pub(crate) const HTTP_ENTITY: &str = "application/vnd.ikigai.http-entity";
pub(crate) const OUTCOME: &str = "outcome";
pub(crate) const ALREADY: &str = "already";
const MAX_REMEMBERED: usize = 4096;
fn acted() -> &'static Mutex<HashMap<String, i64>> {
static ACTED: OnceLock<Mutex<HashMap<String, i64>>> = OnceLock::new();
ACTED.get_or_init(Default::default)
}
pub(crate) fn claim(scope: &str, key: &Path, token: &str, exp: i64, now: i64) -> Result<bool> {
let mut acted = acted().lock().unwrap_or_else(|e| e.into_inner());
acted.retain(|_, until| *until > now);
let key = name(scope, key, token);
if acted.contains_key(&key) {
return Ok(false);
}
if acted.len() >= MAX_REMEMBERED {
return Err(Error::Unavailable(format!(
"{MAX_REMEMBERED} decisions are remembered and none has expired; try again later"
)));
}
acted.insert(key, exp);
Ok(true)
}
pub(crate) fn release(scope: &str, key: &Path, token: &str) {
let mut acted = acted().lock().unwrap_or_else(|e| e.into_inner());
acted.remove(&name(scope, key, token));
}
fn name(scope: &str, key: &Path, token: &str) -> String {
format!("{scope}|{}|{token}", key.display())
}
pub(crate) fn see_other(word: &'static str, page: Representation) -> Representation {
debug_assert!(word.bytes().all(|b| b.is_ascii_lowercase() || b == b'-'));
let mut bytes = format!(
"Content-Type: {}\r\nLocation: ?{OUTCOME}={word}\r\n\r\n",
page.repr_type
)
.into_bytes();
bytes.extend_from_slice(&page.bytes);
Representation::new(ReprType::new(HTTP_ENTITY), bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_decision_is_claimed_once_until_its_link_expires() {
let (token, key) = ("prg-test-token-once", Path::new("/k/once.pub"));
assert!(claim("t", key, token, 200, 100).unwrap());
assert!(
!claim("t", key, token, 200, 150).unwrap(),
"the replay finds it"
);
assert!(
claim("other", key, token, 200, 150).unwrap(),
"scoped by endpoint"
);
assert!(
claim("t", Path::new("/k/other.pub"), token, 200, 150).unwrap(),
"scoped by verifying key"
);
assert!(
claim("t", key, token, 300, 250).unwrap(),
"forgotten once expired"
);
}
#[test]
fn a_released_claim_can_be_taken_again() {
let (token, key) = ("prg-test-token-release", Path::new("/k/release.pub"));
assert!(claim("t", key, token, i64::MAX, 0).unwrap());
release("t", key, token);
assert!(claim("t", key, token, i64::MAX, 0).unwrap());
}
#[test]
fn see_other_wraps_the_page_with_a_fixed_location() {
let page = Representation::new(
ReprType::new("text/html").with_param("charset", "utf-8"),
b"<p>x</p>".to_vec(),
);
let answer = see_other("recorded", page);
assert_eq!(answer.repr_type.to_string(), HTTP_ENTITY);
assert_eq!(
answer.bytes,
b"Content-Type: text/html;charset=utf-8\r\nLocation: ?outcome=recorded\r\n\r\n<p>x</p>"
);
}
}