Skip to main content

icydb_core/patch/
map.rs

1//! Module: patch::map
2//! Responsibility: module-local ownership and contracts for patch::map.
3//! Does not own: cross-module orchestration outside this module.
4//! Boundary: exposes this module API while keeping implementation details internal.
5
6use candid::CandidType;
7use serde::{Deserialize, Serialize};
8
9///
10/// MapPatch
11///
12/// Deterministic map mutations.
13///
14/// - Maps are unordered values; insertion order is discarded.
15/// - `Insert` is an upsert.
16/// - `Replace` is a no-op when the key is missing.
17/// - `Remove` is a no-op when the key is missing.
18/// - `Clear` must be the only patch in the batch.
19///
20
21#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
22pub enum MapPatch<K, V> {
23    Insert { key: K, value: V },
24    Remove { key: K },
25    Replace { key: K, value: V },
26    Clear,
27}
28
29impl<K, V> From<(K, Option<V>)> for MapPatch<K, V> {
30    fn from((key, value): (K, Option<V>)) -> Self {
31        match value {
32            Some(value) => Self::Insert { key, value },
33            None => Self::Remove { key },
34        }
35    }
36}
37
38///
39/// TESTS
40///
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::{patch::MergePatchError, traits::UpdateView};
46    use std::collections::{BTreeMap, HashMap};
47
48    #[test]
49    fn map_replace_updates_existing_entry() {
50        let mut map: HashMap<String, u8> = [("keep".into(), 1u8), ("replace".into(), 2u8)]
51            .into_iter()
52            .collect();
53
54        let patches = vec![MapPatch::Replace {
55            key: "replace".to_string(),
56            value: 8u8,
57        }];
58
59        map.merge(patches).expect("map patch merge should succeed");
60
61        assert_eq!(map.get("keep"), Some(&1));
62        assert_eq!(map.get("replace"), Some(&8));
63    }
64
65    #[test]
66    fn btree_map_clear_replaces_with_empty_map() {
67        let mut map: BTreeMap<String, u8> =
68            [("a".into(), 1u8), ("b".into(), 2u8)].into_iter().collect();
69
70        map.merge(vec![MapPatch::Clear])
71            .expect("map clear patch should succeed");
72
73        assert!(map.is_empty());
74    }
75
76    #[test]
77    fn map_remove_missing_key_is_noop() {
78        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
79        map.merge(vec![MapPatch::Remove {
80            key: "missing".to_string(),
81        }])
82        .expect("missing remove key should be ignored");
83        assert_eq!(map.get("a"), Some(&1));
84    }
85
86    #[test]
87    fn map_replace_missing_key_is_noop() {
88        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
89        map.merge(vec![MapPatch::Replace {
90            key: "missing".to_string(),
91            value: 3u8,
92        }])
93        .expect("missing replace key should be ignored");
94        assert_eq!(map.get("a"), Some(&1));
95    }
96
97    #[test]
98    fn map_clear_with_other_operations_returns_error() {
99        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
100        let err = map
101            .merge(vec![
102                MapPatch::Clear,
103                MapPatch::Insert {
104                    key: "b".to_string(),
105                    value: 2u8,
106                },
107            ])
108            .expect_err("clear combined with key ops should fail");
109        assert!(matches!(
110            err.leaf(),
111            MergePatchError::CardinalityViolation {
112                expected: 1,
113                actual: 2,
114            }
115        ));
116    }
117
118    #[test]
119    fn map_duplicate_key_operations_returns_error() {
120        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
121        let err = map
122            .merge(vec![
123                MapPatch::Insert {
124                    key: "a".to_string(),
125                    value: 3u8,
126                },
127                MapPatch::Replace {
128                    key: "a".to_string(),
129                    value: 4u8,
130                },
131            ])
132            .expect_err("duplicate key operations should fail");
133        assert!(matches!(
134            err.leaf(),
135            MergePatchError::InvalidShape {
136                expected: "unique key operations per map patch batch",
137                actual: "duplicate key operation",
138            }
139        ));
140    }
141}