From 7755b39fe5ee87bc9ced38821f221250a56b88ba Mon Sep 17 00:00:00 2001
From: Kubat <mael.martin31@gmail.com>
Date: Thu, 26 Jan 2023 18:35:35 +0100
Subject: [PATCH] AMALIB: Add base things for the cache on client-side

---
 src/rust/Cargo.lock                           |   2 +
 src/rust/Cargo.toml                           |   1 +
 src/rust/amalib/Cargo.toml                    |   1 +
 src/rust/amalib/src/{amacache.rs => cache.rs} |  87 ++++++-------
 src/rust/amalib/src/db_objects.rs             | 114 ++++++++++++++++++
 src/rust/amalib/src/lib.rs                    |   8 +-
 src/rust/amalib/src/tags.rs                   |  89 ++++++++++++++
 7 files changed, 252 insertions(+), 50 deletions(-)
 rename src/rust/amalib/src/{amacache.rs => cache.rs} (55%)
 create mode 100644 src/rust/amalib/src/db_objects.rs
 create mode 100644 src/rust/amalib/src/tags.rs

diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index 9b67a7a8..93fb1fba 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -30,6 +30,7 @@ version = "0.1.0"
 dependencies = [
  "commons",
  "getset",
+ "hashbrown 0.13.2",
  "lru",
  "serde",
  "smallstring",
@@ -315,6 +316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
 dependencies = [
  "ahash",
+ "serde",
 ]
 
 [[package]]
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index 79db8e61..dfe7552c 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -32,6 +32,7 @@ lazy_static = "^1"
 thiserror = "^1"
 
 smallvec = { version = "^1", default-features = false, features = ["serde"] }
+hashbrown = { version = "^0.13", features = ["serde"] }
 
 toml = { version = "^0.5", features = ["preserve_order"] }
 serde = { version = "^1", default-features = false, features = [
diff --git a/src/rust/amalib/Cargo.toml b/src/rust/amalib/Cargo.toml
index 3c68c021..bbd3b6c1 100644
--- a/src/rust/amalib/Cargo.toml
+++ b/src/rust/amalib/Cargo.toml
@@ -8,6 +8,7 @@ license.workspace = true
 [dependencies]
 serde.workspace = true
 tokio.workspace = true
+hashbrown.workspace = true
 
 smallstring = { path = "../smallstring" }
 commons = { path = "../commons" }
diff --git a/src/rust/amalib/src/amacache.rs b/src/rust/amalib/src/cache.rs
similarity index 55%
rename from src/rust/amalib/src/amacache.rs
rename to src/rust/amalib/src/cache.rs
index 1b1ad27f..f86ec433 100644
--- a/src/rust/amalib/src/amacache.rs
+++ b/src/rust/amalib/src/cache.rs
@@ -1,45 +1,9 @@
+use crate::*;
+use hashbrown::HashMap;
 use lru::LruCache;
 use serde::{Deserialize, Serialize};
 use smallstring::SmallString;
-use std::{collections::HashMap, num::NonZeroUsize};
-
-/// Represent a global id for a kara, the pair (database, id).
-#[derive(Debug, Deserialize, Serialize)]
-pub struct KaraId {
-    pub repo: SmallString,
-    pub id: i64,
-}
-
-/// Represent what we want to override in a kara.
-#[derive(Debug, Deserialize, Serialize)]
-pub struct KaraOverride {}
-
-/// A list of informations about a kara. This is the data from lektor. When
-/// queried we patch it with the overrides given by the user.
-#[derive(Debug)]
-pub struct KaraInfo {}
-
-#[allow(dead_code)]
-#[derive(Debug)]
-pub struct Playlist {
-    name: SmallString,
-    ctime: i64,
-    creator: SmallString,
-    content: Vec<i64>,
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(transparent)]
-pub struct AmaDB {
-    /// Contains the content of the queue.
-    #[serde(skip)]
-    queue: Vec<i64>,
-
-    /// We cache the database so we don't have to make requests every time we
-    /// refresh the queue or a playlist.
-    databases: AmaDBCache,
-}
+use std::num::NonZeroUsize;
 
 /// We cache objects locally so we don't have to always make requests to
 /// lektord. When we get the update stored database event from mpv we clear the
@@ -47,14 +11,13 @@ pub struct AmaDB {
 /// it's the database, we clear everything.
 #[allow(dead_code)]
 #[derive(Debug, Deserialize, Serialize)]
-struct AmaDBCache {
-    /// Cache requests to lektord.
-    #[serde(skip, default = "new_empty_kara_lru")]
-    lru_lektor: LruCache<i64, KaraInfo>,
-
-    /// Cache requests to lektord that where patched.
+pub struct AmaDB {
+    /// Cache requests to lektord and patched karas. The first element is the
+    /// lektord entry, the second one is the patched version.
+    /// TODO: Find a structure that does COW to not duplicate too much
+    ///       information in the patched version...
     #[serde(skip, default = "new_empty_kara_lru")]
-    lru_patched: LruCache<i64, KaraInfo>,
+    karas: LruCache<i64, (KaraInfo, Option<KaraInfo>)>,
 
     /// The playlists. We don't save them...
     #[serde(skip, default = "new_empty_playlist_lru")]
@@ -63,7 +26,7 @@ struct AmaDBCache {
     /// The list of overrides, indexed by the local id of the kara.
     overrides: HashMap<i64, KaraOverride>,
 
-    /// The conversion list for local id <-> repo id. Overrides are declared
+    /// The conversion list for `local id <-> repo id`. Overrides are declared
     /// with the repo ids because it's the only thing that every client have in
     /// common. We must adapt to the local id.
     local_ids: HashMap<i64, KaraId>,
@@ -71,7 +34,7 @@ struct AmaDBCache {
 
 /// Create a new empty LRU cache. We set the value to the maximal count of
 /// visible karas multiplied by a threeshold.
-fn new_empty_kara_lru() -> LruCache<i64, KaraInfo> {
+fn new_empty_kara_lru() -> LruCache<i64, (KaraInfo, Option<KaraInfo>)> {
     use crate::constants::*;
     LruCache::new(
         NonZeroUsize::new(
@@ -108,4 +71,32 @@ impl AmaDB {
     pub fn clear_playlists_cache(&mut self) {
         todo!()
     }
+
+    /// Add an override to the override list. If a previous override is present
+    /// for the specified ID, we merge the new override into the previous one.
+    /// If the kara is present in the cache, we generate the patched version.
+    pub fn add_kara_override(&mut self, _id: KaraId, _override: KaraOverride) {
+        todo!()
+    }
+
+    /// Add a new kara into the lektord cache. If a patch is present for the
+    /// kara, it's patched and inserted into the patched cache.
+    pub fn add_kara(&mut self, _id: i64, _kara: KaraInfo) {
+        todo!()
+    }
+
+    /// Get the [KaraInfo] correspondig to the id. If the kara was patched with
+    /// a [KaraOverride], we return the patched version, overwise we return the
+    /// original version. If the [KaraInfo] was not found then we return [None].
+    pub fn kara(&mut self, id: i64) -> Option<&KaraInfo> {
+        match self.karas.get(&id)? {
+            (_, Some(info)) | (info, None) => Some(info),
+        }
+    }
+
+    /// Get the [Playlist] correspondif to the name. If the [Playlist] was not
+    /// found, we return [None].
+    pub fn playlist<'a>(&'a mut self, name: &SmallString) -> Option<&'a Playlist> {
+        self.playlists.get(name)
+    }
 }
diff --git a/src/rust/amalib/src/db_objects.rs b/src/rust/amalib/src/db_objects.rs
new file mode 100644
index 00000000..2667a256
--- /dev/null
+++ b/src/rust/amalib/src/db_objects.rs
@@ -0,0 +1,114 @@
+use crate::tags::*;
+use getset::{CopyGetters, Getters};
+use serde::{Deserialize, Serialize};
+use smallstring::SmallString;
+
+/// Represent a global id for a kara, the pair (database, id).
+#[derive(Debug, Deserialize, Serialize, Getters, CopyGetters)]
+pub struct KaraId {
+    #[getset(get = "pub")]
+    repo: SmallString,
+
+    #[getset(get_copy = "pub")]
+    id: i64,
+}
+
+/// Represent what we want to override in a kara.
+#[derive(Debug, Deserialize, Serialize)]
+pub struct KaraOverride {}
+
+/// A list of informations about a kara. This is the data from lektor. When
+/// queried we patch it with the overrides given by the user.
+#[derive(Debug, Getters)]
+#[getset(get = "pub")]
+pub struct KaraInfo {
+    song_title: SmallString,
+    song_source: SmallString,
+    song_type: SmallString,
+    song_origin: SmallString,
+    languages: Vec<SmallString>,
+    kara_makers: Vec<SmallString>,
+
+    #[getset(skip)]
+    tags: SimpleTagSystem,
+}
+
+#[allow(dead_code)]
+#[derive(Debug, Getters)]
+#[getset(get = "pub")]
+pub struct Playlist {
+    name: SmallString,
+    ctime: i64,
+    creator: SmallString,
+    content: Vec<i64>,
+
+    #[getset(skip)]
+    tags: SimpleTagSystem,
+}
+
+impl From<(&str, i64)> for KaraId {
+    fn from((repo, id): (&str, i64)) -> Self {
+        let repo = repo.into();
+        Self { repo, id }
+    }
+}
+
+impl From<(SmallString, i64)> for KaraId {
+    fn from((repo, id): (SmallString, i64)) -> Self {
+        Self { repo, id }
+    }
+}
+
+impl<'a> TaggedObject<'a> for Playlist {
+    type TagKeyType = <SimpleTagSystem as TaggedObject<'a>>::TagKeyType;
+    type TagValueType = <SimpleTagSystem as TaggedObject<'a>>::TagValueType;
+    type TagKeyIterator = <SimpleTagSystem as TaggedObject<'a>>::TagKeyIterator;
+    type TagValueIterator = <SimpleTagSystem as TaggedObject<'a>>::TagValueIterator;
+
+    fn add_tag(&mut self, tag: Self::TagKeyType, value: Option<Self::TagValueType>) {
+        self.tags.add_tag(tag, value)
+    }
+
+    fn delete_tag<S: AsRef<str>>(&mut self, tag: S) {
+        self.tags.delete_tag(tag)
+    }
+
+    fn clear_tags(&mut self) {
+        self.tags.clear()
+    }
+
+    fn tags(&'a self) -> Self::TagKeyIterator {
+        self.tags.tags()
+    }
+
+    fn iter_tag<S: AsRef<str>>(&'a self, tag: S) -> Option<Self::TagValueIterator> {
+        self.tags.iter_tag(tag)
+    }
+}
+
+impl<'a> TaggedObject<'a> for KaraInfo {
+    type TagKeyType = <SimpleTagSystem as TaggedObject<'a>>::TagKeyType;
+    type TagValueType = <SimpleTagSystem as TaggedObject<'a>>::TagValueType;
+    type TagKeyIterator = <SimpleTagSystem as TaggedObject<'a>>::TagKeyIterator;
+    type TagValueIterator = <SimpleTagSystem as TaggedObject<'a>>::TagValueIterator;
+
+    fn add_tag(&mut self, tag: Self::TagKeyType, value: Option<Self::TagValueType>) {
+        self.tags.add_tag(tag, value)
+    }
+
+    fn delete_tag<S: AsRef<str>>(&mut self, tag: S) {
+        self.tags.delete_tag(tag)
+    }
+
+    fn clear_tags(&mut self) {
+        self.tags.clear()
+    }
+
+    fn tags(&'a self) -> Self::TagKeyIterator {
+        self.tags.tags()
+    }
+
+    fn iter_tag<S: AsRef<str>>(&'a self, tag: S) -> Option<Self::TagValueIterator> {
+        self.tags.iter_tag(tag)
+    }
+}
diff --git a/src/rust/amalib/src/lib.rs b/src/rust/amalib/src/lib.rs
index 476fc5b1..87ac4a9b 100644
--- a/src/rust/amalib/src/lib.rs
+++ b/src/rust/amalib/src/lib.rs
@@ -4,17 +4,21 @@
 //! communicate with the lektord server and elements to store and organise the
 //! queried informations.
 
-mod amacache;
+mod cache;
 mod connexion;
 mod constants;
+mod db_objects;
 mod query;
 mod response;
+mod tags;
 mod uri;
 
-pub use amacache::*;
+pub use cache::*;
 pub use connexion::*;
+pub use db_objects::*;
 pub use query::*;
 pub use response::*;
+pub use tags::*;
 pub use uri::*;
 
 pub(crate) use commons::{log::*, *};
diff --git a/src/rust/amalib/src/tags.rs b/src/rust/amalib/src/tags.rs
new file mode 100644
index 00000000..0d003575
--- /dev/null
+++ b/src/rust/amalib/src/tags.rs
@@ -0,0 +1,89 @@
+use hashbrown::HashMap;
+use smallstring::SmallString;
+
+/// Represent an object that can be tagged, e.g. a playlist or a kara. We
+/// describe all the operations on such object here.
+pub trait TaggedObject<'a> {
+    /// The type for the keys, e.g. the tag names.
+    type TagKeyType: AsRef<str> + 'a;
+
+    /// The type for the values of the tags. We enforce a string representation
+    /// for such tag.
+    type TagValueType: AsRef<str> + 'a;
+
+    /// The type of iterator for the list of keys.
+    type TagKeyIterator: Iterator<Item = &'a Self::TagKeyType>;
+
+    /// The type of iterator for the list the values of a tag.
+    type TagValueIterator: Iterator<Item = &'a Self::TagValueType>;
+
+    /// Add a tag to the object.
+    fn add_tag(&mut self, tag: Self::TagKeyType, value: Option<Self::TagValueType>);
+
+    /// Delete all values for a tag for the object.
+    fn delete_tag<S: AsRef<str>>(&mut self, tag: S);
+
+    /// Remove all tags from the object.
+    fn clear_tags(&mut self);
+
+    /// Get all the tags present on a kara.
+    fn tags(&'a self) -> Self::TagKeyIterator;
+
+    /// Iterate over the values of a tag.
+    fn iter_tag<S: AsRef<str>>(&'a self, tag: S) -> Option<Self::TagValueIterator>;
+
+    /// Has a tag with the specified name.
+    fn has_tag<S: AsRef<str>>(&'a self, tag: S) -> bool {
+        self.tags().any(|t| t.as_ref() == tag.as_ref())
+    }
+
+    /// Contains the (tag, value) pair.
+    fn has_tag_value<S: AsRef<str>>(&'a self, tag: S, value: S) -> bool {
+        self.iter_tag(tag)
+            .map(|mut values| values.any(|v| v.as_ref() == value.as_ref()))
+            .unwrap_or_default()
+    }
+}
+
+/// A simple tag system. Any object can reuse this type or implement the
+/// [TaggedObject] trait itself. To reuse this type, you can forward calls to
+/// the simple tag field when implementing the trait.
+pub(crate) type SimpleTagSystem = HashMap<SmallString, Vec<SmallString>>;
+
+impl<'a> TaggedObject<'a> for SimpleTagSystem {
+    type TagKeyType = SmallString;
+    type TagValueType = SmallString;
+    type TagKeyIterator = hashbrown::hash_map::Keys<'a, SmallString, Vec<SmallString>>;
+    type TagValueIterator = std::slice::Iter<'a, SmallString>;
+
+    fn tags(&'a self) -> Self::TagKeyIterator {
+        self.keys()
+    }
+
+    fn iter_tag<S: AsRef<str>>(&'a self, tag: S) -> Option<Self::TagValueIterator> {
+        self.get(tag.as_ref()).map(|tags| tags.iter())
+    }
+
+    fn add_tag(&mut self, tag: SmallString, value: Option<SmallString>) {
+        match self.get_mut(&tag) {
+            Some(values) => values.extend(value),
+            None => {
+                self.insert(tag, value.into_iter().collect());
+            }
+        }
+    }
+
+    fn delete_tag<S: AsRef<str>>(&mut self, tag: S) {
+        if let Some(values) = self.get_mut(tag.as_ref()) {
+            values.clear()
+        }
+    }
+
+    fn clear_tags(&mut self) {
+        self.clear()
+    }
+
+    fn has_tag<S: AsRef<str>>(&'a self, tag: S) -> bool {
+        self.contains_key(tag.as_ref())
+    }
+}
-- 
GitLab