From 920cc72c9f9c9304e0629cd267b50def1c8dd9f9 Mon Sep 17 00:00:00 2001
From: Elliu <elliu@hashi.re>
Date: Tue, 27 May 2025 10:25:41 +0200
Subject: [PATCH] UTILS: dkmap: allow more shapes of data to be stored

Use a IntoDKMap trait to provide the interface between the stored data and
the key management
---
 lektor_nkdb/src/database/pool.rs   |  97 ++++++++++++------------
 lektor_nkdb/src/database/update.rs |   2 +-
 lektor_repo/src/lib.rs             |   2 +
 lektor_utils/src/dkmap.rs          | 116 ++++++++++++++++++++---------
 lektor_utils/tests/dkmap.rs        |  35 +++++----
 5 files changed, 157 insertions(+), 95 deletions(-)

diff --git a/lektor_nkdb/src/database/pool.rs b/lektor_nkdb/src/database/pool.rs
index 75de9409..51b5534a 100644
--- a/lektor_nkdb/src/database/pool.rs
+++ b/lektor_nkdb/src/database/pool.rs
@@ -5,23 +5,19 @@
 //! specific things like the [u64] to [Kid] / [Kara] mappings, do that in the epoch struct.
 
 use crate::{kara::status::KaraState, strings, EpochData, KId, Kara, KaraStatus, RemoteKId};
-use anyhow::{Context as _, Result};
+use anyhow::{anyhow, Context as _, Result};
 use futures::prelude::*;
-use lektor_utils::dkmap::DKMap;
+use lektor_utils::dkmap::{DKMap, IntoDKMapping};
 use serde::{Deserialize, Serialize};
 
-/// ID mapping from local to remote. Note that there is a one-to-one relation between
-/// [RemoteKId] and [KId], even with metadata updates or file update.
-// State of the karas: virtual or not, if physical: filesize, filehash,...
+/// ID mapping from local to remote.
+/// Note that there is a one-to-many relation between from [KId] to [RemoteKId]
+/// State of the karas: virtual or not, if physical: filesize, filehash,...
 type KaraMappingValue = (KId, RemoteKId, KaraState);
 type KaraMapping = DKMap<KId, RemoteKId, KaraMappingValue>;
 
-pub trait IntoMapping: Sized {
-    fn into_mapping(self) -> KaraMappingValue;
-}
-
-impl IntoMapping for &'_ Kara {
-    fn into_mapping(self) -> KaraMappingValue {
+impl IntoDKMapping<KId, RemoteKId, KaraState> for &'_ Kara {
+    fn into_keys_value(&self) -> KaraMappingValue {
         (
             self.id,
             self.remote.clone(),
@@ -33,41 +29,26 @@ impl IntoMapping for &'_ Kara {
             },
         )
     }
-}
-
-impl IntoMapping for (KId, RemoteKId, KaraState) {
-    fn into_mapping(self) -> KaraMappingValue {
-        self
-    }
-}
-
-impl IntoMapping for (RemoteKId, KaraState, KId) {
-    fn into_mapping(self) -> KaraMappingValue {
-        (self.2, self.0, self.1)
-    }
-}
 
-impl IntoMapping for (KaraState, KId, RemoteKId) {
-    fn into_mapping(self) -> KaraMappingValue {
-        (self.1, self.2, self.0)
+    fn into_keys(&self) -> (KId, RemoteKId) {
+        (self.id, self.remote.clone())
     }
-}
 
-impl IntoMapping for (KId, KaraState, RemoteKId) {
-    fn into_mapping(self) -> KaraMappingValue {
-        (self.0, self.2, self.1)
+    fn into_primary_key(&self) -> KId {
+        self.id
     }
-}
 
-impl IntoMapping for (RemoteKId, KId, KaraState) {
-    fn into_mapping(self) -> KaraMappingValue {
-        (self.1, self.0, self.2)
+    fn into_secondary_key(&self) -> RemoteKId {
+        self.remote.clone()
     }
-}
 
-impl IntoMapping for (KaraState, RemoteKId, KId) {
-    fn into_mapping(self) -> KaraMappingValue {
-        (self.2, self.1, self.0)
+    fn into_value(&self) -> KaraState {
+        match self.kara_status {
+            KaraStatus::Virtual => KaraState::Virtual,
+            // Don't try to discriminate PhysicalUnavailable and PhysicalAvailable here because
+            // it might take some time to resolve
+            _ => KaraState::PhysicalUnavailable,
+        }
     }
 }
 
@@ -118,13 +99,13 @@ impl Default for Pool {
     }
 }
 
-impl<M: IntoMapping> FromIterator<M> for Pool {
+impl<M: IntoDKMapping<KId, RemoteKId, KaraMappingValue>> FromIterator<M> for Pool {
     fn from_iter<T: IntoIterator<Item = M>>(iter: T) -> Self {
         let (mappings, ids): (Vec<KaraMappingValue>, Vec<u64>) = iter
             .into_iter()
             .map(|kara| {
-                let (KId(id), rkid, remaining_values) = kara.into_mapping();
-                ((KId(id), rkid, remaining_values), id)
+                let (KId(id), rkid, state) = kara.into_value();
+                ((KId(id), rkid, state), id)
             })
             .unzip();
 
@@ -149,17 +130,39 @@ impl Pool {
         KId(ret)
     }
 
-    pub(crate) fn add_mapping(&mut self, mapping: impl IntoMapping) {
-        self.kara_mapping.insert_or_update(mapping.into_mapping());
+    pub(crate) fn add_mapping(
+        &mut self,
+        mapping: impl IntoDKMapping<KId, RemoteKId, KaraMappingValue>,
+    ) {
+        self.kara_mapping.insert_or_update(mapping.into_value());
     }
 
-    pub(crate) fn update_state_from_kid(&mut self, id: KId, state: KaraState) -> Result<()> {
+    pub(crate) fn update_mapping(&mut self, mapping: KaraMappingValue) -> Result<()> {
         self.kara_mapping
-            .update(&id, state)
-            .with_context(|| format!("failed to set state for kara {id}"))?;
+            .update(mapping.into_value())
+            .with_context(|| {
+                format!(
+                    "failed to set state for kara {}",
+                    mapping.into_primary_key()
+                )
+            })?;
         Ok(())
     }
 
+    // FIXME: delete?
+    pub(crate) fn update_state_from_kid(&mut self, id: KId, state: KaraState) -> Result<()> {
+        if let Some(mapping) = self.kara_mapping.get_k1(&id) {
+            let mut new = mapping.into_value();
+            new.2 = state;
+            self.kara_mapping
+                .update(new)
+                .with_context(|| format!("failed to set state for kara {id}"))?;
+            Ok(())
+        } else {
+            Err(anyhow!("update_state_from_kid with unknown kid"))
+        }
+    }
+
     pub(crate) fn get_state(&self, kid: KId) -> Option<KaraState> {
         Some(self.kara_mapping.get_k1(&kid)?.2)
     }
diff --git a/lektor_nkdb/src/database/update.rs b/lektor_nkdb/src/database/update.rs
index 69a88ca7..43325f96 100644
--- a/lektor_nkdb/src/database/update.rs
+++ b/lektor_nkdb/src/database/update.rs
@@ -163,7 +163,7 @@ impl<'a, Storage: DatabaseStorage> UpdateHandler<'a, Storage> {
             self.pool
                 .write()
                 .await
-                .add_mapping((rkid.clone(), kid, state));
+                .add_mapping((kid, rkid.clone(), state));
 
             Some((kid, rkid))
         };
diff --git a/lektor_repo/src/lib.rs b/lektor_repo/src/lib.rs
index d4a7f2ce..8adc3a30 100644
--- a/lektor_repo/src/lib.rs
+++ b/lektor_repo/src/lib.rs
@@ -189,6 +189,8 @@ impl Repo {
                     {
                         log::error!("error downloading karaoke {kid}: {err}")
                     } else {
+                        // FIXME: here, also pass rkid, to use the cheaper
+                        // Pool.update_mapping instead of Pool.update_state_from_kid
                         return handler
                             .set_physical_available(kid, hash, filesize)
                             .await
diff --git a/lektor_utils/src/dkmap.rs b/lektor_utils/src/dkmap.rs
index 0397892e..d2a9bb87 100644
--- a/lektor_utils/src/dkmap.rs
+++ b/lektor_utils/src/dkmap.rs
@@ -2,25 +2,68 @@ use anyhow::{anyhow, Context as _, Result};
 use serde::{Deserialize, Deserializer, Serialize};
 use std::{cmp::Eq, collections::HashMap, hash::Hash, mem};
 
+// Use a custom trait and not just Into<(K1, K2)>, to allow for conversion copying only needed
+// values, and not all [Value]
+pub trait IntoDKMapping<K1, K2, Value>: Sized {
+    fn into_keys_value(&self) -> (K1, K2, Value);
+    fn into_keys(&self) -> (K1, K2);
+    fn into_primary_key(&self) -> K1;
+    fn into_secondary_key(&self) -> K2;
+    fn into_value(&self) -> Value;
+}
+
+// Let's be nice and provide a built-in implementation for simple tuples
+impl<K1, K2, Value> IntoDKMapping<K1, K2, (K1, K2, Value)> for (K1, K2, Value)
+where
+    K1: Hash + Eq + Copy,
+    K2: Hash + Eq + Clone,
+    Value: Clone,
+{
+    fn into_keys_value(&self) -> (K1, K2, (K1, K2, Value)) {
+        (
+            self.0,
+            self.1.clone(),
+            (self.0, self.1.clone(), self.2.clone()),
+        )
+    }
+
+    fn into_keys(&self) -> (K1, K2) {
+        (self.0, self.1.clone())
+    }
+
+    fn into_primary_key(&self) -> K1 {
+        self.0
+    }
+
+    fn into_secondary_key(&self) -> K2 {
+        self.1.clone()
+    }
+
+    fn into_value(&self) -> (K1, K2, Value) {
+        (self.0, self.1.clone(), self.2.clone())
+    }
+}
+
 #[derive(Debug, Serialize, Clone, Default)]
-pub struct DKMap<K1: Hash + Eq, K2: Hash + Eq, Values> {
+pub struct DKMap<K1: Hash + Eq, K2: Hash + Eq, Value> {
     #[serde(skip)]
     primary_key_mapping: HashMap<K1, usize>,
     #[serde(skip)]
     secondary_key_mapping: HashMap<K2, Vec<usize>>,
 
-    values: Vec<Values>,
+    values: Vec<Value>,
 }
 
 #[derive(Debug, Deserialize, Clone)]
-struct DKMapDeserializeProxy<Values> {
-    values: Vec<Values>,
+struct DKMapDeserializeProxy<Value> {
+    values: Vec<Value>,
 }
 
-impl<K1, K2, Values> DKMap<K1, K2, (K1, K2, Values)>
+impl<K1, K2, Value> DKMap<K1, K2, Value>
 where
     K1: Hash + Eq + Copy,
     K2: Hash + Eq + Clone,
+    Value: IntoDKMapping<K1, K2, Value>,
 {
     pub fn with_capacity(capacity: usize) -> Self {
         Self {
@@ -30,13 +73,13 @@ where
         }
     }
 
-    pub fn get_k1(&self, key: &K1) -> Option<&(K1, K2, Values)> {
+    pub fn get_k1(&self, key: &K1) -> Option<&Value> {
         self.primary_key_mapping
             .get(key)
             .and_then(|index| self.values.get(*index))
     }
 
-    pub fn get_k2(&self, key: &K2) -> Option<Vec<&(K1, K2, Values)>> {
+    pub fn get_k2(&self, key: &K2) -> Option<Vec<&Value>> {
         self.secondary_key_mapping.get(key).map(|indexes| {
             let mut ret = vec![];
             for index in indexes {
@@ -46,29 +89,34 @@ where
         })
     }
 
-    pub fn update(&mut self, key: &K1, values: Values) -> Result<(K1, K2, Values)> {
+    pub fn update(&mut self, value: Value) -> Result<Value> {
         self.primary_key_mapping
-            .get(key)
+            .get(&value.into_primary_key())
             .map(|index| {
-                let old_values = mem::replace(&mut self.values[*index].2, values);
-                let (k1, k2, _) = &self.values[*index];
-                (*k1, k2.clone(), old_values)
+                if value.into_keys() != self.values[*index].into_keys() {
+                    None
+                } else {
+                    Some(mem::replace(&mut self.values[*index], value.into_value()))
+                }
             })
-            .context("Called DKMap::update() on a non existing key")
+            .context("Called DKMap::update() on a non existing key")?
+            .context("Tried to update with an object that changes primary or secondary key")
     }
 
     /// Insert OR replace the value at value.0 (K1)
     ///
     /// Note that neither K1 nor K2 will be updated for an already existing mapping, ever.
     /// Only the remaining values will be
-    pub fn insert_or_update(&mut self, value: (K1, K2, Values)) -> Option<(K1, K2, Values)> {
-        let (k1, k2) = (value.0, value.1.clone());
+    pub fn insert_or_update(&mut self, value: Value) -> Option<Value> {
+        let (k1, k2) = value.into_keys();
         match self.primary_key_mapping.get(&k1) {
-            Some(index) => Some((
-                k1,
-                self.values[*index].1.clone(),
-                mem::replace(&mut self.values[*index].2, value.2),
-            )),
+            Some(index) => {
+                if value.into_keys() != self.values[*index].into_keys() {
+                    None
+                } else {
+                    Some(mem::replace(&mut self.values[*index], value.into_value()))
+                }
+            }
             None => {
                 self.values.push(value);
                 let index = self.values.len() - 1;
@@ -83,8 +131,8 @@ where
     }
 
     /// Insert only. If the key K1 is already present, returns an Error
-    pub fn insert_only(&mut self, value: (K1, K2, Values)) -> Result<()> {
-        let (k1, k2) = (value.0, value.1.clone());
+    pub fn insert_only(&mut self, value: Value) -> Result<()> {
+        let (k1, k2) = value.into_keys();
         match self.primary_key_mapping.get(&k1) {
             Some(_) => Err(anyhow!("K1 already present in the DKMap")),
             None => {
@@ -100,35 +148,35 @@ where
         }
     }
 
-    pub fn iter(&self) -> impl Iterator<Item = &(K1, K2, Values)> + '_ {
+    pub fn iter(&self) -> impl Iterator<Item = &Value> + '_ {
         self.values.iter()
     }
 }
 
-impl<K1, K2, Values> FromIterator<(K1, K2, Values)> for DKMap<K1, K2, (K1, K2, Values)>
+impl<K1, K2, Value> FromIterator<Value> for DKMap<K1, K2, Value>
 where
     K1: Hash + Eq + Copy,
     K2: Hash + Eq + Clone,
-    Values: Copy,
+    Value: IntoDKMapping<K1, K2, Value> + Clone,
 {
-    fn from_iter<T: IntoIterator<Item = (K1, K2, Values)>>(iter: T) -> Self {
+    fn from_iter<T: IntoIterator<Item = Value>>(iter: T) -> Self {
         let iter = iter.into_iter();
         let (min, max) = iter.size_hint();
         let init = Self::with_capacity(max.unwrap_or(min));
-        iter.fold(init, |mut ret, (key1, key2, values)| {
-            ret.insert_or_update((key1, key2, values));
+        iter.fold(init, |mut ret, value| {
+            ret.insert_or_update(value);
             ret
         })
     }
 }
 
-impl<K1, K2, Values> IntoIterator for DKMap<K1, K2, (K1, K2, Values)>
+impl<K1, K2, Value> IntoIterator for DKMap<K1, K2, Value>
 where
     K1: Hash + Eq + Copy,
     K2: Hash + Eq + Clone,
-    Values: Copy,
+    Value: IntoDKMapping<K1, K2, Value> + Clone,
 {
-    type Item = (K1, K2, Values);
+    type Item = Value;
     type IntoIter = std::vec::IntoIter<Self::Item>;
 
     fn into_iter(self) -> Self::IntoIter {
@@ -136,17 +184,17 @@ where
     }
 }
 
-impl<'de, K1, K2, Values> Deserialize<'de> for DKMap<K1, K2, (K1, K2, Values)>
+impl<'de, K1, K2, Value> Deserialize<'de> for DKMap<K1, K2, Value>
 where
     K1: Hash + Eq + Copy + Deserialize<'de>,
     K2: Hash + Eq + Clone + Deserialize<'de>,
-    Values: Copy + Deserialize<'de>,
+    Value: IntoDKMapping<K1, K2, Value> + Clone + Deserialize<'de>,
 {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
         D: Deserializer<'de>,
     {
-        let proxy = DKMapDeserializeProxy::<(K1, K2, Values)>::deserialize(deserializer)?;
+        let proxy = DKMapDeserializeProxy::<Value>::deserialize(deserializer)?;
         Ok(DKMap::from_iter(proxy.values))
     }
 }
diff --git a/lektor_utils/tests/dkmap.rs b/lektor_utils/tests/dkmap.rs
index d0aff9be..033e8596 100644
--- a/lektor_utils/tests/dkmap.rs
+++ b/lektor_utils/tests/dkmap.rs
@@ -29,9 +29,14 @@ fn test_from_iter() {
     assert_eq!(map.get_k2(&5), Some(vec![&(4, 5, 6)]));
 
     let map = TestDKMap::from_iter(vec![(1, 2, 3), (1, 5, 6)]);
-    assert_eq!(map.get_k1(&1), Some(&(1, 2, 6)));
-    assert_eq!(map.get_k2(&2), Some(vec![&(1, 2, 6)]));
+    assert_eq!(map.get_k1(&1), Some(&(1, 2, 3)));
+    assert_eq!(map.get_k2(&2), Some(vec![&(1, 2, 3)]));
     assert_eq!(map.get_k2(&5), None);
+
+    let map = TestDKMap::from_iter(vec![(1, 2, 3), (10, 2, 6)]);
+    assert_eq!(map.get_k1(&1), Some(&(1, 2, 3)));
+    assert_eq!(map.get_k1(&10), Some(&(10, 2, 6)));
+    assert_eq!(map.get_k2(&2), Some(vec![&(1, 2, 3), &(10, 2, 6)]));
 }
 
 fn test_functional_empty_dkmap(mut map: TestDKMap) -> Result<()> {
@@ -45,20 +50,20 @@ fn test_functional_empty_dkmap(mut map: TestDKMap) -> Result<()> {
     assert_eq!(map.get_k1(&1), Some(&(1, 2, 3)));
     assert_eq!(map.get_k1(&4), Some(&(4, 5, 6)));
 
-    assert_eq!(map.insert_or_update((4, 7, 8)), Some((4, 5, 6)));
+    assert_eq!(map.insert_or_update((4, 7, 8)), None);
 
     // Make sure that primary key and secondary key are not updated
-    assert_eq!(map.get_k1(&4), Some(&(4, 5, 8)));
+    assert_eq!(map.get_k1(&4), Some(&(4, 5, 6)));
 
     let old_prim = 10;
     let old = (old_prim, 11, 12);
     assert_eq!(map.insert_or_update(old), None);
-    assert_eq!(map.insert_or_update((old_prim, 20, 21)), Some(old));
+    assert_eq!(map.insert_or_update((old_prim, 20, 21)), None);
 
-    assert_ok!(map.update(&1, 5));
-    assert_eq!(map.get_k1(&1), Some(&(1, 2, 5)));
+    assert_err!(map.update((1, 99, 99)));
+    assert_eq!(map.get_k1(&1), Some(&(1, 2, 3)));
 
-    assert_err!(map.update(&100, 5));
+    assert_err!(map.update((0, 0, 0)));
 
     assert_eq!(map.get_k1(&101), None);
     assert_eq!(map.insert_or_update((101, 102, 103)), None);
@@ -88,10 +93,14 @@ fn test_update() {
     let mut map = TestDKMap::from_iter(vec![(1, 2, 3)]);
 
     assert_eq!(map.get_k1(&1), Some(&(1, 2, 3)));
-    assert_ok!(map.update(&1, 5));
+    assert_err!(map.update((99, 99, 99)));
+    assert_eq!(map.get_k1(&1), Some(&(1, 2, 3)));
+
+    let old = map.update((1, 2, 5));
+    assert_eq!(old.unwrap(), (1, 2, 3));
     assert_eq!(map.get_k1(&1), Some(&(1, 2, 5)));
 
-    assert_err!(map.update(&2, 5));
+    assert_err!(map.update((100, 100, 100)));
 }
 
 #[test]
@@ -101,8 +110,8 @@ fn test_get() {
     assert_eq!(map.get_k1(&4), Some(&(4, 2, 6)));
     assert_eq!(map.get_k2(&2), Some(vec![&(1, 2, 3), &(4, 2, 6)]));
 
-    assert_ok!(map.update(&1, 10));
-    assert_eq!(map.get_k2(&2), Some(vec![&(1, 2, 10), &(4, 2, 6)]));
+    assert_err!(map.update((1, 99, 10)));
+    assert_eq!(map.get_k2(&2), Some(vec![&(1, 2, 3), &(4, 2, 6)]));
 }
 
 #[test]
@@ -130,7 +139,7 @@ fn test_into_iter() {
     assert_eq!(map_iter.next(), Some((4, 5, 6)));
 
     let mut map_iter = TestDKMap::from_iter(vec![(1, 2, 3), (1, 5, 6)]).into_iter();
-    assert_eq!(map_iter.next(), Some((1, 2, 6)));
+    assert_eq!(map_iter.next(), Some((1, 2, 3)));
     assert_eq!(map_iter.next(), None);
 }
 
-- 
GitLab