use std::collections::{HashMap, HashSet};
use libjmap::ChangeStatus;
#[derive(Debug, Clone, Default)]
pub struct StateChanges {
pub created: HashSet<String>,
pub updated: HashSet<String>,
pub destroyed: HashSet<String>,
}
impl StateChanges {
pub fn new() -> Self {
Self::default()
}
pub fn created(item_id: String) -> Self {
let mut changes = Self::new();
changes.created.insert(item_id);
changes
}
pub fn updated(item_id: String) -> Self {
let mut changes = Self::new();
changes.updated.insert(item_id);
changes
}
pub fn destroyed(item_id: String) -> Self {
let mut changes = Self::new();
changes.destroyed.insert(item_id);
changes
}
}
#[derive(Debug, Clone)]
pub struct StateNode {
pub parent_state: Option<String>,
pub changes: StateChanges,
}
#[derive(Debug, Default)]
pub struct StateCache {
nodes: HashMap<String, StateNode>,
latest_state: Option<String>,
}
impl StateCache {
pub fn new() -> Self {
Self::default()
}
pub fn query_changes(&self, from_state: &str, item_id: &str) -> Option<ChangeStatus> {
let to_state = self.latest_state.as_ref()?;
if from_state == to_state {
return Some(ChangeStatus::NotChanged);
}
let mut cursor = to_state.as_str();
let mut accumulated_changes = StateChanges::new();
loop {
if cursor == from_state {
break;
}
let node = self.nodes.get(cursor)?;
accumulated_changes
.created
.extend(node.changes.created.iter().cloned());
accumulated_changes
.updated
.extend(node.changes.updated.iter().cloned());
accumulated_changes
.destroyed
.extend(node.changes.destroyed.iter().cloned());
cursor = node.parent_state.as_ref()?; }
if accumulated_changes.destroyed.contains(item_id) {
Some(ChangeStatus::Deleted)
} else if accumulated_changes.updated.contains(item_id)
|| accumulated_changes.created.contains(item_id)
{
Some(ChangeStatus::Changed)
} else {
Some(ChangeStatus::NotChanged)
}
}
pub fn add_transition(&mut self, old_state: String, new_state: String, changes: StateChanges) {
if !self.nodes.contains_key(&old_state) {
self.nodes.insert(
old_state.clone(),
StateNode {
parent_state: None,
changes: StateChanges::new(),
},
);
}
self.nodes.insert(
new_state.clone(),
StateNode {
parent_state: Some(old_state),
changes,
},
);
self.latest_state = Some(new_state);
}
pub fn latest_state(&self) -> Option<&str> {
self.latest_state.as_deref()
}
}
#[cfg(test)]
mod tests {
use libjmap::ChangeStatus;
use super::*;
#[test]
fn test_empty_cache() {
let cache = StateCache::new();
assert_eq!(cache.query_changes("state1", "item1"), None);
assert_eq!(cache.latest_state(), None);
}
#[test]
fn test_single_transition() {
let mut cache = StateCache::new();
let changes = StateChanges::updated("item1".to_string());
cache.add_transition("state1".to_string(), "state2".to_string(), changes);
assert_eq!(cache.latest_state(), Some("state2"));
assert_eq!(
cache.query_changes("state1", "item1"),
Some(ChangeStatus::Changed)
);
assert_eq!(
cache.query_changes("state1", "item2"),
Some(ChangeStatus::NotChanged)
);
}
#[test]
fn test_multiple_transitions() {
let mut cache = StateCache::new();
cache.add_transition(
"state1".to_string(),
"state2".to_string(),
StateChanges::updated("item1".to_string()),
);
cache.add_transition(
"state2".to_string(),
"state3".to_string(),
StateChanges::created("item2".to_string()),
);
cache.add_transition(
"state3".to_string(),
"state4".to_string(),
StateChanges::destroyed("item1".to_string()),
);
assert_eq!(cache.latest_state(), Some("state4"));
assert_eq!(
cache.query_changes("state1", "item1"),
Some(ChangeStatus::Deleted)
);
assert_eq!(
cache.query_changes("state1", "item2"),
Some(ChangeStatus::Changed)
);
assert_eq!(
cache.query_changes("state2", "item1"),
Some(ChangeStatus::Deleted)
);
assert_eq!(
cache.query_changes("state2", "item2"),
Some(ChangeStatus::Changed)
);
assert_eq!(
cache.query_changes("state2", "item1"),
Some(ChangeStatus::Deleted)
);
assert_eq!(
cache.query_changes("state1", "item3"),
Some(ChangeStatus::NotChanged)
);
}
#[test]
fn test_same_state() {
let mut cache = StateCache::new();
cache.add_transition(
"state1".to_string(),
"state2".to_string(),
StateChanges::updated("item1".to_string()),
);
assert_eq!(
cache.query_changes("state2", "item1"),
Some(ChangeStatus::NotChanged)
);
}
}