use std::collections::HashMap;
use std::sync::Mutex;
use crate::types::{Prefix, Said};
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
#[error(
"Conflicting event at prefix={prefix}, sn={sn}: first-seen SAID={first_seen}, new SAID={new_said}"
)]
pub struct FirstSeenConflict {
pub prefix: String,
pub sn: u64,
pub first_seen: String,
pub new_said: String,
}
pub trait FirstSeenPolicy: Send + Sync {
fn try_accept(&self, prefix: &Prefix, sn: u64, said: &Said) -> Result<(), FirstSeenConflict>;
fn was_seen(&self, prefix: &Prefix, sn: u64) -> Option<Said>;
fn try_supersede(
&self,
prefix: &Prefix,
sn: u64,
new_said: &Said,
is_new_establishment: bool,
) -> Result<(), FirstSeenConflict>;
}
pub struct InMemoryFirstSeen {
seen: Mutex<HashMap<(String, u64), SeenEntry>>,
}
#[derive(Clone)]
struct SeenEntry {
said: Said,
is_establishment: bool,
}
impl InMemoryFirstSeen {
pub fn new() -> Self {
Self {
seen: Mutex::new(HashMap::new()),
}
}
}
impl Default for InMemoryFirstSeen {
fn default() -> Self {
Self::new()
}
}
impl FirstSeenPolicy for InMemoryFirstSeen {
fn try_accept(&self, prefix: &Prefix, sn: u64, said: &Said) -> Result<(), FirstSeenConflict> {
let key = (prefix.as_str().to_string(), sn);
let mut seen = self.seen.lock().unwrap_or_else(|e| e.into_inner());
if let Some(existing) = seen.get(&key) {
if existing.said != *said {
return Err(FirstSeenConflict {
prefix: prefix.as_str().to_string(),
sn,
first_seen: existing.said.as_str().to_string(),
new_said: said.as_str().to_string(),
});
}
Ok(())
} else {
seen.insert(
key,
SeenEntry {
said: said.clone(),
is_establishment: false, },
);
Ok(())
}
}
fn was_seen(&self, prefix: &Prefix, sn: u64) -> Option<Said> {
let key = (prefix.as_str().to_string(), sn);
let seen = self.seen.lock().unwrap_or_else(|e| e.into_inner());
seen.get(&key).map(|e| e.said.clone())
}
fn try_supersede(
&self,
prefix: &Prefix,
sn: u64,
new_said: &Said,
is_new_establishment: bool,
) -> Result<(), FirstSeenConflict> {
let key = (prefix.as_str().to_string(), sn);
let mut seen = self.seen.lock().unwrap_or_else(|e| e.into_inner());
if let Some(existing) = seen.get(&key) {
if existing.said == *new_said {
return Ok(()); }
if is_new_establishment && !existing.is_establishment {
seen.insert(
key,
SeenEntry {
said: new_said.clone(),
is_establishment: true,
},
);
Ok(())
} else {
Err(FirstSeenConflict {
prefix: prefix.as_str().to_string(),
sn,
first_seen: existing.said.as_str().to_string(),
new_said: new_said.as_str().to_string(),
})
}
} else {
seen.insert(
key,
SeenEntry {
said: new_said.clone(),
is_establishment: is_new_establishment,
},
);
Ok(())
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn first_event_accepted() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let said = Said::new_unchecked("ESAID1".to_string());
assert!(policy.try_accept(&prefix, 0, &said).is_ok());
}
#[test]
fn same_event_idempotent() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let said = Said::new_unchecked("ESAID1".to_string());
policy.try_accept(&prefix, 0, &said).unwrap();
assert!(policy.try_accept(&prefix, 0, &said).is_ok());
}
#[test]
fn different_event_rejected() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let said1 = Said::new_unchecked("ESAID1".to_string());
let said2 = Said::new_unchecked("ESAID2".to_string());
policy.try_accept(&prefix, 0, &said1).unwrap();
let err = policy.try_accept(&prefix, 0, &said2).unwrap_err();
assert_eq!(err.first_seen, "ESAID1");
assert_eq!(err.new_said, "ESAID2");
}
#[test]
fn different_sequences_independent() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let said1 = Said::new_unchecked("ESAID1".to_string());
let said2 = Said::new_unchecked("ESAID2".to_string());
policy.try_accept(&prefix, 0, &said1).unwrap();
assert!(policy.try_accept(&prefix, 1, &said2).is_ok());
}
#[test]
fn was_seen_returns_said() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let said = Said::new_unchecked("ESAID1".to_string());
assert!(policy.was_seen(&prefix, 0).is_none());
policy.try_accept(&prefix, 0, &said).unwrap();
assert_eq!(policy.was_seen(&prefix, 0), Some(said));
}
#[test]
fn rotation_supersedes_interaction() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let ixn_said = Said::new_unchecked("ESAID_IXN".to_string());
let rot_said = Said::new_unchecked("ESAID_ROT".to_string());
policy.try_accept(&prefix, 1, &ixn_said).unwrap();
let result = policy.try_supersede(&prefix, 1, &rot_said, true);
assert!(result.is_ok());
assert_eq!(policy.was_seen(&prefix, 1), Some(rot_said));
}
#[test]
fn interaction_cannot_supersede_rotation() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let rot_said = Said::new_unchecked("ESAID_ROT".to_string());
let ixn_said = Said::new_unchecked("ESAID_IXN".to_string());
policy.try_supersede(&prefix, 1, &rot_said, true).unwrap();
let result = policy.try_supersede(&prefix, 1, &ixn_said, false);
assert!(result.is_err());
}
#[test]
fn rotation_cannot_supersede_rotation() {
let policy = InMemoryFirstSeen::new();
let prefix = Prefix::new_unchecked("ETest".to_string());
let rot1 = Said::new_unchecked("ESAID_ROT1".to_string());
let rot2 = Said::new_unchecked("ESAID_ROT2".to_string());
policy.try_supersede(&prefix, 1, &rot1, true).unwrap();
let result = policy.try_supersede(&prefix, 1, &rot2, true);
assert!(result.is_err());
}
}