use evorule_reactor::Fact;
use evorule_tcb::JsonValue;
use serde::{Deserialize, Serialize};
fn json_value_to_serde(v: &JsonValue) -> serde_json::Value {
match v {
JsonValue::Null => serde_json::Value::Null,
JsonValue::Bool(b) => serde_json::Value::Bool(*b),
JsonValue::Integer(i) => serde_json::Value::Number((*i).into()),
JsonValue::String(s) => serde_json::Value::String(s.clone()),
JsonValue::Array(arr) => {
serde_json::Value::Array(arr.iter().map(json_value_to_serde).collect())
}
JsonValue::Object(map) => {
let mut obj = serde_json::Map::new();
for (k, v) in map.iter() {
obj.insert(k.clone(), json_value_to_serde(v));
}
serde_json::Value::Object(obj)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RewindSnapshot {
pub payload: serde_json::Value,
pub queue: Vec<serde_json::Value>,
pub version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PayloadDiff {
pub added: Vec<(String, serde_json::Value)>,
pub removed: Vec<(String, serde_json::Value)>,
pub changed: Vec<(String, serde_json::Value, serde_json::Value)>,
pub unchanged: Vec<String>,
}
impl PayloadDiff {
pub fn is_empty(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
}
pub fn change_count(&self) -> usize {
self.added.len() + self.removed.len() + self.changed.len()
}
pub fn summary(&self) -> String {
format!(
"diff: +{} -{} ~{} (={} unchanged)",
self.added.len(),
self.removed.len(),
self.changed.len(),
self.unchanged.len()
)
}
}
pub(crate) fn apply_payload_update(payload: &mut JsonValue, path: &str, value: JsonValue) {
if let Some(target) = evorule_tcb::path::resolve_path_mut(payload, path) {
*target = value;
return;
}
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return;
}
let field = match parts.last() {
Some(f) => *f,
None => return,
};
let parent_obj = if parts.len() == 1 {
if let JsonValue::Object(map) = payload {
map
} else {
return;
}
} else {
let mut current = payload;
for &part in &parts[0..parts.len() - 1] {
if let JsonValue::Object(map) = current {
if !map.contains_key(part) {
map.insert(part.to_string(), JsonValue::empty_object());
}
current = match map.get_mut(part) {
Some(v) => v,
None => return,
};
} else {
return;
}
}
if let JsonValue::Object(map) = current {
map
} else {
return;
}
};
parent_obj.insert(field.to_string(), value);
}
pub fn rewind(facts: &[Fact], target_version: u64) -> Option<RewindSnapshot> {
if target_version == 0 {
return Some(RewindSnapshot {
payload: serde_json::Value::Object(serde_json::Map::new()),
queue: Vec::new(),
version: 0,
});
}
let mut payload = JsonValue::empty_object();
let mut queue: Vec<JsonValue> = Vec::new();
let mut version: u64 = 0;
for fact in facts {
match fact {
Fact::StateTransition {
new_payload,
new_queue,
..
} => {
payload = new_payload.clone();
queue = new_queue.clone();
version += 1;
}
Fact::IoResponse { .. } => {
version += 1;
}
Fact::PayloadUpdate { path, value, .. } => {
apply_payload_update(&mut payload, path, value.clone());
version += 1;
}
_ => {}
}
if version == target_version {
break;
}
}
if version < target_version {
return None;
}
Some(RewindSnapshot {
payload: json_value_to_serde(&payload),
queue: queue.iter().map(json_value_to_serde).collect(),
version,
})
}
pub fn diff(facts: &[Fact], v_a: u64, v_b: u64) -> PayloadDiff {
let payload_a = rewind(facts, v_a)
.map(|s| s.payload)
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
let payload_b = rewind(facts, v_b)
.map(|s| s.payload)
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
compute_diff(&payload_a, &payload_b)
}
fn compute_diff(payload_a: &serde_json::Value, payload_b: &serde_json::Value) -> PayloadDiff {
let mut added = Vec::new();
let mut removed = Vec::new();
let mut changed = Vec::new();
let mut unchanged = Vec::new();
if let Some(map_a) = payload_a.as_object() {
for (key, val_a) in map_a {
match payload_b.as_object().and_then(|m| m.get(key)) {
Some(val_b) => {
if val_a == val_b {
unchanged.push(key.clone());
} else {
changed.push((key.clone(), val_a.clone(), val_b.clone()));
}
}
None => {
removed.push((key.clone(), val_a.clone()));
}
}
}
}
if let Some(map_b) = payload_b.as_object() {
for (key, val_b) in map_b {
if payload_a.as_object().and_then(|m| m.get(key)).is_none() {
added.push((key.clone(), val_b.clone()));
}
}
}
PayloadDiff {
added,
removed,
changed,
unchanged,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use evorule_reactor::FactId;
use evorule_tcb::JsonValue;
fn serde_to_json(v: serde_json::Value) -> JsonValue {
match v {
serde_json::Value::Null => JsonValue::Null,
serde_json::Value::Bool(b) => JsonValue::Bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
JsonValue::Integer(i)
} else {
JsonValue::Null
}
}
serde_json::Value::String(s) => JsonValue::String(s),
serde_json::Value::Array(arr) => {
JsonValue::Array(arr.into_iter().map(serde_to_json).collect())
}
serde_json::Value::Object(map) => {
let mut obj = std::collections::BTreeMap::new();
for (k, v) in map {
obj.insert(k, serde_to_json(v));
}
JsonValue::Object(obj)
}
}
}
fn make_state_transition(version: u64, payload: serde_json::Value) -> Fact {
Fact::StateTransition {
id: FactId(version),
cause: FactId(version.saturating_sub(1)),
new_payload: serde_to_json(payload),
new_queue: vec![],
}
}
fn make_io_response(version: u64) -> Fact {
Fact::IoResponse {
id: FactId(version),
request_id: FactId(version.saturating_sub(1)),
result: JsonValue::Null,
error: None,
}
}
#[test]
fn test_rewind_zero_returns_empty() {
let facts: Vec<Fact> = vec![];
let result = rewind(&facts, 0);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 0);
assert!(snap.payload.as_object().unwrap().is_empty());
assert!(snap.queue.is_empty());
}
#[test]
fn test_rewind_basic_state_transition() {
let facts = vec![make_state_transition(1, serde_json::json!({"amount": 100}))];
let result = rewind(&facts, 1);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 1);
assert_eq!(snap.payload, serde_json::json!({"amount": 100}));
}
#[test]
fn test_rewind_multiple_state_transitions() {
let facts = vec![
make_state_transition(1, serde_json::json!({"step": 1})),
make_state_transition(2, serde_json::json!({"step": 2})),
make_state_transition(3, serde_json::json!({"step": 3})),
];
let result = rewind(&facts, 2);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 2);
assert_eq!(snap.payload, serde_json::json!({"step": 2}));
}
#[test]
fn test_rewind_io_response_increments_version() {
let facts = vec![
make_state_transition(1, serde_json::json!({"amount": 50})),
make_io_response(2),
];
let result = rewind(&facts, 2);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 2);
assert_eq!(snap.payload, serde_json::json!({"amount": 50}));
}
#[test]
fn test_rewind_out_of_range_returns_none() {
let facts = vec![make_state_transition(1, serde_json::json!({"amount": 100}))];
let result = rewind(&facts, 5);
assert!(result.is_none());
}
#[test]
fn test_diff_added_field() {
let facts = vec![
make_state_transition(1, serde_json::json!({"a": 1})),
make_state_transition(2, serde_json::json!({"a": 1, "b": 2})),
];
let diff_result = diff(&facts, 1, 2);
assert_eq!(diff_result.added.len(), 1);
assert_eq!(diff_result.added[0].0, "b");
assert_eq!(diff_result.removed.len(), 0);
assert_eq!(diff_result.changed.len(), 0);
assert_eq!(diff_result.unchanged.len(), 1);
assert_eq!(diff_result.unchanged[0], "a");
}
#[test]
fn test_diff_removed_field() {
let facts = vec![
make_state_transition(1, serde_json::json!({"a": 1, "b": 2})),
make_state_transition(2, serde_json::json!({"a": 1})),
];
let diff_result = diff(&facts, 1, 2);
assert_eq!(diff_result.removed.len(), 1);
assert_eq!(diff_result.removed[0].0, "b");
assert_eq!(diff_result.added.len(), 0);
assert_eq!(diff_result.changed.len(), 0);
}
#[test]
fn test_diff_changed_field() {
let facts = vec![
make_state_transition(1, serde_json::json!({"a": 1})),
make_state_transition(2, serde_json::json!({"a": 99})),
];
let diff_result = diff(&facts, 1, 2);
assert_eq!(diff_result.changed.len(), 1);
assert_eq!(diff_result.changed[0].0, "a");
assert_eq!(diff_result.changed[0].1, serde_json::json!(1));
assert_eq!(diff_result.changed[0].2, serde_json::json!(99));
assert_eq!(diff_result.added.len(), 0);
assert_eq!(diff_result.removed.len(), 0);
}
#[test]
fn test_diff_identical_payloads() {
let facts = vec![
make_state_transition(1, serde_json::json!({"a": 1, "b": 2})),
make_state_transition(2, serde_json::json!({"a": 1, "b": 2})),
];
let diff_result = diff(&facts, 1, 2);
assert!(diff_result.is_empty());
assert_eq!(diff_result.unchanged.len(), 2);
}
#[test]
fn test_payload_diff_summary() {
let diff_result = PayloadDiff {
added: vec![("x".to_string(), serde_json::json!(1))],
removed: vec![("y".to_string(), serde_json::json!(2))],
changed: vec![("z".to_string(), serde_json::json!(3), serde_json::json!(4))],
unchanged: vec!["w".to_string()],
};
let summary = diff_result.summary();
assert!(summary.contains("+1"));
assert!(summary.contains("-1"));
assert!(summary.contains("~1"));
assert!(summary.contains("=1"));
}
fn make_payload_update(version: u64, path: &str, value: serde_json::Value) -> Fact {
Fact::PayloadUpdate {
id: FactId(version),
path: path.to_string(),
value: serde_to_json(value),
}
}
#[test]
fn test_rewind_payload_update_applied() {
let facts = vec![
make_state_transition(1, serde_json::json!({"amount": 100})),
make_payload_update(2, "amount", serde_json::json!(200)),
];
let result = rewind(&facts, 2);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 2);
assert_eq!(snap.payload, serde_json::json!({"amount": 200}));
}
#[test]
fn test_rewind_payload_update_version_increment() {
let facts = vec![
make_state_transition(1, serde_json::json!({"x": 1})),
make_payload_update(2, "y", serde_json::json!(2)),
];
let result = rewind(&facts, 1);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 1);
assert_eq!(snap.payload, serde_json::json!({"x": 1}));
let result = rewind(&facts, 2);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 2);
assert_eq!(snap.payload, serde_json::json!({"x": 1, "y": 2}));
}
#[test]
fn test_rewind_mixed_facts() {
let facts = vec![
make_state_transition(1, serde_json::json!({"a": 1})),
make_io_response(2),
make_payload_update(3, "b", serde_json::json!(2)),
make_state_transition(4, serde_json::json!({"a": 1, "b": 2, "c": 3})),
];
let result = rewind(&facts, 3);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 3);
assert_eq!(snap.payload, serde_json::json!({"a": 1, "b": 2}));
let result = rewind(&facts, 4);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 4);
assert_eq!(snap.payload, serde_json::json!({"a": 1, "b": 2, "c": 3}));
}
#[test]
fn test_rewind_payload_update_nested_path() {
let facts = vec![
make_state_transition(1, serde_json::json!({})),
make_payload_update(2, "user.profile.name", serde_json::json!("Alice")),
];
let result = rewind(&facts, 2);
assert!(result.is_some());
let snap = result.unwrap();
assert_eq!(snap.version, 2);
assert_eq!(
snap.payload,
serde_json::json!({"user": {"profile": {"name": "Alice"}}})
);
}
#[test]
fn test_diff_with_payload_update() {
let facts = vec![
make_state_transition(1, serde_json::json!({"a": 1})),
make_payload_update(2, "b", serde_json::json!(2)),
];
let diff_result = diff(&facts, 1, 2);
assert_eq!(diff_result.added.len(), 1);
assert_eq!(diff_result.added[0].0, "b");
assert_eq!(diff_result.removed.len(), 0);
assert_eq!(diff_result.changed.len(), 0);
assert_eq!(diff_result.unchanged.len(), 1);
assert_eq!(diff_result.unchanged[0], "a");
}
#[test]
fn test_diff_payload_update_changed_field() {
let facts = vec![
make_state_transition(1, serde_json::json!({"a": 1})),
make_payload_update(2, "a", serde_json::json!(99)),
];
let diff_result = diff(&facts, 1, 2);
assert_eq!(diff_result.changed.len(), 1);
assert_eq!(diff_result.changed[0].0, "a");
assert_eq!(diff_result.changed[0].1, serde_json::json!(1));
assert_eq!(diff_result.changed[0].2, serde_json::json!(99));
assert_eq!(diff_result.added.len(), 0);
assert_eq!(diff_result.removed.len(), 0);
}
}