1use serde::{Deserialize, Serialize};
13use serde_json::Value as Json;
14
15use crate::node::{Key, Node};
16
17pub type NodePath = Vec<String>;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum PatchField {
26 Children,
28 Items,
30 Rows,
32 Nodes,
34 Lines,
36 Titles,
38 Datasets,
40 Data,
42 Panes,
44}
45
46#[allow(
49 clippy::large_enum_variant,
50 reason = "patch ops are built once per update; boxing every node adds noise"
51)]
52#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
53#[serde(untagged)]
54pub enum PatchItem {
55 Node(Node),
57 Item(Json),
59}
60
61#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
63#[serde(tag = "op", rename_all = "lowercase")]
64pub enum NodeOp {
65 Replace {
67 path: NodePath,
69 node: Node,
71 },
72 Set {
74 path: NodePath,
76 props: serde_json::Map<String, Json>,
78 },
79 Splice {
81 path: NodePath,
83 field: PatchField,
85 index: u32,
87 remove: u32,
89 insert: Vec<PatchItem>,
91 },
92 Append {
95 path: NodePath,
97 field: PatchField,
99 items: Vec<PatchItem>,
101 },
102 Item {
104 path: NodePath,
106 field: PatchField,
108 key: String,
110 value: PatchItem,
112 },
113}
114
115impl NodeOp {
116 #[must_use]
118 pub fn replace(
119 path: impl IntoIterator<Item = impl Into<String>>,
120 node: impl Into<Node>,
121 ) -> Self {
122 Self::Replace {
123 path: path.into_iter().map(Into::into).collect(),
124 node: node.into(),
125 }
126 }
127
128 #[must_use]
130 pub fn set(
131 path: impl IntoIterator<Item = impl Into<String>>,
132 props: serde_json::Map<String, Json>,
133 ) -> Self {
134 Self::Set {
135 path: path.into_iter().map(Into::into).collect(),
136 props,
137 }
138 }
139
140 #[must_use]
142 pub fn splice(
143 path: impl IntoIterator<Item = impl Into<String>>,
144 field: PatchField,
145 index: u32,
146 remove: u32,
147 insert: impl IntoIterator<Item = impl Into<PatchItem>>,
148 ) -> Self {
149 Self::Splice {
150 path: path.into_iter().map(Into::into).collect(),
151 field,
152 index,
153 remove,
154 insert: insert.into_iter().map(Into::into).collect(),
155 }
156 }
157
158 #[must_use]
160 pub fn append(
161 path: impl IntoIterator<Item = impl Into<String>>,
162 field: PatchField,
163 items: impl IntoIterator<Item = impl Into<PatchItem>>,
164 ) -> Self {
165 Self::Append {
166 path: path.into_iter().map(Into::into).collect(),
167 field,
168 items: items.into_iter().map(Into::into).collect(),
169 }
170 }
171
172 #[must_use]
174 pub fn item(
175 path: impl IntoIterator<Item = impl Into<String>>,
176 field: PatchField,
177 key: impl Into<String>,
178 value: impl Into<PatchItem>,
179 ) -> Self {
180 Self::Item {
181 path: path.into_iter().map(Into::into).collect(),
182 field,
183 key: key.into(),
184 value: value.into(),
185 }
186 }
187}
188
189impl From<Json> for PatchItem {
190 fn from(json: Json) -> Self {
191 Self::Item(json)
192 }
193}
194
195impl From<String> for PatchItem {
196 fn from(text: String) -> Self {
197 Self::Item(Json::String(text))
198 }
199}
200
201impl From<&str> for PatchItem {
202 fn from(text: &str) -> Self {
203 Self::Item(Json::String(text.to_owned()))
204 }
205}
206
207impl From<Node> for PatchItem {
208 fn from(node: Node) -> Self {
209 Self::Node(node)
210 }
211}
212
213#[derive(Clone, Debug, PartialEq, Eq)]
217pub enum PatchError {
218 MissingPath {
220 path: NodePath,
222 },
223 FieldMismatch {
225 path: NodePath,
227 field: PatchField,
229 kind: String,
231 },
232 MissingKey {
234 path: NodePath,
236 field: PatchField,
238 key: String,
240 },
241 OutOfBounds {
243 path: NodePath,
245 field: PatchField,
247 index: u32,
249 len: usize,
251 },
252 BadItem {
255 path: NodePath,
257 field: PatchField,
259 detail: String,
261 },
262 BadPatch {
264 path: NodePath,
266 detail: String,
268 },
269 MissingItemKey {
271 path: NodePath,
273 },
274}
275
276impl std::fmt::Display for PatchError {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 Self::MissingPath { path } => write!(f, "no node at path {path:?}"),
280 Self::FieldMismatch { path, field, kind } => {
281 write!(
282 f,
283 "node at {path:?} is {kind}, which has no {field:?} collection"
284 )
285 }
286 Self::MissingKey { path, field, key } => {
287 write!(f, "no {field:?} member keyed {key:?} at {path:?}")
288 }
289 Self::OutOfBounds {
290 path,
291 field,
292 index,
293 len,
294 } => {
295 write!(
296 f,
297 "splice at {index} past end ({len}) of {field:?} at {path:?}"
298 )
299 }
300 Self::BadItem {
301 path,
302 field,
303 detail,
304 } => {
305 write!(f, "bad {field:?} item at {path:?}: {detail}")
306 }
307 Self::BadPatch { path, detail } => {
308 write!(f, "set at {path:?} broke the node: {detail}")
309 }
310 Self::MissingItemKey { path } => {
311 write!(f, "panes insert at {path:?} without a key")
312 }
313 }
314 }
315}
316
317impl std::error::Error for PatchError {}
318
319#[must_use]
323pub fn diff(old: &Node, new: &Node) -> Vec<NodeOp> {
324 let mut ops = Vec::new();
325 diff_node(old, new, &NodePath::new(), &mut ops);
326 let new_bytes = serde_json::to_vec(new).map_or(usize::MAX, |v| v.len());
328 let ops_bytes = serde_json::to_vec(&ops).map_or(usize::MAX, |v| v.len());
329 if ops_bytes
330 .checked_mul(2)
331 .is_none_or(|twice| twice > new_bytes)
332 {
333 return vec![NodeOp::Replace {
334 path: NodePath::new(),
335 node: new.clone(),
336 }];
337 }
338 ops
339}
340
341pub fn apply(old: &Node, ops: &[NodeOp]) -> Result<Node, PatchError> {
348 let mut root = old.clone();
349 for op in ops {
350 apply_op(&mut root, op)?;
351 }
352 Ok(root)
353}
354
355#[allow(
358 clippy::large_enum_variant,
359 reason = "patch ops are built once per update; boxing every node adds noise"
360)]
361#[derive(Clone, Debug, PartialEq)]
362enum Member {
363 Node {
365 key: String,
367 node: Node,
369 },
370 Item {
372 key: String,
374 json: Json,
376 },
377}
378
379impl Member {
380 fn key(&self) -> &str {
383 match self {
384 Self::Node { key, .. } | Self::Item { key, .. } => key,
385 }
386 }
387
388 fn to_patch_item(&self) -> PatchItem {
390 match self {
391 Self::Node { node, .. } => PatchItem::Node(node.clone()),
392 Self::Item { json, .. } => PatchItem::Item(json.clone()),
393 }
394 }
395}
396
397fn is_positional(key: &str) -> bool {
400 key.starts_with('#')
401}
402
403fn derived_key(explicit: Option<&str>, index: usize) -> String {
405 match explicit {
406 Some(key) => key.to_owned(),
407 None => format!("#{index}"),
408 }
409}
410
411fn child_path(path: &NodePath, key: &str) -> NodePath {
413 let mut path = path.clone();
414 path.push(key.to_owned());
415 path
416}
417
418fn collection_fields(kind: &str) -> &'static [&'static str] {
421 match kind {
422 "Layout" => &["children"],
423 "Tabs" => &["titles", "panes"],
424 "List" => &["items"],
425 "Table" => &["rows"],
426 "Tree" => &["nodes"],
427 "Log" => &["lines"],
428 "Chart" => &["datasets"],
429 "Sparkline" | "BarChart" => &["data"],
430 _ => &[],
431 }
432}
433
434fn node_fields(node: &Node) -> &'static [PatchField] {
436 match node {
437 Node::Layout(_) => &[PatchField::Children],
438 Node::Tabs(_) => &[PatchField::Titles, PatchField::Panes],
439 Node::List(_) => &[PatchField::Items],
440 Node::Table(_) => &[PatchField::Rows],
441 Node::Tree(_) => &[PatchField::Nodes],
442 Node::Log(_) => &[PatchField::Lines],
443 Node::Chart(_) => &[PatchField::Datasets],
444 Node::Sparkline(_) | Node::BarChart(_) => &[PatchField::Data],
445 _ => &[],
446 }
447}
448
449fn seq_members<T: Serialize, F: Fn(&T) -> Option<String>>(
451 items: &[T],
452 explicit_key: F,
453) -> Vec<Member> {
454 items
455 .iter()
456 .enumerate()
457 .map(|(index, value)| Member::Item {
458 key: explicit_key(value).unwrap_or_else(|| format!("#{index}")),
459 json: serde_json::to_value(value).unwrap_or(Json::Null),
460 })
461 .collect()
462}
463
464fn members(node: &Node, field: PatchField) -> Vec<Member> {
467 match (node, field) {
468 (Node::Layout(l), PatchField::Children) => l
469 .children
470 .iter()
471 .enumerate()
472 .map(|(index, child)| Member::Node {
473 key: derived_key(child.common().key, index),
474 node: child.clone(),
475 })
476 .collect(),
477 (Node::Tabs(t), PatchField::Panes) => t
478 .panes
479 .iter()
480 .flatten()
481 .map(|(key, pane)| Member::Node {
482 key: key.clone(),
483 node: pane.clone(),
484 })
485 .collect(),
486 (Node::Tabs(t), PatchField::Titles) => {
487 seq_members(&t.titles, |title| Some(title.key.clone()))
488 }
489 (Node::List(l), PatchField::Items) => seq_members(&l.items, |item| match item {
490 crate::widgets::list::ListItem::Full { key: Some(key), .. } => Some(key.clone()),
491 _ => None,
492 }),
493 (Node::Table(t), PatchField::Rows) => seq_members(&t.rows, |row| row.key.clone()),
494 (Node::Tree(t), PatchField::Nodes) => seq_members(&t.nodes, |n| Some(n.key.clone())),
495 (Node::Log(l), PatchField::Lines) => seq_members(&l.lines, |_| None),
496 (Node::Chart(c), PatchField::Datasets) => seq_members(&c.datasets, |_| None),
497 (Node::Sparkline(s), PatchField::Data) => seq_members(&s.data, |_| None),
498 (Node::BarChart(b), PatchField::Data) => seq_members(&b.data, |_| None),
499 _ => Vec::new(),
500 }
501}
502
503fn diff_node(old: &Node, new: &Node, path: &NodePath, ops: &mut Vec<NodeOp>) {
505 if old.kind() != new.kind() || old.common().key != new.common().key {
507 ops.push(NodeOp::Replace {
508 path: path.clone(),
509 node: new.clone(),
510 });
511 return;
512 }
513 diff_props(old, new, path, ops);
515 for field in node_fields(new) {
517 diff_collection(old, new, *field, path, ops);
518 }
519}
520
521fn diff_props(old: &Node, new: &Node, path: &NodePath, ops: &mut Vec<NodeOp>) {
523 let old_json = serde_json::to_value(old).unwrap_or(Json::Null);
524 let new_json = serde_json::to_value(new).unwrap_or(Json::Null);
525 let (Some(old_obj), Some(new_obj)) = (old_json.as_object(), new_json.as_object()) else {
526 return;
527 };
528 let skip = collection_fields(new.kind());
529 let is_skipped = |key: &&str| *key == "t" || skip.contains(key);
530 let mut props = serde_json::Map::new();
531 for (key, value) in new_obj {
532 if is_skipped(&key.as_str()) {
533 continue;
534 }
535 if old_obj.get(key) != Some(value) {
536 props.insert(key.clone(), value.clone());
537 }
538 }
539 for key in old_obj
540 .keys()
541 .filter(|key| !new_obj.contains_key(key.as_str()))
542 {
543 if is_skipped(&key.as_str()) {
544 continue;
545 }
546 props.insert(key.clone(), Json::Null);
547 }
548 if !props.is_empty() {
549 ops.push(NodeOp::Set {
550 path: path.clone(),
551 props,
552 });
553 }
554}
555
556fn diff_collection(
558 old: &Node,
559 new: &Node,
560 field: PatchField,
561 path: &NodePath,
562 ops: &mut Vec<NodeOp>,
563) {
564 let old_m = members(old, field);
565 let new_m = members(new, field);
566 if old_m.len() == new_m.len() && old_m.iter().zip(&new_m).all(|(o, n)| o.key() == n.key()) {
568 diff_stable(&old_m, &new_m, field, path, ops);
569 return;
570 }
571 if matches!(
573 field,
574 PatchField::Items | PatchField::Rows | PatchField::Lines
575 ) && new_m.len() > old_m.len()
576 && old_m.iter().zip(&new_m).all(|(o, n)| o == n)
577 {
578 ops.push(NodeOp::Append {
579 path: path.clone(),
580 field,
581 items: new_m[old_m.len()..]
582 .iter()
583 .map(Member::to_patch_item)
584 .collect(),
585 });
586 return;
587 }
588 if field == PatchField::Panes
591 && !new_m.iter().all(|member| match member {
592 Member::Node { key, node } => node.common().key == Some(key.as_str()),
593 Member::Item { .. } => false,
594 })
595 {
596 ops.push(NodeOp::Replace {
597 path: path.clone(),
598 node: new.clone(),
599 });
600 return;
601 }
602 let kept = splice_diff(&old_m, &new_m, field, path, ops);
605 let stable = kept
606 .into_iter()
607 .map(|(new_index, old_index)| (&old_m[old_index], &new_m[new_index]));
608 for (o, n) in stable {
609 if o == n {
610 continue;
611 }
612 emit_stable_change(o, n, field, path, ops);
613 }
614}
615
616fn emit_stable_change(
620 o: &Member,
621 n: &Member,
622 field: PatchField,
623 path: &NodePath,
624 ops: &mut Vec<NodeOp>,
625) {
626 match (o, n) {
627 (Member::Node { node: old_node, .. }, Member::Node { key, node }) => {
628 diff_node(old_node, node, &child_path(path, key), ops);
629 }
630 (_, Member::Item { key, json }) => ops.push(NodeOp::Item {
631 path: path.clone(),
632 field,
633 key: key.clone(),
634 value: PatchItem::Item(json.clone()),
635 }),
636 (Member::Item { .. }, Member::Node { .. }) => {}
637 }
638}
639
640fn diff_stable(
642 old_m: &[Member],
643 new_m: &[Member],
644 field: PatchField,
645 path: &NodePath,
646 ops: &mut Vec<NodeOp>,
647) {
648 for (o, n) in old_m.iter().zip(new_m) {
649 if o == n {
650 continue;
651 }
652 emit_stable_change(o, n, field, path, ops);
653 }
654}
655
656fn splice_diff(
659 old_m: &[Member],
660 new_m: &[Member],
661 field: PatchField,
662 path: &NodePath,
663 ops: &mut Vec<NodeOp>,
664) -> Vec<(usize, usize)> {
665 let old_by_key: std::collections::HashMap<&str, usize> = old_m
667 .iter()
668 .enumerate()
669 .filter(|(_, m)| !is_positional(m.key()))
670 .map(|(index, m)| (m.key(), index))
671 .collect();
672 let pairs: Vec<(usize, usize)> = new_m
673 .iter()
674 .enumerate()
675 .filter(|(_, m)| !is_positional(m.key()))
676 .filter_map(|(new_index, m)| {
677 old_by_key
678 .get(m.key())
679 .map(|&old_index| (new_index, old_index))
680 })
681 .collect();
682 let kept = longest_increasing_chain(&pairs); let mut gaps: Vec<(usize, usize, Vec<&Member>)> = Vec::new(); let (mut n_cursor, mut o_cursor) = (0usize, 0usize);
689 for (new_index, old_index) in &kept {
690 if *new_index > n_cursor || *old_index > o_cursor {
691 gaps.push((
692 o_cursor,
693 old_index - o_cursor,
694 new_m[n_cursor..*new_index].iter().collect(),
695 ));
696 }
697 n_cursor = new_index + 1;
698 o_cursor = old_index + 1;
699 }
700 if new_m.len() > n_cursor || old_m.len() > o_cursor {
701 gaps.push((
702 o_cursor,
703 old_m.len() - o_cursor,
704 new_m[n_cursor..].iter().collect(),
705 ));
706 }
707 let mut delta: isize = 0; for (start, removed, inserts) in &gaps {
709 if *removed == 0 && inserts.is_empty() {
710 continue;
711 }
712 let index = isize::try_from(*start).unwrap_or(0) + delta;
713 ops.push(NodeOp::Splice {
714 path: path.clone(),
715 field,
716 index: u32::try_from(index.max(0)).unwrap_or(u32::MAX),
717 remove: u32::try_from(*removed).unwrap_or(u32::MAX),
718 insert: inserts.iter().map(|m| m.to_patch_item()).collect(),
719 });
720 delta +=
721 isize::try_from(inserts.len()).unwrap_or(0) - isize::try_from(*removed).unwrap_or(0);
722 }
723 kept
724}
725
726fn longest_increasing_chain(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
729 let mut tails: Vec<usize> = Vec::new(); let mut tail_pair: Vec<usize> = Vec::new(); let mut prev = vec![usize::MAX; pairs.len()];
732 for (i, &(_, old_index)) in pairs.iter().enumerate() {
733 match tails.binary_search(&old_index) {
734 Ok(pos) => {
735 tails[pos] = old_index;
736 tail_pair[pos] = i;
737 }
738 Err(pos) => {
739 prev[i] = pos.checked_sub(1).map_or(usize::MAX, |q| tail_pair[q]);
740 if pos == tails.len() {
741 tails.push(old_index);
742 tail_pair.push(i);
743 } else {
744 tails[pos] = old_index;
745 tail_pair[pos] = i;
746 }
747 }
748 }
749 }
750 let mut chain = Vec::new();
751 let mut cursor = tail_pair.last().copied();
752 while let Some(i) = cursor {
753 chain.push(pairs[i]);
754 cursor = (prev[i] != usize::MAX).then_some(prev[i]);
755 }
756 chain.reverse();
757 chain
758}
759
760fn node_at<'a>(root: &'a mut Node, path: &NodePath) -> Result<&'a mut Node, PatchError> {
763 let mut node = root;
764 for (depth, segment) in path.iter().enumerate() {
765 let missing = || PatchError::MissingPath {
766 path: path[..=depth].to_vec(),
767 };
768 node = match segment.as_str() {
769 "child" => match node {
770 Node::Block(block) => block.child.as_deref_mut(),
771 _ => None,
772 }
773 .ok_or_else(missing)?,
774 "fallback" => fallback_slot(node).ok_or_else(missing)?,
775 "action" => match node {
776 Node::Empty(empty) => empty.action.as_deref_mut(),
777 _ => None,
778 }
779 .ok_or_else(missing)?,
780 key => descend_member(node, key).ok_or_else(missing)?,
781 };
782 }
783 Ok(node)
784}
785
786fn fallback_slot(node: &mut Node) -> Option<&mut Node> {
788 macro_rules! common_fallback {
789 ($($variant:ident),* $(,)?) => {
790 match node {
791 $(Node::$variant(inner) => inner.common.fallback.as_deref_mut(),)*
792 Node::Unknown(unknown) => unknown.fallback.as_deref_mut(),
793 }
794 };
795 }
796 common_fallback!(
797 Layout, Block, Paragraph, List, Table, Tabs, Gauge, LineGauge, Sparkline, BarChart, Chart,
798 Scrollbar, Markdown, Code, Diff, Image, Log, Tree, KeyHints, Input, Toggle, Select, Button,
799 Spinner, Empty
800 )
801}
802
803fn descend_member<'a>(node: &'a mut Node, key: &str) -> Option<&'a mut Node> {
806 match node {
807 Node::Layout(layout) => {
808 let position = layout
809 .children
810 .iter()
811 .enumerate()
812 .position(|(index, child)| derived_key(child.common().key, index) == key)?;
813 layout.children.get_mut(position)
814 }
815 Node::Tabs(tabs) => tabs.panes.as_mut()?.get_mut(key),
816 _ => None,
817 }
818}
819
820fn apply_op(root: &mut Node, op: &NodeOp) -> Result<(), PatchError> {
821 match op {
822 NodeOp::Replace { path, node } => {
823 *node_at(root, path)? = node.clone();
824 }
825 NodeOp::Set { path, props } => set_props(node_at(root, path)?, path, props)?,
826 NodeOp::Splice {
827 path,
828 field,
829 index,
830 remove,
831 insert,
832 } => splice_at(node_at(root, path)?, path, *field, *index, *remove, insert)?,
833 NodeOp::Append { path, field, items } => {
834 append_at(node_at(root, path)?, path, *field, items)?;
835 }
836 NodeOp::Item {
837 path,
838 field,
839 key,
840 value,
841 } => replace_item(node_at(root, path)?, path, *field, key, value)?,
842 }
843 Ok(())
844}
845
846fn set_props(
848 slot: &mut Node,
849 path: &NodePath,
850 props: &serde_json::Map<String, Json>,
851) -> Result<(), PatchError> {
852 let bad = |detail: String| PatchError::BadPatch {
853 path: path.clone(),
854 detail,
855 };
856 let mut json = serde_json::to_value(&*slot).map_err(|e| bad(e.to_string()))?;
857 let Some(obj) = json.as_object_mut() else {
858 return Err(bad("node is not a JSON object".into()));
859 };
860 for (key, value) in props {
861 if value.is_null() {
862 obj.remove(key);
863 } else {
864 obj.insert(key.clone(), value.clone());
865 }
866 }
867 *slot = serde_json::from_value(json).map_err(|e| bad(e.to_string()))?;
868 Ok(())
869}
870
871fn plain_item(value: &PatchItem, field: PatchField, path: &NodePath) -> Result<Json, PatchError> {
874 match value {
875 PatchItem::Item(json) => Ok(json.clone()),
876 PatchItem::Node(_) => Err(PatchError::BadItem {
877 path: path.clone(),
878 field,
879 detail: "a node cannot be a plain item".into(),
880 }),
881 }
882}
883
884fn member_node(value: &PatchItem, field: PatchField, path: &NodePath) -> Result<Node, PatchError> {
886 match value {
887 PatchItem::Node(node) => Ok(node.clone()),
888 PatchItem::Item(json) => {
889 serde_json::from_value(json.clone()).map_err(|e| PatchError::BadItem {
890 path: path.clone(),
891 field,
892 detail: e.to_string(),
893 })
894 }
895 }
896}
897
898fn pane_insert(value: &PatchItem, path: &NodePath) -> Result<(Key, Node), PatchError> {
900 let node = member_node(value, PatchField::Panes, path)?;
901 let Some(key) = node.common().key.map(str::to_owned) else {
902 return Err(PatchError::MissingItemKey { path: path.clone() });
903 };
904 Ok((key, node))
905}
906
907fn splice_seq<T: serde::de::DeserializeOwned>(
909 items: &mut Vec<T>,
910 field: PatchField,
911 path: &NodePath,
912 start: usize,
913 end: usize,
914 insert: &[PatchItem],
915) -> Result<(), PatchError> {
916 if end > items.len() {
917 return Err(PatchError::OutOfBounds {
918 path: path.clone(),
919 field,
920 index: u32::try_from(start).unwrap_or(u32::MAX),
921 len: items.len(),
922 });
923 }
924 let inserted = insert
925 .iter()
926 .map(|value| plain_item(value, field, path))
927 .collect::<Result<Vec<_>, _>>()?
928 .into_iter()
929 .map(|json| serde_json::from_value::<T>(json))
930 .collect::<Result<Vec<_>, _>>()
931 .map_err(|e| PatchError::BadItem {
932 path: path.clone(),
933 field,
934 detail: e.to_string(),
935 })?;
936 items.splice(start..end, inserted);
937 Ok(())
938}
939
940fn splice_at(
941 slot: &mut Node,
942 path: &NodePath,
943 field: PatchField,
944 index: u32,
945 remove: u32,
946 insert: &[PatchItem],
947) -> Result<(), PatchError> {
948 let Some(end) = index.checked_add(remove) else {
949 return Err(PatchError::OutOfBounds {
950 path: path.clone(),
951 field,
952 index,
953 len: usize::MAX,
954 });
955 };
956 let (start, end) = (usize::try_from(index).unwrap_or(usize::MAX), end as usize);
957 match (slot, field) {
958 (Node::Layout(layout), PatchField::Children) => {
959 if end > layout.children.len() {
960 return Err(PatchError::OutOfBounds {
961 path: path.clone(),
962 field,
963 index,
964 len: layout.children.len(),
965 });
966 }
967 let inserted = insert
968 .iter()
969 .map(|value| member_node(value, field, path))
970 .collect::<Result<Vec<_>, _>>()?;
971 layout.children.splice(start..end, inserted);
972 }
973 (Node::Tabs(tabs), PatchField::Panes) => {
974 let map = tabs
975 .panes
976 .get_or_insert_with(std::collections::BTreeMap::new);
977 let mut entries: Vec<(Key, Node)> = map
978 .iter()
979 .map(|(key, pane)| (key.clone(), pane.clone()))
980 .collect();
981 if end > entries.len() {
982 return Err(PatchError::OutOfBounds {
983 path: path.clone(),
984 field,
985 index,
986 len: entries.len(),
987 });
988 }
989 let inserted = insert
990 .iter()
991 .map(|value| pane_insert(value, path))
992 .collect::<Result<Vec<_>, _>>()?;
993 entries.splice(start..end, inserted);
994 tabs.panes = if entries.is_empty() {
997 None
998 } else {
999 Some(entries.into_iter().collect())
1000 };
1001 }
1002 (Node::List(list), PatchField::Items) => {
1003 splice_seq(&mut list.items, field, path, start, end, insert)?;
1004 }
1005 (Node::Table(table), PatchField::Rows) => {
1006 splice_seq(&mut table.rows, field, path, start, end, insert)?;
1007 }
1008 (Node::Tree(tree), PatchField::Nodes) => {
1009 splice_seq(&mut tree.nodes, field, path, start, end, insert)?;
1010 }
1011 (Node::Log(log), PatchField::Lines) => {
1012 splice_seq(&mut log.lines, field, path, start, end, insert)?;
1013 }
1014 (Node::Tabs(tabs), PatchField::Titles) => {
1015 splice_seq(&mut tabs.titles, field, path, start, end, insert)?;
1016 }
1017 (Node::Chart(chart), PatchField::Datasets) => {
1018 splice_seq(&mut chart.datasets, field, path, start, end, insert)?;
1019 }
1020 (Node::Sparkline(sparkline), PatchField::Data) => {
1021 splice_seq(&mut sparkline.data, field, path, start, end, insert)?;
1022 }
1023 (Node::BarChart(bars), PatchField::Data) => {
1024 splice_seq(&mut bars.data, field, path, start, end, insert)?;
1025 }
1026 (node, _) => {
1027 return Err(PatchError::FieldMismatch {
1028 path: path.clone(),
1029 field,
1030 kind: node.kind().to_owned(),
1031 });
1032 }
1033 }
1034 Ok(())
1035}
1036
1037fn append_at(
1041 slot: &mut Node,
1042 path: &NodePath,
1043 field: PatchField,
1044 items: &[PatchItem],
1045) -> Result<(), PatchError> {
1046 let convert = |value: &PatchItem| plain_item(value, field, path);
1047 let jsons = items.iter().map(convert).collect::<Result<Vec<_>, _>>()?;
1048 match (slot, field) {
1049 (Node::List(list), PatchField::Items) => push_items(&mut list.items, jsons, path, field)?,
1050 (Node::Table(table), PatchField::Rows) => push_items(&mut table.rows, jsons, path, field)?,
1051 (Node::Log(log), PatchField::Lines) => push_items(&mut log.lines, jsons, path, field)?,
1052 (node, _) => {
1053 return Err(PatchError::FieldMismatch {
1054 path: path.clone(),
1055 field,
1056 kind: node.kind().to_owned(),
1057 });
1058 }
1059 }
1060 Ok(())
1061}
1062
1063fn push_items<T: serde::de::DeserializeOwned>(
1064 items: &mut Vec<T>,
1065 jsons: Vec<Json>,
1066 path: &NodePath,
1067 field: PatchField,
1068) -> Result<(), PatchError> {
1069 for json in jsons {
1070 let item = serde_json::from_value::<T>(json).map_err(|e| PatchError::BadItem {
1071 path: path.clone(),
1072 field,
1073 detail: e.to_string(),
1074 })?;
1075 items.push(item);
1076 }
1077 Ok(())
1078}
1079
1080fn replace_item(
1083 slot: &mut Node,
1084 path: &NodePath,
1085 field: PatchField,
1086 key: &str,
1087 value: &PatchItem,
1088) -> Result<(), PatchError> {
1089 let missing = || PatchError::MissingKey {
1090 path: path.clone(),
1091 field,
1092 key: key.to_owned(),
1093 };
1094 match (slot, field) {
1095 (Node::Layout(layout), PatchField::Children) => {
1096 let position = layout
1097 .children
1098 .iter()
1099 .enumerate()
1100 .find_map(|(index, child)| {
1101 (derived_key(child.common().key, index) == key).then_some(index)
1102 })
1103 .ok_or_else(missing)?;
1104 layout.children[position] = member_node(value, field, path)?;
1105 }
1106 (Node::Tabs(tabs), PatchField::Panes) => {
1107 let pane = tabs
1108 .panes
1109 .as_mut()
1110 .and_then(|m| m.get_mut(key))
1111 .ok_or_else(missing)?;
1112 *pane = member_node(value, field, path)?;
1113 }
1114 (Node::List(list), PatchField::Items) => replace_seq(
1115 &mut list.items,
1116 field,
1117 path,
1118 key,
1119 value,
1120 |item| match item {
1121 crate::widgets::list::ListItem::Full {
1122 key: Some(item_key),
1123 ..
1124 } => Some(item_key.as_str()),
1125 _ => None,
1126 },
1127 )?,
1128 (Node::Table(table), PatchField::Rows) => {
1129 replace_seq(&mut table.rows, field, path, key, value, |row| {
1130 row.key.as_deref()
1131 })?;
1132 }
1133 (Node::Tree(tree), PatchField::Nodes) => {
1134 replace_seq(&mut tree.nodes, field, path, key, value, |n| {
1135 Some(n.key.as_str())
1136 })?;
1137 }
1138 (Node::Tabs(tabs), PatchField::Titles) => {
1139 replace_seq(&mut tabs.titles, field, path, key, value, |title| {
1140 Some(title.key.as_str())
1141 })?;
1142 }
1143 (Node::Log(log), PatchField::Lines) => {
1144 replace_seq(&mut log.lines, field, path, key, value, |_| None)?;
1145 }
1146 (Node::Chart(chart), PatchField::Datasets) => {
1147 replace_seq(&mut chart.datasets, field, path, key, value, |_| None)?;
1148 }
1149 (Node::Sparkline(sparkline), PatchField::Data) => {
1150 replace_seq(&mut sparkline.data, field, path, key, value, |_| None)?;
1151 }
1152 (Node::BarChart(bars), PatchField::Data) => {
1153 replace_seq(&mut bars.data, field, path, key, value, |_| None)?;
1154 }
1155 (node, _) => {
1156 return Err(PatchError::FieldMismatch {
1157 path: path.clone(),
1158 field,
1159 kind: node.kind().to_owned(),
1160 });
1161 }
1162 }
1163 Ok(())
1164}
1165
1166fn replace_seq<T: serde::de::DeserializeOwned, F: Fn(&T) -> Option<&str>>(
1169 items: &mut [T],
1170 field: PatchField,
1171 path: &NodePath,
1172 key: &str,
1173 value: &PatchItem,
1174 explicit_key: F,
1175) -> Result<(), PatchError> {
1176 let missing = || PatchError::MissingKey {
1177 path: path.clone(),
1178 field,
1179 key: key.to_owned(),
1180 };
1181 let position = items
1182 .iter()
1183 .enumerate()
1184 .find_map(|(index, item)| (explicit_key(item) == Some(key)).then_some(index))
1185 .or_else(|| {
1186 key.strip_prefix('#')
1187 .and_then(|index| index.parse::<usize>().ok())
1188 .filter(|&index| index < items.len())
1189 })
1190 .ok_or_else(missing)?;
1191 let json = plain_item(value, field, path)?;
1192 items[position] = serde_json::from_value(json).map_err(|e| PatchError::BadItem {
1193 path: path.clone(),
1194 field,
1195 detail: e.to_string(),
1196 })?;
1197 Ok(())
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 #![allow(
1203 clippy::unwrap_used,
1204 clippy::expect_used,
1205 clippy::panic,
1206 reason = "tests assert by unwrapping"
1207 )]
1208 use super::*;
1209 use crate::extras::log::Log;
1210
1211 #[test]
1212 fn ops_round_trip() {
1213 let ops = vec![
1214 NodeOp::Replace {
1215 path: NodePath::new(),
1216 node: Log::new(["a"]).into(),
1217 },
1218 NodeOp::Set {
1219 path: NodePath::new(),
1220 props: serde_json::Map::from_iter([("follow".to_owned(), Json::Bool(false))]),
1221 },
1222 NodeOp::Splice {
1223 path: vec!["main".to_owned()],
1224 field: PatchField::Items,
1225 index: 2,
1226 remove: 1,
1227 insert: vec![PatchItem::Item(Json::Null)],
1228 },
1229 NodeOp::Append {
1230 path: NodePath::new(),
1231 field: PatchField::Lines,
1232 items: vec![PatchItem::Item(Json::String("new".into()))],
1233 },
1234 NodeOp::Item {
1235 path: NodePath::new(),
1236 field: PatchField::Rows,
1237 key: "r1".into(),
1238 value: PatchItem::Item(Json::Null),
1239 },
1240 ];
1241 let json = serde_json::to_string(&ops).unwrap_or_default();
1242 let back: Vec<NodeOp> = serde_json::from_str(&json).unwrap_or_default();
1243 assert_eq!(ops, back);
1244 }
1245}