use serde::{Deserialize, Serialize};
use firewatch_core::NodeReport;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Kind {
Findings,
Unreachable,
Reachable,
}
impl Kind {
fn slot(self) -> u8 {
match self {
Kind::Findings => 0,
Kind::Unreachable | Kind::Reachable => 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Entry {
pub node: String,
pub kind: Kind,
pub text: String,
pub detected_at: u64,
pub waiting_since: u64,
pub catch_up_since: Option<u64>,
pub heard: Option<NodeReport>,
pub affected_since: Option<u64>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Outbox {
entries: Vec<Entry>,
}
impl Outbox {
pub fn from_entries(entries: Vec<Entry>) -> Self {
Outbox { entries }
}
pub fn entries(&self) -> &[Entry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn waiting(&self, node: &str, kind: Kind) -> Option<&Entry> {
self.entries
.iter()
.find(|e| e.node == node && e.kind.slot() == kind.slot())
}
pub fn put(&mut self, mut entry: Entry) {
match self
.entries
.iter_mut()
.find(|e| e.node == entry.node && e.kind.slot() == entry.kind.slot())
{
Some(old) => {
entry.waiting_since = old.waiting_since;
entry.catch_up_since = entry.catch_up_since.or(old.catch_up_since);
*old = entry;
}
None => self.entries.push(entry),
}
}
pub fn cancel(&mut self, node: &str, kind: Kind) {
self.entries
.retain(|e| !(e.node == node && e.kind.slot() == kind.slot()));
}
pub fn first(&self) -> Option<&Entry> {
self.entries.first()
}
pub fn sent(&mut self) {
if !self.entries.is_empty() {
self.entries.remove(0);
}
}
pub fn oldest(&self) -> Option<u64> {
self.entries.iter().map(|e| e.waiting_since).min()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(node: &str, kind: Kind, text: &str, at: u64) -> Entry {
Entry {
node: node.into(),
kind,
text: text.into(),
detected_at: at,
waiting_since: at,
catch_up_since: None,
heard: None,
affected_since: None,
}
}
#[test]
fn a_newer_message_replaces_the_waiting_one_in_place() {
let mut outbox = Outbox::default();
outbox.put(entry("a", Kind::Findings, "a red", 10));
outbox.put(entry("b", Kind::Findings, "b red", 20));
outbox.put(entry("a", Kind::Findings, "a amber", 30));
assert_eq!(outbox.len(), 2);
let first = outbox.first().unwrap();
assert_eq!((first.text.as_str(), first.detected_at), ("a amber", 30));
assert_eq!(first.waiting_since, 10);
assert_eq!(outbox.oldest(), Some(10));
}
#[test]
fn findings_and_reachability_wait_separately() {
let mut outbox = Outbox::default();
outbox.put(entry("a", Kind::Findings, "red", 10));
outbox.put(entry("a", Kind::Unreachable, "down", 20));
assert_eq!(outbox.len(), 2);
outbox.cancel("a", Kind::Reachable);
assert_eq!(outbox.len(), 1);
assert!(outbox.waiting("a", Kind::Unreachable).is_none());
}
#[test]
fn sent_removes_only_the_first() {
let mut outbox = Outbox::default();
outbox.put(entry("a", Kind::Findings, "1", 10));
outbox.put(entry("b", Kind::Findings, "2", 20));
outbox.sent();
assert_eq!(outbox.first().unwrap().text, "2");
outbox.sent();
outbox.sent();
assert_eq!(outbox.len(), 0);
assert_eq!(outbox.oldest(), None);
}
}