dittolive_ditto/dql/differ.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
use std::{collections::HashSet, fmt};
use ffi_sdk::{self, ffi_utils::repr_c};
use crate::dql::QueryResultItem;
/// Describes the move of an item in a [`Diff`].
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DiffMove {
/// The index in the list of "old items" that the item has moved from.
pub from: usize,
/// The index in the list of "new items" that the item has moved to.
pub to: usize,
}
impl From<(usize, usize)> for DiffMove {
fn from((from, to): (usize, usize)) -> Self {
Self { from, to }
}
}
/// Represents a diff between two arrays.
///
/// Create a diff between arrays of [`QueryResultItem`]s using a [`Differ`].
///
/// Alternatively, you can directly create a [`Diff`] via [`Diff::empty`]:
/// ```rust
/// # use dittolive_ditto::dql::Diff;
/// let mut diff = Diff::empty();
/// diff.insertions.extend([1, 2, 3]);
/// ```
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[non_exhaustive]
pub struct Diff {
/// The set of indexes in the _new_ array at which new items have been inserted.
pub insertions: HashSet<usize>,
/// The set of indexes in the _old_ array at which old items have been deleted.
pub deletions: HashSet<usize>,
/// The set of indexes in the _new_ array at which items have been updated.
pub updates: HashSet<usize>,
/// A set of tuples each representing a move of an item `from` a particular
/// index in the _old_ array `to` a particular index in the _new_ array.
pub moves: HashSet<DiffMove>,
}
impl Diff {
/// Create a new empty [Diff] instance.
///
/// This is equivalent to calling `Diff::default()`.
pub fn empty() -> Self {
Self::default()
}
}
/// Calculates diffs between arrays of [`QueryResultItem`]s.
///
/// Use a [`Differ`] with a
/// [`StoreObserver`](crate::store::StoreObserver) to get the diff
/// between subsequent query results delivered by the store observer.
pub struct Differ {
raw: repr_c::Box<ffi_sdk::FfiDiffer>,
}
impl Differ {
/// Create a new [`Differ`] instance.
#[inline]
pub fn new() -> Self {
Self {
raw: ffi_sdk::dittoffi_differ_new(),
}
}
/// Calculate the diff of the provided items against the last set of items
/// that were passed to this differ.
///
/// The returned [`Diff`] identifies changes from the old array of items
/// to the new array of items using indices into both arrays.
///
/// Initially, the differ has no items, so the first call to this method
/// will always return a diff showing all items as insertions.
///
/// The identity of items is determined by their `_id` field.
///
/// - `items` The query result items to compare against the last array of items.
pub fn diff(&self, items: impl IntoIterator<Item = QueryResultItem>) -> Diff {
let raw_items: Vec<_> = items.into_iter().map(|item| item.raw.clone()).collect();
let diff_cbor = ffi_sdk::dittoffi_differ_diff(&self.raw, raw_items.as_slice().into());
let deserialized: serialization::SerializedDiff =
::serde_cbor::from_slice(&diff_cbor.0).unwrap();
Diff {
insertions: deserialized.insertions.into_iter().collect(),
deletions: deserialized.deletions.into_iter().collect(),
updates: deserialized.updates.into_iter().collect(),
moves: deserialized
.moves
.into_iter()
.map(|m| DiffMove::from((m.from, m.to)))
.collect(),
}
}
}
impl fmt::Debug for Differ {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Differ").finish()
}
}
impl Default for Differ {
#[inline]
fn default() -> Self {
Self::new()
}
}
/// Private module for serialization and deserialization of [Diff]
mod serialization {
/// Model for serialized CBOR data for a [Diff].
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(super) struct SerializedDiff {
pub(super) insertions: Vec<usize>,
pub(super) deletions: Vec<usize>,
pub(super) updates: Vec<usize>,
pub(super) moves: Vec<SerializedDiffMove>,
}
/// Model for serialized CBOR data for a [DiffMove] element of a [Diff].
#[derive(Debug, PartialEq, Eq)]
pub(super) struct SerializedDiffMove {
pub(super) from: usize,
pub(super) to: usize,
}
impl From<(usize, usize)> for SerializedDiffMove {
fn from((from, to): (usize, usize)) -> Self {
Self { from, to }
}
}
// A DiffMove gets deserialized from a 2-element array where the first element is the "from"
// index and the second element is the "to" index.
impl<'de> serde::Deserialize<'de> for SerializedDiffMove {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let arr: Vec<usize> = serde::Deserialize::deserialize(deserializer)?;
if arr.len() != 2 {
return Err(serde::de::Error::custom(
"DiffMove must be a 2-element array",
));
}
Ok(SerializedDiffMove {
from: arr[0],
to: arr[1],
})
}
}
impl serde::Serialize for SerializedDiffMove {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeSeq;
let mut state = serializer.serialize_seq(Some(2))?;
state.serialize_element(&self.from)?;
state.serialize_element(&self.to)?;
state.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diff_serialization_and_deserialization_works() {
let diff = SerializedDiff {
insertions: [0, 1, 2].into(),
deletions: [3, 4].into(),
updates: [5].into(),
moves: vec![(6, 7), (8, 9)].into_iter().map(Into::into).collect(),
};
let cbor_data = serde_cbor::to_vec(&diff).unwrap();
let deserialized_diff: SerializedDiff = serde_cbor::from_slice(&cbor_data).unwrap();
assert_eq!(diff.insertions, deserialized_diff.insertions);
assert_eq!(diff.deletions, deserialized_diff.deletions);
assert_eq!(diff.updates, deserialized_diff.updates);
assert_eq!(diff.moves, deserialized_diff.moves);
}
}
}