Skip to main content

icydb_core/patch/
map.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3
4///
5/// MapPatch
6///
7/// Deterministic map mutations.
8///
9/// - Maps are unordered values; insertion order is discarded.
10/// - `Insert` is an upsert.
11/// - `Replace` requires an existing key.
12/// - `Remove` requires an existing key.
13/// - `Clear` must be the only patch in the batch.
14///
15
16#[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///
34/// TESTS
35///
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::{patch::MergePatchError, traits::UpdateView};
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}