1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3
4#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
17pub enum MapPatch<K, V> {
18 Insert { key: K, value: V },
19 Remove { key: K },
20 Replace { key: K, value: V },
21 Clear,
22}
23
24impl<K, V> From<(K, Option<V>)> for MapPatch<K, V> {
25 fn from((key, value): (K, Option<V>)) -> Self {
26 match value {
27 Some(value) => Self::Insert { key, value },
28 None => Self::Remove { key },
29 }
30 }
31}
32
33#[cfg(test)]
38mod tests {
39 use super::*;
40 use crate::patch::merge::{MergePatch, MergePatchError};
41 use std::collections::{BTreeMap, HashMap};
42
43 #[test]
44 fn map_replace_updates_existing_entry() {
45 let mut map: HashMap<String, u8> = [("keep".into(), 1u8), ("replace".into(), 2u8)]
46 .into_iter()
47 .collect();
48
49 let patches = vec![MapPatch::Replace {
50 key: "replace".to_string(),
51 value: 8u8,
52 }];
53
54 map.merge(patches).expect("map patch merge should succeed");
55
56 assert_eq!(map.get("keep"), Some(&1));
57 assert_eq!(map.get("replace"), Some(&8));
58 }
59
60 #[test]
61 fn btree_map_clear_replaces_with_empty_map() {
62 let mut map: BTreeMap<String, u8> =
63 [("a".into(), 1u8), ("b".into(), 2u8)].into_iter().collect();
64
65 map.merge(vec![MapPatch::Clear])
66 .expect("map clear patch should succeed");
67
68 assert!(map.is_empty());
69 }
70
71 #[test]
72 fn map_remove_missing_key_returns_error() {
73 let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
74 let err = map
75 .merge(vec![MapPatch::Remove {
76 key: "missing".to_string(),
77 }])
78 .expect_err("missing remove key should fail");
79 assert!(matches!(
80 err.leaf(),
81 MergePatchError::MissingKey {
82 operation: "remove"
83 }
84 ));
85 }
86
87 #[test]
88 fn map_replace_missing_key_returns_error() {
89 let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
90 let err = map
91 .merge(vec![MapPatch::Replace {
92 key: "missing".to_string(),
93 value: 3u8,
94 }])
95 .expect_err("missing replace key should fail");
96 assert!(matches!(
97 err.leaf(),
98 MergePatchError::MissingKey {
99 operation: "replace"
100 }
101 ));
102 }
103
104 #[test]
105 fn map_clear_with_other_operations_returns_error() {
106 let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
107 let err = map
108 .merge(vec![
109 MapPatch::Clear,
110 MapPatch::Insert {
111 key: "b".to_string(),
112 value: 2u8,
113 },
114 ])
115 .expect_err("clear combined with key ops should fail");
116 assert!(matches!(
117 err.leaf(),
118 MergePatchError::CardinalityViolation {
119 expected: 1,
120 actual: 2,
121 }
122 ));
123 }
124
125 #[test]
126 fn map_duplicate_key_operations_returns_error() {
127 let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
128 let err = map
129 .merge(vec![
130 MapPatch::Insert {
131 key: "a".to_string(),
132 value: 3u8,
133 },
134 MapPatch::Replace {
135 key: "a".to_string(),
136 value: 4u8,
137 },
138 ])
139 .expect_err("duplicate key operations should fail");
140 assert!(matches!(
141 err.leaf(),
142 MergePatchError::InvalidShape {
143 expected: "unique key operations per map patch batch",
144 actual: "duplicate key operation",
145 }
146 ));
147 }
148}