Skip to main content

icydb_core/
view.rs

1use crate::traits::{AsView, CreateView, UpdateView};
2use candid::CandidType;
3use serde::{Deserialize, Serialize};
4
5///
6/// Type Aliases
7///
8
9pub type View<T> = <T as AsView>::ViewType;
10pub type Create<T> = <T as CreateView>::CreateViewType;
11pub type Update<T> = <T as UpdateView>::UpdateViewType;
12
13///
14/// ListPatch
15///
16
17/// Positional list patches applied in order.
18/// Indices refer to the list state at the time each patch executes.
19/// `Insert` clamps out-of-bounds indices to the tail; `Remove` ignores invalid indices.
20/// `Update` only applies to existing elements and never creates new entries.
21/// `Overwrite` replaces the entire list with the provided values.
22#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
23pub enum ListPatch<U> {
24    Update { index: usize, patch: U },
25    Insert { index: usize, value: U },
26    Push { value: U },
27    Overwrite { values: Vec<U> },
28    Remove { index: usize },
29    Clear,
30}
31
32///
33/// SetPatch
34///
35
36/// Set operations applied in-order; `Overwrite` replaces the entire set.
37#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
38pub enum SetPatch<U> {
39    Insert(U),
40    Remove(U),
41    Overwrite { values: Vec<U> },
42    Clear,
43}
44
45///
46/// MapPatch
47///
48/// Deterministic map mutations.
49///
50/// - Maps are unordered values; insertion order is discarded.
51/// - `Insert` is an upsert.
52/// - `Replace` requires an existing key.
53/// - `Remove` requires an existing key.
54/// - `Clear` must be the only patch in the batch.
55///
56/// Invalid patch shapes and missing-key operations are returned by
57/// `UpdateView::merge` as `Error` values with
58/// `ErrorDetail::ViewPatch` detail.
59///
60#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
61pub enum MapPatch<K, V> {
62    Insert { key: K, value: V },
63    Remove { key: K },
64    Replace { key: K, value: V },
65    Clear,
66}
67
68impl<K, V> From<(K, Option<V>)> for MapPatch<K, V> {
69    fn from((key, value): (K, Option<V>)) -> Self {
70        match value {
71            Some(value) => Self::Insert { key, value },
72            None => Self::Remove { key },
73        }
74    }
75}
76
77///
78/// TESTS
79///
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::error::ErrorClass;
85    use crate::traits::ViewPatchError;
86    use std::collections::{BTreeMap, HashMap, HashSet};
87
88    #[test]
89    fn vec_partial_patches() {
90        let mut values = vec![10u8, 20, 30];
91        let patches = vec![
92            ListPatch::Update {
93                index: 1,
94                patch: 99,
95            },
96            ListPatch::Insert {
97                index: 1,
98                value: 11,
99            },
100            ListPatch::Remove { index: 0 },
101        ];
102
103        values
104            .merge(patches)
105            .expect("list patch merge should succeed");
106        assert_eq!(values, vec![11, 99, 30]);
107    }
108
109    #[test]
110    fn vec_overwrite_replaces_contents() {
111        let mut values = vec![1u8, 2, 3];
112        let patches = vec![ListPatch::Overwrite {
113            values: vec![9u8, 8],
114        }];
115
116        values
117            .merge(patches)
118            .expect("list patch merge should succeed");
119        assert_eq!(values, vec![9, 8]);
120    }
121
122    #[test]
123    fn set_insert_remove_without_clear() {
124        let mut set: HashSet<u8> = [1, 2, 3].into_iter().collect();
125        let patches = vec![SetPatch::Remove(2), SetPatch::Insert(4)];
126
127        set.merge(patches).expect("set patch merge should succeed");
128        let expected: HashSet<u8> = [1, 3, 4].into_iter().collect();
129        assert_eq!(set, expected);
130    }
131
132    #[test]
133    fn set_overwrite_replaces_contents() {
134        let mut set: HashSet<u8> = [1, 2, 3].into_iter().collect();
135        let patches = vec![SetPatch::Overwrite {
136            values: vec![3u8, 4, 5],
137        }];
138
139        set.merge(patches).expect("set patch merge should succeed");
140        let expected: HashSet<u8> = [3, 4, 5].into_iter().collect();
141        assert_eq!(set, expected);
142    }
143
144    #[test]
145    fn map_insert_in_place_and_remove() {
146        let mut map: HashMap<String, u8> = [("a".into(), 1u8), ("keep".into(), 9u8)]
147            .into_iter()
148            .collect();
149
150        let patches = vec![
151            MapPatch::Insert {
152                key: "a".to_string(),
153                value: 5u8,
154            },
155            MapPatch::Remove {
156                key: "keep".to_string(),
157            },
158            MapPatch::Insert {
159                key: "insert".to_string(),
160                value: 7u8,
161            },
162        ];
163
164        map.merge(patches).expect("map patch merge should succeed");
165
166        assert_eq!(map.get("a"), Some(&5));
167        assert_eq!(map.get("insert"), Some(&7));
168        assert!(!map.contains_key("keep"));
169    }
170
171    #[test]
172    fn map_replace_updates_existing_entry() {
173        let mut map: HashMap<String, u8> = [("keep".into(), 1u8), ("replace".into(), 2u8)]
174            .into_iter()
175            .collect();
176
177        let patches = vec![MapPatch::Replace {
178            key: "replace".to_string(),
179            value: 8u8,
180        }];
181
182        map.merge(patches).expect("map patch merge should succeed");
183
184        assert_eq!(map.get("keep"), Some(&1));
185        assert_eq!(map.get("replace"), Some(&8));
186    }
187
188    #[test]
189    fn btree_map_clear_replaces_with_empty_map() {
190        let mut map: BTreeMap<String, u8> =
191            [("a".into(), 1u8), ("b".into(), 2u8)].into_iter().collect();
192
193        map.merge(vec![MapPatch::Clear])
194            .expect("map clear patch should succeed");
195
196        assert!(map.is_empty());
197    }
198
199    #[test]
200    fn map_remove_missing_key_returns_error() {
201        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
202        let err = map
203            .merge(vec![MapPatch::Remove {
204                key: "missing".to_string(),
205            }])
206            .expect_err("missing remove key should fail");
207        assert_eq!(err.class, ErrorClass::NotFound);
208        assert!(matches!(
209            err.leaf(),
210            Some(ViewPatchError::MissingKey {
211                operation: "remove"
212            })
213        ));
214    }
215
216    #[test]
217    fn map_replace_missing_key_returns_error() {
218        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
219        let err = map
220            .merge(vec![MapPatch::Replace {
221                key: "missing".to_string(),
222                value: 3u8,
223            }])
224            .expect_err("missing replace key should fail");
225        assert_eq!(err.class, ErrorClass::NotFound);
226        assert!(matches!(
227            err.leaf(),
228            Some(ViewPatchError::MissingKey {
229                operation: "replace"
230            })
231        ));
232    }
233
234    #[test]
235    fn map_clear_with_other_operations_returns_error() {
236        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
237        let err = map
238            .merge(vec![
239                MapPatch::Clear,
240                MapPatch::Insert {
241                    key: "b".to_string(),
242                    value: 2u8,
243                },
244            ])
245            .expect_err("clear combined with key ops should fail");
246        assert_eq!(err.class, ErrorClass::Unsupported);
247        assert!(matches!(
248            err.leaf(),
249            Some(ViewPatchError::CardinalityViolation {
250                expected: 1,
251                actual: 2,
252            })
253        ));
254    }
255
256    #[test]
257    fn map_duplicate_key_operations_returns_error() {
258        let mut map: HashMap<String, u8> = std::iter::once(("a".into(), 1u8)).collect();
259        let err = map
260            .merge(vec![
261                MapPatch::Insert {
262                    key: "a".to_string(),
263                    value: 3u8,
264                },
265                MapPatch::Replace {
266                    key: "a".to_string(),
267                    value: 4u8,
268                },
269            ])
270            .expect_err("duplicate key operations should fail");
271        assert_eq!(err.class, ErrorClass::Unsupported);
272        assert!(matches!(
273            err.leaf(),
274            Some(ViewPatchError::InvalidPatchShape {
275                expected: "unique key operations per map patch batch",
276                actual: "duplicate key operation",
277            })
278        ));
279    }
280}