use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledArtifact {
pub root_id: u64,
pub view: SerializedView,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializedView {
pub view_type: String,
pub props: serde_json::Value,
pub children: Vec<SerializedView>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RuntimePatch {
ReplaceView {
node_id: u64,
new_view: SerializedView,
},
UpdateState {
node_id: u64,
field: String,
value: serde_json::Value,
},
Batch(Vec<RuntimePatch>),
}
pub struct PatchEngine {
previous_view: Option<SerializedView>,
}
impl Default for PatchEngine {
fn default() -> Self {
Self::new()
}
}
impl PatchEngine {
pub fn new() -> Self {
Self {
previous_view: None,
}
}
pub fn generate_patch(&mut self, artifact: CompiledArtifact) -> RuntimePatch {
let mut patches = Vec::new();
if let Some(prev) = &self.previous_view {
self.diff_recursive(artifact.root_id, prev, &artifact.view, &mut patches);
} else {
patches.push(RuntimePatch::ReplaceView {
node_id: artifact.root_id,
new_view: artifact.view.clone(),
});
}
self.previous_view = Some(artifact.view);
if patches.len() == 1 {
patches.remove(0)
} else {
RuntimePatch::Batch(patches)
}
}
fn diff_recursive(
&self,
node_id: u64,
old: &SerializedView,
new: &SerializedView,
patches: &mut Vec<RuntimePatch>,
) {
if old.view_type != new.view_type {
patches.push(RuntimePatch::ReplaceView {
node_id,
new_view: new.clone(),
});
return;
}
if old.props != new.props || old.children.len() != new.children.len() {
patches.push(RuntimePatch::ReplaceView {
node_id,
new_view: new.clone(),
});
return;
}
for (i, (old_child, new_child)) in old.children.iter().zip(new.children.iter()).enumerate()
{
let child_id = node_id * 100 + (i as u64 + 1);
self.diff_recursive(child_id, old_child, new_child, patches);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_patch_engine_first_run() {
let mut engine = PatchEngine::new();
let artifact = CompiledArtifact {
root_id: 1,
view: SerializedView {
view_type: "Text".to_string(),
props: json!({"text": "Hello"}),
children: vec![],
},
};
let patch = engine.generate_patch(artifact);
if let RuntimePatch::ReplaceView { node_id, .. } = patch {
assert_eq!(node_id, 1);
} else {
panic!("Expected ReplaceView patch");
}
}
#[test]
fn test_patch_engine_diff_same() {
let mut engine = PatchEngine::new();
let view = SerializedView {
view_type: "Text".to_string(),
props: json!({"text": "Hello"}),
children: vec![],
};
let artifact1 = CompiledArtifact { root_id: 1, view: view.clone() };
let artifact2 = CompiledArtifact { root_id: 1, view: view.clone() };
engine.generate_patch(artifact1);
let patch = engine.generate_patch(artifact2);
if let RuntimePatch::Batch(patches) = patch {
assert!(patches.is_empty());
} else {
}
}
}