use serde_json::{Map, Value};
use crate::types::{NodeMeta, PatchOp, PatchOpKind, SlopNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubscriptionGapError {
pub expected: u64,
pub received: u64,
}
impl std::fmt::Display for SubscriptionGapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SLOP subscription gap: expected seq {}, got {}",
self.expected, self.received
)
}
}
impl std::error::Error for SubscriptionGapError {}
pub struct StateMirror {
tree: SlopNode,
version: u64,
seq: u64,
}
impl StateMirror {
pub fn new(tree: SlopNode, version: u64) -> Self {
Self { tree, version, seq: 0 }
}
pub fn new_with_seq(tree: SlopNode, version: u64, seq: u64) -> Self {
Self { tree, version, seq }
}
pub fn apply_patch(&mut self, ops: &[PatchOp], version: u64) {
for op in ops {
let segments = parse_path(&op.path);
apply_one(
&mut self.tree,
&segments,
&op.op,
op.value.as_ref(),
op.index,
);
}
self.version = version;
}
pub fn apply_patch_with_seq(
&mut self,
ops: &[PatchOp],
version: u64,
seq: u64,
) -> Result<(), SubscriptionGapError> {
let expected = self.seq + 1;
if seq != expected {
return Err(SubscriptionGapError {
expected,
received: seq,
});
}
self.seq = seq;
self.apply_patch(ops, version);
Ok(())
}
pub fn tree(&self) -> &SlopNode {
&self.tree
}
pub fn version(&self) -> u64 {
self.version
}
pub fn seq(&self) -> u64 {
self.seq
}
}
fn parse_path(path: &str) -> Vec<String> {
let trimmed = path.strip_prefix('/').unwrap_or(path);
if trimmed.is_empty() {
return Vec::new();
}
trimmed.split('/').map(String::from).collect()
}
fn apply_one(
node: &mut SlopNode,
segments: &[String],
op: &PatchOpKind,
value: Option<&Value>,
index: Option<usize>,
) {
if segments.is_empty() {
if let PatchOpKind::Replace = op {
if let Some(val) = value {
if let Ok(new_node) = serde_json::from_value::<SlopNode>(val.clone()) {
*node = new_node;
}
}
}
return;
}
let (first, rest) = (&segments[0], &segments[1..]);
match first.as_str() {
"properties" => apply_in_properties(node, rest, op, value),
"meta" => apply_in_meta(node, rest, op, value),
"affordances" => apply_in_affordances(node, rest, op, value),
"content_ref" => apply_in_content_ref(node, rest, op, value),
child_id => apply_in_children(node, child_id, rest, op, value, index),
}
}
fn apply_in_properties(
node: &mut SlopNode,
segments: &[String],
op: &PatchOpKind,
value: Option<&Value>,
) {
if segments.is_empty() {
match op {
PatchOpKind::Replace | PatchOpKind::Add => {
if let Some(val) = value {
if let Some(obj) = val.as_object() {
node.properties = Some(obj.clone());
}
}
}
PatchOpKind::Remove => {
node.properties = None;
}
PatchOpKind::Move => {}
}
return;
}
let key = crate::diff::unescape_pointer_segment(&segments[0]);
let props = node.properties.get_or_insert_with(Map::new);
if segments.len() == 1 {
match op {
PatchOpKind::Add | PatchOpKind::Replace => {
if let Some(val) = value {
props.insert(key, val.clone());
}
}
PatchOpKind::Remove => {
props.remove(key.as_str());
}
PatchOpKind::Move => {}
}
} else {
if let Some(v) = props.get_mut(key.as_str()) {
apply_in_value(v, &segments[1..], op, value);
}
}
}
fn apply_in_children(
node: &mut SlopNode,
child_id: &str,
rest: &[String],
op: &PatchOpKind,
value: Option<&Value>,
index: Option<usize>,
) {
let children = node.children.get_or_insert_with(Vec::new);
if rest.is_empty() {
match op {
PatchOpKind::Add => {
if let Some(val) = value {
if let Ok(child) = serde_json::from_value::<SlopNode>(val.clone()) {
match index {
Some(i) => {
let clamped = i.min(children.len());
children.insert(clamped, child);
}
None => children.push(child),
}
}
}
}
PatchOpKind::Replace => {
if let Some(val) = value {
if let Ok(child) = serde_json::from_value::<SlopNode>(val.clone()) {
if let Some(pos) = children.iter().position(|c| c.id == child_id) {
children[pos] = child;
} else {
children.push(child);
}
}
}
}
PatchOpKind::Remove => {
children.retain(|c| c.id != child_id);
}
PatchOpKind::Move => {
if let Some(dest) = index {
if let Some(pos) = children.iter().position(|c| c.id == child_id) {
let child = children.remove(pos);
let clamped = dest.min(children.len());
children.insert(clamped, child);
}
}
}
}
} else {
if let Some(child) = children.iter_mut().find(|c| c.id == child_id) {
apply_one(child, rest, op, value, index);
}
}
}
fn apply_in_meta(
node: &mut SlopNode,
segments: &[String],
op: &PatchOpKind,
value: Option<&Value>,
) {
if segments.is_empty() {
match op {
PatchOpKind::Replace | PatchOpKind::Add => {
if let Some(val) = value {
if let Ok(m) = serde_json::from_value::<NodeMeta>(val.clone()) {
node.meta = Some(m);
}
}
}
PatchOpKind::Remove => {
node.meta = None;
}
PatchOpKind::Move => {}
}
return;
}
let field = &segments[0];
let meta = node.meta.get_or_insert_with(NodeMeta::default);
if segments.len() == 1 {
match op {
PatchOpKind::Remove => {
set_meta_field(meta, field, None);
}
PatchOpKind::Add | PatchOpKind::Replace => {
set_meta_field(meta, field, value);
}
PatchOpKind::Move => {}
}
}
}
fn set_meta_field(meta: &mut NodeMeta, field: &str, value: Option<&Value>) {
match field {
"summary" => {
meta.summary = value.and_then(|v| v.as_str()).map(String::from);
}
"salience" => {
meta.salience = value.and_then(|v| v.as_f64());
}
"pinned" => {
meta.pinned = value.and_then(|v| v.as_bool());
}
"changed" => {
meta.changed = value.and_then(|v| v.as_bool());
}
"focus" => {
meta.focus = value.and_then(|v| v.as_bool());
}
"urgency" => {
meta.urgency = value.and_then(|v| serde_json::from_value(v.clone()).ok());
}
"reason" => {
meta.reason = value.and_then(|v| v.as_str()).map(String::from);
}
"total_children" => {
meta.total_children = value.and_then(|v| v.as_u64()).map(|n| n as usize);
}
"window" => {
meta.window = value.and_then(|v| {
let arr = v.as_array()?;
Some((
arr.first()?.as_u64()? as usize,
arr.get(1)?.as_u64()? as usize,
))
});
}
"created" => {
meta.created = value.and_then(|v| v.as_str()).map(String::from);
}
"updated" => {
meta.updated = value.and_then(|v| v.as_str()).map(String::from);
}
_ => {}
}
}
fn apply_in_affordances(
node: &mut SlopNode,
segments: &[String],
op: &PatchOpKind,
value: Option<&Value>,
) {
if segments.is_empty() {
match op {
PatchOpKind::Replace | PatchOpKind::Add => {
if let Some(val) = value {
if let Ok(affs) =
serde_json::from_value::<Vec<crate::types::Affordance>>(val.clone())
{
node.affordances = Some(affs);
}
}
}
PatchOpKind::Remove => {
node.affordances = None;
}
PatchOpKind::Move => {}
}
return;
}
if let Ok(idx) = segments[0].parse::<usize>() {
let affs = node.affordances.get_or_insert_with(Vec::new);
if segments.len() == 1 {
match op {
PatchOpKind::Add => {
if let Some(val) = value {
if let Ok(aff) =
serde_json::from_value::<crate::types::Affordance>(val.clone())
{
if idx <= affs.len() {
affs.insert(idx, aff);
} else {
affs.push(aff);
}
}
}
}
PatchOpKind::Replace => {
if let Some(val) = value {
if let Ok(aff) =
serde_json::from_value::<crate::types::Affordance>(val.clone())
{
if idx < affs.len() {
affs[idx] = aff;
}
}
}
}
PatchOpKind::Remove => {
if idx < affs.len() {
affs.remove(idx);
}
}
PatchOpKind::Move => {}
}
}
}
}
fn apply_in_content_ref(
node: &mut SlopNode,
segments: &[String],
op: &PatchOpKind,
value: Option<&Value>,
) {
if !segments.is_empty() {
return;
}
match op {
PatchOpKind::Replace | PatchOpKind::Add => {
if let Some(val) = value {
if let Ok(cr) = serde_json::from_value::<crate::types::ContentRef>(val.clone()) {
node.content_ref = Some(cr);
}
}
}
PatchOpKind::Remove => {
node.content_ref = None;
}
PatchOpKind::Move => {}
}
}
fn apply_in_value(
target: &mut Value,
segments: &[String],
op: &PatchOpKind,
value: Option<&Value>,
) {
if segments.is_empty() {
if let PatchOpKind::Replace | PatchOpKind::Add = op {
if let Some(val) = value {
*target = val.clone();
}
}
return;
}
let (first_raw, rest) = (&segments[0], &segments[1..]);
let first = crate::diff::unescape_pointer_segment(first_raw);
if let Some(obj) = target.as_object_mut() {
if rest.is_empty() {
match op {
PatchOpKind::Add | PatchOpKind::Replace => {
if let Some(val) = value {
obj.insert(first, val.clone());
}
}
PatchOpKind::Remove => {
obj.remove(first.as_str());
}
PatchOpKind::Move => {}
}
} else if let Some(child) = obj.get_mut(first.as_str()) {
apply_in_value(child, rest, op, value);
}
} else if let Some(arr) = target.as_array_mut() {
if let Ok(idx) = first.parse::<usize>() {
if rest.is_empty() {
match op {
PatchOpKind::Add => {
if let Some(val) = value {
if idx <= arr.len() {
arr.insert(idx, val.clone());
}
}
}
PatchOpKind::Replace => {
if let Some(val) = value {
if idx < arr.len() {
arr[idx] = val.clone();
}
}
}
PatchOpKind::Remove => {
if idx < arr.len() {
arr.remove(idx);
}
}
PatchOpKind::Move => {}
}
} else if idx < arr.len() {
apply_in_value(&mut arr[idx], rest, op, value);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{PatchOp, PatchOpKind, SlopNode};
use serde_json::json;
fn make_tree() -> SlopNode {
serde_json::from_value(json!({
"id": "app",
"type": "root",
"properties": {"label": "My App"},
"children": [
{
"id": "counter",
"type": "status",
"properties": {"count": 0}
}
]
}))
.unwrap()
}
#[test]
fn test_new_and_getters() {
let tree = make_tree();
let mirror = StateMirror::new(tree.clone(), 1);
assert_eq!(mirror.version(), 1);
assert_eq!(mirror.tree().id, "app");
}
#[test]
fn test_replace_property() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Replace,
path: "/counter/properties/count".into(),
value: Some(json!(42)),
index: None,
}],
2,
);
assert_eq!(mirror.version(), 2);
let counter = &mirror.tree().children.as_ref().unwrap()[0];
assert_eq!(counter.properties.as_ref().unwrap()["count"], 42);
}
#[test]
fn test_add_property() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Add,
path: "/counter/properties/label".into(),
value: Some(json!("Counter")),
index: None,
}],
2,
);
let counter = &mirror.tree().children.as_ref().unwrap()[0];
assert_eq!(counter.properties.as_ref().unwrap()["label"], "Counter");
}
#[test]
fn test_remove_child() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Remove,
path: "/counter".into(),
value: None,
index: None,
}],
2,
);
assert!(mirror.tree().children.as_ref().unwrap().is_empty());
}
#[test]
fn test_add_child() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Add,
path: "/settings".into(),
value: Some(json!({"id": "settings", "type": "group"})),
index: None,
}],
2,
);
let children = mirror.tree().children.as_ref().unwrap();
assert_eq!(children.len(), 2);
assert_eq!(children[1].id, "settings");
}
#[test]
fn test_set_meta_field() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Add,
path: "/counter/meta/salience".into(),
value: Some(json!(0.9)),
index: None,
}],
2,
);
let counter = &mirror.tree().children.as_ref().unwrap()[0];
assert_eq!(counter.meta.as_ref().unwrap().salience, Some(0.9));
}
#[test]
fn test_remove_meta_field() {
let mut tree = make_tree();
tree.meta = Some(NodeMeta {
summary: Some("hello".into()),
..NodeMeta::default()
});
let mut mirror = StateMirror::new(tree, 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Remove,
path: "/meta/summary".into(),
value: None,
index: None,
}],
2,
);
assert!(mirror.tree().meta.as_ref().unwrap().summary.is_none());
}
#[test]
fn test_add_content_ref_field() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Add,
path: "/counter/content_ref".into(),
value: Some(json!({
"type": "text",
"mime": "text/plain",
"summary": "42 bytes"
})),
index: None,
}],
2,
);
let counter = &mirror.tree().children.as_ref().unwrap()[0];
let cr = counter
.content_ref
.as_ref()
.expect("content_ref should be set on the node");
assert_eq!(cr.mime, "text/plain");
let children_len = counter.children.as_ref().map(|c| c.len()).unwrap_or(0);
assert_eq!(children_len, 0);
}
#[test]
fn test_remove_content_ref_field() {
let mut tree = make_tree();
tree.children.as_mut().unwrap()[0].content_ref = Some(crate::types::ContentRef {
content_type: crate::types::ContentType::Text,
mime: "text/plain".into(),
summary: "x".into(),
size: None,
uri: None,
preview: None,
encoding: None,
hash: None,
});
let mut mirror = StateMirror::new(tree, 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Remove,
path: "/counter/content_ref".into(),
value: None,
index: None,
}],
2,
);
let counter = &mirror.tree().children.as_ref().unwrap()[0];
assert!(counter.content_ref.is_none());
}
#[test]
fn test_field_keyword_routes_to_field_not_children() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[
PatchOp {
op: PatchOpKind::Add,
path: "/counter/affordances".into(),
value: Some(json!([{"action": "cancel"}])),
index: None,
},
PatchOp {
op: PatchOpKind::Add,
path: "/counter/meta".into(),
value: Some(json!({"summary": "done"})),
index: None,
},
],
2,
);
let counter = &mirror.tree().children.as_ref().unwrap()[0];
assert_eq!(counter.affordances.as_ref().unwrap().len(), 1);
assert_eq!(
counter.meta.as_ref().and_then(|m| m.summary.as_deref()),
Some("done")
);
let stray = counter
.children
.as_ref()
.map(|c| c.iter().any(|x| x.id.is_empty()))
.unwrap_or(false);
assert!(!stray, "field add must not create an id-less child");
}
#[test]
fn test_multiple_ops() {
let mut mirror = StateMirror::new(make_tree(), 1);
mirror.apply_patch(
&[
PatchOp {
op: PatchOpKind::Replace,
path: "/counter/properties/count".into(),
value: Some(json!(10)),
index: None,
},
PatchOp {
op: PatchOpKind::Add,
path: "/properties/version".into(),
value: Some(json!("2.0")),
index: None,
},
],
2,
);
let counter = &mirror.tree().children.as_ref().unwrap()[0];
assert_eq!(counter.properties.as_ref().unwrap()["count"], 10);
assert_eq!(mirror.tree().properties.as_ref().unwrap()["version"], "2.0");
}
#[test]
fn test_move_reorders_children() {
let tree = SlopNode {
id: "root".into(),
node_type: "root".into(),
properties: None,
children: Some(vec![
SlopNode::new("a", "item"),
SlopNode::new("b", "item"),
SlopNode::new("c", "item"),
]),
affordances: None,
meta: None,
content_ref: None,
};
let mut mirror = StateMirror::new(tree, 1);
mirror.apply_patch(
&[PatchOp {
op: PatchOpKind::Move,
path: "/c".into(),
value: None,
index: Some(0),
}],
2,
);
let ids: Vec<&str> = mirror
.tree()
.children
.as_ref()
.unwrap()
.iter()
.map(|c| c.id.as_str())
.collect();
assert_eq!(ids, vec!["c", "a", "b"]);
}
}