Skip to main content

icydb_core/traits/
view.rs

1use crate::view::{ListPatch, MapPatch, SetPatch};
2use candid::CandidType;
3use std::{
4    collections::{
5        BTreeMap, BTreeSet, HashMap, HashSet, btree_map::Entry as BTreeMapEntry,
6        hash_map::Entry as HashMapEntry,
7    },
8    hash::{BuildHasher, Hash},
9    iter::IntoIterator,
10};
11use thiserror::Error as ThisError;
12
13///
14/// AsView
15///
16/// Recursive for all field/value nodes
17/// `from_view` is infallible; view values are treated as canonical.
18///
19
20pub trait AsView: Sized {
21    type ViewType: Default;
22
23    fn as_view(&self) -> Self::ViewType;
24    fn from_view(view: Self::ViewType) -> Self;
25}
26
27impl AsView for () {
28    type ViewType = Self;
29
30    fn as_view(&self) -> Self::ViewType {}
31
32    fn from_view((): Self::ViewType) -> Self {}
33}
34
35impl AsView for String {
36    type ViewType = Self;
37
38    fn as_view(&self) -> Self::ViewType {
39        self.clone()
40    }
41
42    fn from_view(view: Self::ViewType) -> Self {
43        view
44    }
45}
46
47// Make Box<T> *not* appear in the view type
48impl<T: AsView> AsView for Box<T> {
49    type ViewType = T::ViewType;
50
51    fn as_view(&self) -> Self::ViewType {
52        // Delegate to inner value
53        T::as_view(self.as_ref())
54    }
55
56    fn from_view(view: Self::ViewType) -> Self {
57        // Re-box after reconstructing inner
58        Self::new(T::from_view(view))
59    }
60}
61
62impl<T: AsView> AsView for Option<T> {
63    type ViewType = Option<T::ViewType>;
64
65    fn as_view(&self) -> Self::ViewType {
66        self.as_ref().map(AsView::as_view)
67    }
68
69    fn from_view(view: Self::ViewType) -> Self {
70        view.map(T::from_view)
71    }
72}
73
74impl<T: AsView> AsView for Vec<T> {
75    type ViewType = Vec<T::ViewType>;
76
77    fn as_view(&self) -> Self::ViewType {
78        self.iter().map(AsView::as_view).collect()
79    }
80
81    fn from_view(view: Self::ViewType) -> Self {
82        view.into_iter().map(T::from_view).collect()
83    }
84}
85
86impl<T, S> AsView for HashSet<T, S>
87where
88    T: AsView + Eq + Hash + Clone,
89    S: BuildHasher + Default,
90{
91    type ViewType = Vec<T::ViewType>;
92
93    fn as_view(&self) -> Self::ViewType {
94        self.iter().map(AsView::as_view).collect()
95    }
96
97    fn from_view(view: Self::ViewType) -> Self {
98        view.into_iter().map(T::from_view).collect()
99    }
100}
101
102impl<K, V, S> AsView for HashMap<K, V, S>
103where
104    K: AsView + Eq + Hash + Clone,
105    V: AsView,
106    S: BuildHasher + Default,
107{
108    type ViewType = Vec<(K::ViewType, V::ViewType)>;
109
110    fn as_view(&self) -> Self::ViewType {
111        self.iter()
112            .map(|(k, v)| (k.as_view(), v.as_view()))
113            .collect()
114    }
115
116    fn from_view(view: Self::ViewType) -> Self {
117        view.into_iter()
118            .map(|(k, v)| (K::from_view(k), V::from_view(v)))
119            .collect()
120    }
121}
122
123impl<T> AsView for BTreeSet<T>
124where
125    T: AsView + Ord + Clone,
126{
127    type ViewType = Vec<T::ViewType>;
128
129    fn as_view(&self) -> Self::ViewType {
130        self.iter().map(AsView::as_view).collect()
131    }
132
133    fn from_view(view: Self::ViewType) -> Self {
134        view.into_iter().map(T::from_view).collect()
135    }
136}
137
138impl<K, V> AsView for BTreeMap<K, V>
139where
140    K: AsView + Ord + Clone,
141    V: AsView,
142{
143    type ViewType = Vec<(K::ViewType, V::ViewType)>;
144
145    fn as_view(&self) -> Self::ViewType {
146        self.iter()
147            .map(|(k, v)| (k.as_view(), v.as_view()))
148            .collect()
149    }
150
151    fn from_view(view: Self::ViewType) -> Self {
152        view.into_iter()
153            .map(|(k, v)| (K::from_view(k), V::from_view(v)))
154            .collect()
155    }
156}
157
158#[macro_export]
159macro_rules! impl_view {
160    ($($type:ty),*) => {
161        $(
162            impl AsView for $type {
163                type ViewType = Self;
164
165                fn as_view(&self) -> Self::ViewType {
166                    *self
167                }
168
169                fn from_view(view: Self::ViewType) -> Self {
170                    view
171                }
172            }
173        )*
174    };
175}
176
177impl_view!(bool, i8, i16, i32, i64, u8, u16, u32, u64);
178
179impl AsView for f32 {
180    type ViewType = Self;
181
182    fn as_view(&self) -> Self::ViewType {
183        *self
184    }
185
186    fn from_view(view: Self::ViewType) -> Self {
187        if view.is_finite() {
188            if view == 0.0 { 0.0 } else { view }
189        } else {
190            0.0
191        }
192    }
193}
194
195impl AsView for f64 {
196    type ViewType = Self;
197
198    fn as_view(&self) -> Self::ViewType {
199        *self
200    }
201
202    fn from_view(view: Self::ViewType) -> Self {
203        if view.is_finite() {
204            if view == 0.0 { 0.0 } else { view }
205        } else {
206            0.0
207        }
208    }
209}
210
211///
212/// CreateView
213///
214
215pub trait CreateView: AsView {
216    /// Payload accepted when creating this value.
217    ///
218    /// This is often equal to ViewType, but may differ
219    /// (e.g. Option<T>, defaults, omissions).
220    type CreateViewType: CandidType + Default;
221
222    fn from_create_view(view: Self::CreateViewType) -> Self;
223}
224
225///
226/// ViewPatchError
227///
228/// Structured failures for user-driven patch application.
229///
230#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
231pub enum ViewPatchError {
232    #[error("invalid patch shape: expected {expected}, found {actual}")]
233    InvalidPatchShape {
234        expected: &'static str,
235        actual: &'static str,
236    },
237
238    #[error("missing key for map operation: {operation}")]
239    MissingKey { operation: &'static str },
240
241    #[error("invalid patch cardinality: expected {expected}, found {actual}")]
242    CardinalityViolation { expected: usize, actual: usize },
243
244    #[error("patch merge failed at {path}: {source}")]
245    Context {
246        path: String,
247        #[source]
248        source: Box<Self>,
249    },
250}
251
252impl ViewPatchError {
253    /// Prepend a field segment to the merge error path.
254    #[must_use]
255    pub fn with_field(self, field: impl AsRef<str>) -> Self {
256        self.with_path_segment(field.as_ref())
257    }
258
259    /// Prepend an index segment to the merge error path.
260    #[must_use]
261    pub fn with_index(self, index: usize) -> Self {
262        self.with_path_segment(format!("[{index}]"))
263    }
264
265    /// Return the full contextual path, if available.
266    #[must_use]
267    pub const fn path(&self) -> Option<&str> {
268        match self {
269            Self::Context { path, .. } => Some(path.as_str()),
270            _ => None,
271        }
272    }
273
274    /// Return the innermost, non-context merge error variant.
275    #[must_use]
276    pub fn leaf(&self) -> &Self {
277        match self {
278            Self::Context { source, .. } => source.leaf(),
279            _ => self,
280        }
281    }
282
283    #[must_use]
284    fn with_path_segment(self, segment: impl Into<String>) -> Self {
285        let segment = segment.into();
286        match self {
287            Self::Context { path, source } => Self::Context {
288                path: Self::join_segments(segment.as_str(), path.as_str()),
289                source,
290            },
291            source => Self::Context {
292                path: segment,
293                source: Box::new(source),
294            },
295        }
296    }
297
298    #[must_use]
299    fn join_segments(prefix: &str, suffix: &str) -> String {
300        if suffix.starts_with('[') {
301            format!("{prefix}{suffix}")
302        } else {
303            format!("{prefix}.{suffix}")
304        }
305    }
306}
307
308///
309/// UpdateView
310///
311
312pub trait UpdateView: AsView {
313    /// Payload accepted when updating this value.
314    type UpdateViewType: CandidType + Default;
315
316    /// Merge the update payload into self.
317    fn merge(&mut self, _update: Self::UpdateViewType) -> Result<(), ViewPatchError> {
318        Ok(())
319    }
320}
321
322impl<T> UpdateView for Option<T>
323where
324    T: UpdateView + Default,
325{
326    type UpdateViewType = Option<T::UpdateViewType>;
327
328    fn merge(&mut self, update: Self::UpdateViewType) -> Result<(), ViewPatchError> {
329        match update {
330            None => {
331                // Field was provided (outer Some), inner None means explicit delete
332                *self = None;
333            }
334            Some(inner_update) => {
335                if let Some(inner_value) = self.as_mut() {
336                    inner_value
337                        .merge(inner_update)
338                        .map_err(|err| err.with_field("value"))?;
339                } else {
340                    let mut new_value = T::default();
341                    new_value
342                        .merge(inner_update)
343                        .map_err(|err| err.with_field("value"))?;
344                    *self = Some(new_value);
345                }
346            }
347        }
348
349        Ok(())
350    }
351}
352
353impl<T> UpdateView for Vec<T>
354where
355    T: UpdateView + Default,
356{
357    // Payload is T::UpdateViewType, which *is* CandidType
358    type UpdateViewType = Vec<ListPatch<T::UpdateViewType>>;
359
360    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
361        for patch in patches {
362            match patch {
363                ListPatch::Update { index, patch } => {
364                    if let Some(elem) = self.get_mut(index) {
365                        elem.merge(patch).map_err(|err| err.with_index(index))?;
366                    }
367                }
368                ListPatch::Insert { index, value } => {
369                    let idx = index.min(self.len());
370                    let mut elem = T::default();
371                    elem.merge(value).map_err(|err| err.with_index(idx))?;
372                    self.insert(idx, elem);
373                }
374                ListPatch::Push { value } => {
375                    let idx = self.len();
376                    let mut elem = T::default();
377                    elem.merge(value).map_err(|err| err.with_index(idx))?;
378                    self.push(elem);
379                }
380                ListPatch::Overwrite { values } => {
381                    self.clear();
382                    self.reserve(values.len());
383
384                    for (index, value) in values.into_iter().enumerate() {
385                        let mut elem = T::default();
386                        elem.merge(value).map_err(|err| err.with_index(index))?;
387                        self.push(elem);
388                    }
389                }
390                ListPatch::Remove { index } => {
391                    if index < self.len() {
392                        self.remove(index);
393                    }
394                }
395                ListPatch::Clear => self.clear(),
396            }
397        }
398
399        Ok(())
400    }
401}
402
403impl<T, S> UpdateView for HashSet<T, S>
404where
405    T: UpdateView + Clone + Default + Eq + Hash,
406    S: BuildHasher + Default,
407{
408    type UpdateViewType = Vec<SetPatch<T::UpdateViewType>>;
409
410    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
411        for patch in patches {
412            match patch {
413                SetPatch::Insert(value) => {
414                    let mut elem = T::default();
415                    elem.merge(value).map_err(|err| err.with_field("insert"))?;
416                    self.insert(elem);
417                }
418                SetPatch::Remove(value) => {
419                    let mut elem = T::default();
420                    elem.merge(value).map_err(|err| err.with_field("remove"))?;
421                    self.remove(&elem);
422                }
423                SetPatch::Overwrite { values } => {
424                    self.clear();
425
426                    for (index, value) in values.into_iter().enumerate() {
427                        let mut elem = T::default();
428                        elem.merge(value)
429                            .map_err(|err| err.with_field("overwrite").with_index(index))?;
430                        self.insert(elem);
431                    }
432                }
433                SetPatch::Clear => self.clear(),
434            }
435        }
436
437        Ok(())
438    }
439}
440
441/// Internal representation used to normalize map patches before application.
442enum MapPatchOp<K, V> {
443    Insert { key: K, value: V },
444    Remove { key: K },
445    Replace { key: K, value: V },
446    Clear,
447}
448
449impl<K, V, S> UpdateView for HashMap<K, V, S>
450where
451    K: UpdateView + Clone + Default + Eq + Hash,
452    V: UpdateView + Default,
453    S: BuildHasher + Default,
454{
455    type UpdateViewType = Vec<MapPatch<K::UpdateViewType, V::UpdateViewType>>;
456
457    #[expect(clippy::too_many_lines)]
458    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
459        // Phase 1: decode patch payload into concrete keys.
460        let mut ops = Vec::with_capacity(patches.len());
461        for patch in patches {
462            match patch {
463                MapPatch::Insert { key, value } => {
464                    let mut key_value = K::default();
465                    key_value
466                        .merge(key)
467                        .map_err(|err| err.with_field("insert").with_field("key"))?;
468                    ops.push(MapPatchOp::Insert {
469                        key: key_value,
470                        value,
471                    });
472                }
473                MapPatch::Remove { key } => {
474                    let mut key_value = K::default();
475                    key_value
476                        .merge(key)
477                        .map_err(|err| err.with_field("remove").with_field("key"))?;
478                    ops.push(MapPatchOp::Remove { key: key_value });
479                }
480                MapPatch::Replace { key, value } => {
481                    let mut key_value = K::default();
482                    key_value
483                        .merge(key)
484                        .map_err(|err| err.with_field("replace").with_field("key"))?;
485                    ops.push(MapPatchOp::Replace {
486                        key: key_value,
487                        value,
488                    });
489                }
490                MapPatch::Clear => ops.push(MapPatchOp::Clear),
491            }
492        }
493
494        // Phase 2: reject ambiguous patch batches to keep semantics deterministic.
495        let mut saw_clear = false;
496        let mut touched = HashSet::with_capacity(ops.len());
497        for op in &ops {
498            match op {
499                MapPatchOp::Clear => {
500                    if saw_clear {
501                        return Err(ViewPatchError::InvalidPatchShape {
502                            expected: "at most one Clear operation per map patch batch",
503                            actual: "duplicate Clear operations",
504                        });
505                    }
506                    saw_clear = true;
507                    if ops.len() != 1 {
508                        return Err(ViewPatchError::CardinalityViolation {
509                            expected: 1,
510                            actual: ops.len(),
511                        });
512                    }
513                }
514                MapPatchOp::Insert { key, .. }
515                | MapPatchOp::Remove { key }
516                | MapPatchOp::Replace { key, .. } => {
517                    if saw_clear {
518                        return Err(ViewPatchError::InvalidPatchShape {
519                            expected: "Clear must be the only operation in a map patch batch",
520                            actual: "Clear combined with key operation",
521                        });
522                    }
523                    if !touched.insert(key.clone()) {
524                        return Err(ViewPatchError::InvalidPatchShape {
525                            expected: "unique key operations per map patch batch",
526                            actual: "duplicate key operation",
527                        });
528                    }
529                }
530            }
531        }
532        if saw_clear {
533            self.clear();
534            return Ok(());
535        }
536
537        // Phase 3: apply deterministic map operations.
538        for op in ops {
539            match op {
540                MapPatchOp::Insert { key, value } => match self.entry(key) {
541                    HashMapEntry::Occupied(mut slot) => {
542                        slot.get_mut()
543                            .merge(value)
544                            .map_err(|err| err.with_field("insert").with_field("value"))?;
545                    }
546                    HashMapEntry::Vacant(slot) => {
547                        let mut value_value = V::default();
548                        value_value
549                            .merge(value)
550                            .map_err(|err| err.with_field("insert").with_field("value"))?;
551                        slot.insert(value_value);
552                    }
553                },
554                MapPatchOp::Remove { key } => {
555                    if self.remove(&key).is_none() {
556                        return Err(ViewPatchError::MissingKey {
557                            operation: "remove",
558                        });
559                    }
560                }
561                MapPatchOp::Replace { key, value } => match self.entry(key) {
562                    HashMapEntry::Occupied(mut slot) => {
563                        slot.get_mut()
564                            .merge(value)
565                            .map_err(|err| err.with_field("replace").with_field("value"))?;
566                    }
567                    HashMapEntry::Vacant(_) => {
568                        return Err(ViewPatchError::MissingKey {
569                            operation: "replace",
570                        });
571                    }
572                },
573                MapPatchOp::Clear => {
574                    return Err(ViewPatchError::InvalidPatchShape {
575                        expected: "Clear to be handled before apply phase",
576                        actual: "Clear reached apply phase",
577                    });
578                }
579            }
580        }
581
582        Ok(())
583    }
584}
585
586impl<T> UpdateView for BTreeSet<T>
587where
588    T: UpdateView + Clone + Default + Ord,
589{
590    type UpdateViewType = Vec<SetPatch<T::UpdateViewType>>;
591
592    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
593        for patch in patches {
594            match patch {
595                SetPatch::Insert(value) => {
596                    let mut elem = T::default();
597                    elem.merge(value).map_err(|err| err.with_field("insert"))?;
598                    self.insert(elem);
599                }
600                SetPatch::Remove(value) => {
601                    let mut elem = T::default();
602                    elem.merge(value).map_err(|err| err.with_field("remove"))?;
603                    self.remove(&elem);
604                }
605                SetPatch::Overwrite { values } => {
606                    self.clear();
607
608                    for (index, value) in values.into_iter().enumerate() {
609                        let mut elem = T::default();
610                        elem.merge(value)
611                            .map_err(|err| err.with_field("overwrite").with_index(index))?;
612                        self.insert(elem);
613                    }
614                }
615                SetPatch::Clear => self.clear(),
616            }
617        }
618
619        Ok(())
620    }
621}
622
623impl<K, V> UpdateView for BTreeMap<K, V>
624where
625    K: UpdateView + Clone + Default + Ord,
626    V: UpdateView + Default,
627{
628    type UpdateViewType = Vec<MapPatch<K::UpdateViewType, V::UpdateViewType>>;
629
630    #[expect(clippy::too_many_lines)]
631    fn merge(&mut self, patches: Self::UpdateViewType) -> Result<(), ViewPatchError> {
632        // Phase 1: decode patch payload into concrete keys.
633        let mut ops = Vec::with_capacity(patches.len());
634        for patch in patches {
635            match patch {
636                MapPatch::Insert { key, value } => {
637                    let mut key_value = K::default();
638                    key_value
639                        .merge(key)
640                        .map_err(|err| err.with_field("insert").with_field("key"))?;
641                    ops.push(MapPatchOp::Insert {
642                        key: key_value,
643                        value,
644                    });
645                }
646                MapPatch::Remove { key } => {
647                    let mut key_value = K::default();
648                    key_value
649                        .merge(key)
650                        .map_err(|err| err.with_field("remove").with_field("key"))?;
651                    ops.push(MapPatchOp::Remove { key: key_value });
652                }
653                MapPatch::Replace { key, value } => {
654                    let mut key_value = K::default();
655                    key_value
656                        .merge(key)
657                        .map_err(|err| err.with_field("replace").with_field("key"))?;
658                    ops.push(MapPatchOp::Replace {
659                        key: key_value,
660                        value,
661                    });
662                }
663                MapPatch::Clear => ops.push(MapPatchOp::Clear),
664            }
665        }
666
667        // Phase 2: reject ambiguous patch batches to keep semantics deterministic.
668        let mut saw_clear = false;
669        let mut touched = BTreeSet::new();
670        for op in &ops {
671            match op {
672                MapPatchOp::Clear => {
673                    if saw_clear {
674                        return Err(ViewPatchError::InvalidPatchShape {
675                            expected: "at most one Clear operation per map patch batch",
676                            actual: "duplicate Clear operations",
677                        });
678                    }
679                    saw_clear = true;
680                    if ops.len() != 1 {
681                        return Err(ViewPatchError::CardinalityViolation {
682                            expected: 1,
683                            actual: ops.len(),
684                        });
685                    }
686                }
687                MapPatchOp::Insert { key, .. }
688                | MapPatchOp::Remove { key }
689                | MapPatchOp::Replace { key, .. } => {
690                    if saw_clear {
691                        return Err(ViewPatchError::InvalidPatchShape {
692                            expected: "Clear must be the only operation in a map patch batch",
693                            actual: "Clear combined with key operation",
694                        });
695                    }
696                    if !touched.insert(key.clone()) {
697                        return Err(ViewPatchError::InvalidPatchShape {
698                            expected: "unique key operations per map patch batch",
699                            actual: "duplicate key operation",
700                        });
701                    }
702                }
703            }
704        }
705        if saw_clear {
706            self.clear();
707            return Ok(());
708        }
709
710        // Phase 3: apply deterministic map operations.
711        for op in ops {
712            match op {
713                MapPatchOp::Insert { key, value } => match self.entry(key) {
714                    BTreeMapEntry::Occupied(mut slot) => {
715                        slot.get_mut()
716                            .merge(value)
717                            .map_err(|err| err.with_field("insert").with_field("value"))?;
718                    }
719                    BTreeMapEntry::Vacant(slot) => {
720                        let mut value_value = V::default();
721                        value_value
722                            .merge(value)
723                            .map_err(|err| err.with_field("insert").with_field("value"))?;
724                        slot.insert(value_value);
725                    }
726                },
727                MapPatchOp::Remove { key } => {
728                    if self.remove(&key).is_none() {
729                        return Err(ViewPatchError::MissingKey {
730                            operation: "remove",
731                        });
732                    }
733                }
734                MapPatchOp::Replace { key, value } => match self.entry(key) {
735                    BTreeMapEntry::Occupied(mut slot) => {
736                        slot.get_mut()
737                            .merge(value)
738                            .map_err(|err| err.with_field("replace").with_field("value"))?;
739                    }
740                    BTreeMapEntry::Vacant(_) => {
741                        return Err(ViewPatchError::MissingKey {
742                            operation: "replace",
743                        });
744                    }
745                },
746                MapPatchOp::Clear => {
747                    return Err(ViewPatchError::InvalidPatchShape {
748                        expected: "Clear to be handled before apply phase",
749                        actual: "Clear reached apply phase",
750                    });
751                }
752            }
753        }
754
755        Ok(())
756    }
757}
758
759macro_rules! impl_update_view {
760    ($($type:ty),*) => {
761        $(
762            impl UpdateView for $type {
763                type UpdateViewType = Self;
764
765                fn merge(
766                    &mut self,
767                    update: Self::UpdateViewType,
768                ) -> Result<(), ViewPatchError> {
769                    *self = update;
770
771                    Ok(())
772                }
773            }
774        )*
775    };
776}
777
778impl_update_view!(bool, i8, i16, i32, i64, u8, u16, u32, u64, String);