Skip to main content

icydb_core/traits/
update.rs

1use crate::{
2    traits::AsView,
3    view::{ListPatch, MapPatch, SetPatch},
4};
5use candid::CandidType;
6use std::{
7    collections::{
8        BTreeMap, BTreeSet, HashMap, HashSet, btree_map::Entry as BTreeMapEntry,
9        hash_map::Entry as HashMapEntry,
10    },
11    hash::{BuildHasher, Hash},
12};
13use thiserror::Error as ThisError;
14
15///
16/// ViewPatchError
17///
18/// Structured failures for user-driven patch application.
19///
20
21#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
22pub enum ViewPatchError {
23    #[error("invalid patch shape: expected {expected}, found {actual}")]
24    InvalidShape {
25        expected: &'static str,
26        actual: &'static str,
27    },
28
29    #[error("missing key for map operation: {operation}")]
30    MissingKey { operation: &'static str },
31
32    #[error("invalid patch cardinality: expected {expected}, found {actual}")]
33    CardinalityViolation { expected: usize, actual: usize },
34
35    #[error("patch merge failed at {path}: {source}")]
36    Context {
37        path: String,
38        #[source]
39        source: Box<Self>,
40    },
41}
42
43impl ViewPatchError {
44    /// Prepend a field segment to the merge error path.
45    #[must_use]
46    pub fn with_field(self, field: impl AsRef<str>) -> Self {
47        self.with_path_segment(field.as_ref())
48    }
49
50    /// Prepend an index segment to the merge error path.
51    #[must_use]
52    pub fn with_index(self, index: usize) -> Self {
53        self.with_path_segment(format!("[{index}]"))
54    }
55
56    /// Return the full contextual path, if available.
57    #[must_use]
58    pub const fn path(&self) -> Option<&str> {
59        match self {
60            Self::Context { path, .. } => Some(path.as_str()),
61            _ => None,
62        }
63    }
64
65    /// Return the innermost, non-context merge error variant.
66    #[must_use]
67    pub fn leaf(&self) -> &Self {
68        match self {
69            Self::Context { source, .. } => source.leaf(),
70            _ => self,
71        }
72    }
73
74    #[must_use]
75    fn with_path_segment(self, segment: impl Into<String>) -> Self {
76        let segment = segment.into();
77        match self {
78            Self::Context { path, source } => Self::Context {
79                path: Self::join_segments(segment.as_str(), path.as_str()),
80                source,
81            },
82            source => Self::Context {
83                path: segment,
84                source: Box::new(source),
85            },
86        }
87    }
88
89    #[must_use]
90    fn join_segments(prefix: &str, suffix: &str) -> String {
91        if suffix.starts_with('[') {
92            format!("{prefix}{suffix}")
93        } else {
94            format!("{prefix}.{suffix}")
95        }
96    }
97}
98
99///
100/// UpdateView
101///
102
103pub trait UpdateView: AsView {
104    /// Payload accepted when updating this value.
105    type UpdateViewType: CandidType + Default;
106
107    /// Merge the update payload into self.
108    fn merge(&mut self, _update: Self::UpdateViewType) -> Result<(), ViewPatchError> {
109        Ok(())
110    }
111}
112
113impl<T> UpdateView for Option<T>
114where
115    T: UpdateView + Default,
116{
117    type UpdateViewType = Option<T::UpdateViewType>;
118
119    fn merge(&mut self, update: Self::UpdateViewType) -> Result<(), ViewPatchError> {
120        match update {
121            None => {
122                // Field was provided (outer Some), inner None means explicit delete
123                *self = None;
124            }
125            Some(inner_update) => {
126                if let Some(inner_value) = self.as_mut() {
127                    inner_value
128                        .merge(inner_update)
129                        .map_err(|err| err.with_field("value"))?;
130                } else {
131                    let mut new_value = T::default();
132                    new_value
133                        .merge(inner_update)
134                        .map_err(|err| err.with_field("value"))?;
135                    *self = Some(new_value);
136                }
137            }
138        }
139
140        Ok(())
141    }
142}
143
144impl<T> UpdateView for Vec<T>
145where
146    T: UpdateView + Default,
147{
148    // Payload is T::UpdateViewType, which *is* CandidType
149    type UpdateViewType = Vec<ListPatch<T::UpdateViewType>>;
150
151    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
152        for patch in patches {
153            match patch {
154                ListPatch::Update { index, patch } => {
155                    if let Some(elem) = self.get_mut(index) {
156                        elem.merge(patch).map_err(|err| err.with_index(index))?;
157                    }
158                }
159                ListPatch::Insert { index, value } => {
160                    let idx = index.min(self.len());
161                    let mut elem = T::default();
162                    elem.merge(value).map_err(|err| err.with_index(idx))?;
163                    self.insert(idx, elem);
164                }
165                ListPatch::Push { value } => {
166                    let idx = self.len();
167                    let mut elem = T::default();
168                    elem.merge(value).map_err(|err| err.with_index(idx))?;
169                    self.push(elem);
170                }
171                ListPatch::Overwrite { values } => {
172                    self.clear();
173                    self.reserve(values.len());
174
175                    for (index, value) in values.into_iter().enumerate() {
176                        let mut elem = T::default();
177                        elem.merge(value).map_err(|err| err.with_index(index))?;
178                        self.push(elem);
179                    }
180                }
181                ListPatch::Remove { index } => {
182                    if index < self.len() {
183                        self.remove(index);
184                    }
185                }
186                ListPatch::Clear => self.clear(),
187            }
188        }
189
190        Ok(())
191    }
192}
193
194impl<T, S> UpdateView for HashSet<T, S>
195where
196    T: UpdateView + Clone + Default + Eq + Hash,
197    S: BuildHasher + Default,
198{
199    type UpdateViewType = Vec<SetPatch<T::UpdateViewType>>;
200
201    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
202        for patch in patches {
203            match patch {
204                SetPatch::Insert(value) => {
205                    let mut elem = T::default();
206                    elem.merge(value).map_err(|err| err.with_field("insert"))?;
207                    self.insert(elem);
208                }
209                SetPatch::Remove(value) => {
210                    let mut elem = T::default();
211                    elem.merge(value).map_err(|err| err.with_field("remove"))?;
212                    self.remove(&elem);
213                }
214                SetPatch::Overwrite { values } => {
215                    self.clear();
216
217                    for (index, value) in values.into_iter().enumerate() {
218                        let mut elem = T::default();
219                        elem.merge(value)
220                            .map_err(|err| err.with_field("overwrite").with_index(index))?;
221                        self.insert(elem);
222                    }
223                }
224                SetPatch::Clear => self.clear(),
225            }
226        }
227
228        Ok(())
229    }
230}
231
232/// Internal representation used to normalize map patches before application.
233enum MapPatchOp<K, V> {
234    Insert { key: K, value: V },
235    Remove { key: K },
236    Replace { key: K, value: V },
237    Clear,
238}
239
240impl<K, V, S> UpdateView for HashMap<K, V, S>
241where
242    K: UpdateView + Clone + Default + Eq + Hash,
243    V: UpdateView + Default,
244    S: BuildHasher + Default,
245{
246    type UpdateViewType = Vec<MapPatch<K::UpdateViewType, V::UpdateViewType>>;
247
248    #[expect(clippy::too_many_lines)]
249    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
250        // Phase 1: decode patch payload into concrete keys.
251        let mut ops = Vec::with_capacity(patches.len());
252        for patch in patches {
253            match patch {
254                MapPatch::Insert { key, value } => {
255                    let mut key_value = K::default();
256                    key_value
257                        .merge(key)
258                        .map_err(|err| err.with_field("insert").with_field("key"))?;
259                    ops.push(MapPatchOp::Insert {
260                        key: key_value,
261                        value,
262                    });
263                }
264                MapPatch::Remove { key } => {
265                    let mut key_value = K::default();
266                    key_value
267                        .merge(key)
268                        .map_err(|err| err.with_field("remove").with_field("key"))?;
269                    ops.push(MapPatchOp::Remove { key: key_value });
270                }
271                MapPatch::Replace { key, value } => {
272                    let mut key_value = K::default();
273                    key_value
274                        .merge(key)
275                        .map_err(|err| err.with_field("replace").with_field("key"))?;
276                    ops.push(MapPatchOp::Replace {
277                        key: key_value,
278                        value,
279                    });
280                }
281                MapPatch::Clear => ops.push(MapPatchOp::Clear),
282            }
283        }
284
285        // Phase 2: reject ambiguous patch batches to keep semantics deterministic.
286        let mut saw_clear = false;
287        let mut touched = HashSet::with_capacity(ops.len());
288        for op in &ops {
289            match op {
290                MapPatchOp::Clear => {
291                    if saw_clear {
292                        return Err(ViewPatchError::InvalidShape {
293                            expected: "at most one Clear operation per map patch batch",
294                            actual: "duplicate Clear operations",
295                        });
296                    }
297                    saw_clear = true;
298                    if ops.len() != 1 {
299                        return Err(ViewPatchError::CardinalityViolation {
300                            expected: 1,
301                            actual: ops.len(),
302                        });
303                    }
304                }
305                MapPatchOp::Insert { key, .. }
306                | MapPatchOp::Remove { key }
307                | MapPatchOp::Replace { key, .. } => {
308                    if saw_clear {
309                        return Err(ViewPatchError::InvalidShape {
310                            expected: "Clear must be the only operation in a map patch batch",
311                            actual: "Clear combined with key operation",
312                        });
313                    }
314                    if !touched.insert(key.clone()) {
315                        return Err(ViewPatchError::InvalidShape {
316                            expected: "unique key operations per map patch batch",
317                            actual: "duplicate key operation",
318                        });
319                    }
320                }
321            }
322        }
323        if saw_clear {
324            self.clear();
325            return Ok(());
326        }
327
328        // Phase 3: apply deterministic map operations.
329        for op in ops {
330            match op {
331                MapPatchOp::Insert { key, value } => match self.entry(key) {
332                    HashMapEntry::Occupied(mut slot) => {
333                        slot.get_mut()
334                            .merge(value)
335                            .map_err(|err| err.with_field("insert").with_field("value"))?;
336                    }
337                    HashMapEntry::Vacant(slot) => {
338                        let mut value_value = V::default();
339                        value_value
340                            .merge(value)
341                            .map_err(|err| err.with_field("insert").with_field("value"))?;
342                        slot.insert(value_value);
343                    }
344                },
345                MapPatchOp::Remove { key } => {
346                    if self.remove(&key).is_none() {
347                        return Err(ViewPatchError::MissingKey {
348                            operation: "remove",
349                        });
350                    }
351                }
352                MapPatchOp::Replace { key, value } => match self.entry(key) {
353                    HashMapEntry::Occupied(mut slot) => {
354                        slot.get_mut()
355                            .merge(value)
356                            .map_err(|err| err.with_field("replace").with_field("value"))?;
357                    }
358                    HashMapEntry::Vacant(_) => {
359                        return Err(ViewPatchError::MissingKey {
360                            operation: "replace",
361                        });
362                    }
363                },
364                MapPatchOp::Clear => {
365                    return Err(ViewPatchError::InvalidShape {
366                        expected: "Clear to be handled before apply phase",
367                        actual: "Clear reached apply phase",
368                    });
369                }
370            }
371        }
372
373        Ok(())
374    }
375}
376
377impl<T> UpdateView for BTreeSet<T>
378where
379    T: UpdateView + Clone + Default + Ord,
380{
381    type UpdateViewType = Vec<SetPatch<T::UpdateViewType>>;
382
383    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
384        for patch in patches {
385            match patch {
386                SetPatch::Insert(value) => {
387                    let mut elem = T::default();
388                    elem.merge(value).map_err(|err| err.with_field("insert"))?;
389                    self.insert(elem);
390                }
391                SetPatch::Remove(value) => {
392                    let mut elem = T::default();
393                    elem.merge(value).map_err(|err| err.with_field("remove"))?;
394                    self.remove(&elem);
395                }
396                SetPatch::Overwrite { values } => {
397                    self.clear();
398
399                    for (index, value) in values.into_iter().enumerate() {
400                        let mut elem = T::default();
401                        elem.merge(value)
402                            .map_err(|err| err.with_field("overwrite").with_index(index))?;
403                        self.insert(elem);
404                    }
405                }
406                SetPatch::Clear => self.clear(),
407            }
408        }
409
410        Ok(())
411    }
412}
413
414impl<K, V> UpdateView for BTreeMap<K, V>
415where
416    K: UpdateView + Clone + Default + Ord,
417    V: UpdateView + Default,
418{
419    type UpdateViewType = Vec<MapPatch<K::UpdateViewType, V::UpdateViewType>>;
420
421    #[expect(clippy::too_many_lines)]
422    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
423        // Phase 1: decode patch payload into concrete keys.
424        let mut ops = Vec::with_capacity(patches.len());
425        for patch in patches {
426            match patch {
427                MapPatch::Insert { key, value } => {
428                    let mut key_value = K::default();
429                    key_value
430                        .merge(key)
431                        .map_err(|err| err.with_field("insert").with_field("key"))?;
432                    ops.push(MapPatchOp::Insert {
433                        key: key_value,
434                        value,
435                    });
436                }
437                MapPatch::Remove { key } => {
438                    let mut key_value = K::default();
439                    key_value
440                        .merge(key)
441                        .map_err(|err| err.with_field("remove").with_field("key"))?;
442                    ops.push(MapPatchOp::Remove { key: key_value });
443                }
444                MapPatch::Replace { key, value } => {
445                    let mut key_value = K::default();
446                    key_value
447                        .merge(key)
448                        .map_err(|err| err.with_field("replace").with_field("key"))?;
449                    ops.push(MapPatchOp::Replace {
450                        key: key_value,
451                        value,
452                    });
453                }
454                MapPatch::Clear => ops.push(MapPatchOp::Clear),
455            }
456        }
457
458        // Phase 2: reject ambiguous patch batches to keep semantics deterministic.
459        let mut saw_clear = false;
460        let mut touched = BTreeSet::new();
461        for op in &ops {
462            match op {
463                MapPatchOp::Clear => {
464                    if saw_clear {
465                        return Err(ViewPatchError::InvalidShape {
466                            expected: "at most one Clear operation per map patch batch",
467                            actual: "duplicate Clear operations",
468                        });
469                    }
470                    saw_clear = true;
471                    if ops.len() != 1 {
472                        return Err(ViewPatchError::CardinalityViolation {
473                            expected: 1,
474                            actual: ops.len(),
475                        });
476                    }
477                }
478                MapPatchOp::Insert { key, .. }
479                | MapPatchOp::Remove { key }
480                | MapPatchOp::Replace { key, .. } => {
481                    if saw_clear {
482                        return Err(ViewPatchError::InvalidShape {
483                            expected: "Clear must be the only operation in a map patch batch",
484                            actual: "Clear combined with key operation",
485                        });
486                    }
487                    if !touched.insert(key.clone()) {
488                        return Err(ViewPatchError::InvalidShape {
489                            expected: "unique key operations per map patch batch",
490                            actual: "duplicate key operation",
491                        });
492                    }
493                }
494            }
495        }
496        if saw_clear {
497            self.clear();
498            return Ok(());
499        }
500
501        // Phase 3: apply deterministic map operations.
502        for op in ops {
503            match op {
504                MapPatchOp::Insert { key, value } => match self.entry(key) {
505                    BTreeMapEntry::Occupied(mut slot) => {
506                        slot.get_mut()
507                            .merge(value)
508                            .map_err(|err| err.with_field("insert").with_field("value"))?;
509                    }
510                    BTreeMapEntry::Vacant(slot) => {
511                        let mut value_value = V::default();
512                        value_value
513                            .merge(value)
514                            .map_err(|err| err.with_field("insert").with_field("value"))?;
515                        slot.insert(value_value);
516                    }
517                },
518                MapPatchOp::Remove { key } => {
519                    if self.remove(&key).is_none() {
520                        return Err(ViewPatchError::MissingKey {
521                            operation: "remove",
522                        });
523                    }
524                }
525                MapPatchOp::Replace { key, value } => match self.entry(key) {
526                    BTreeMapEntry::Occupied(mut slot) => {
527                        slot.get_mut()
528                            .merge(value)
529                            .map_err(|err| err.with_field("replace").with_field("value"))?;
530                    }
531                    BTreeMapEntry::Vacant(_) => {
532                        return Err(ViewPatchError::MissingKey {
533                            operation: "replace",
534                        });
535                    }
536                },
537                MapPatchOp::Clear => {
538                    return Err(ViewPatchError::InvalidShape {
539                        expected: "Clear to be handled before apply phase",
540                        actual: "Clear reached apply phase",
541                    });
542                }
543            }
544        }
545
546        Ok(())
547    }
548}
549
550macro_rules! impl_update_view {
551    ($($type:ty),*) => {
552        $(
553            impl UpdateView for $type {
554                type UpdateViewType = Self;
555
556                fn merge(
557                    &mut self,
558                    update: Self::UpdateViewType,
559                ) -> Result<(), ViewPatchError> {
560                    *self = update;
561
562                    Ok(())
563                }
564            }
565        )*
566    };
567}
568
569impl_update_view!(bool, i8, i16, i32, i64, u8, u16, u32, u64, String);