diff --git a/Cargo.lock b/Cargo.lock index 8203337..ff6a59e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.90", ] [[package]] @@ -221,7 +221,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.90", ] [[package]] @@ -236,6 +236,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -259,7 +265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn", + "syn 2.0.90", ] [[package]] @@ -278,6 +284,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_filter" version = "0.1.2" @@ -553,6 +565,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -588,6 +609,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "kordophone-db" +version = "0.1.0" +dependencies = [ + "chrono", + "microrm", + "serde", + "time", + "uuid", +] + [[package]] name = "kpcli" version = "0.1.0" @@ -613,6 +645,16 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -641,6 +683,33 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "microrm" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7119c145e9ee33d8a79a2c7e15c4f429d681079cea976692a55c78aaae34f5ae" +dependencies = [ + "itertools", + "libsqlite3-sys", + "log", + "microrm-macros", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "microrm-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3e29bc1b3c1dc742de1b3a23f11e472a2adc936a967dc5d769c26a809326b8" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -733,7 +802,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.90", ] [[package]] @@ -821,9 +890,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -970,22 +1039,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.198" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.90", ] [[package]] @@ -1050,9 +1119,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.60" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1082,9 +1162,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "num-conv", @@ -1102,9 +1182,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -1136,7 +1216,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.90", ] [[package]] @@ -1220,9 +1300,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "rand", @@ -1231,13 +1311,13 @@ dependencies = [ [[package]] name = "uuid-macro-internal" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" +checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.90", ] [[package]] @@ -1283,7 +1363,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -1305,7 +1385,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 82180ee..85fc82d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ - "kordophone", + "kordophone", + "kordophone-db", "kpcli" ] resolver = "2" diff --git a/kordophone-db/Cargo.toml b/kordophone-db/Cargo.toml new file mode 100644 index 0000000..ac8e6db --- /dev/null +++ b/kordophone-db/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "kordophone-db" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4.38" +microrm = "0.4.4" +serde = { version = "1.0.215", features = ["derive"] } +time = "0.3.37" +uuid = { version = "1.11.0", features = ["v4"] } diff --git a/kordophone-db/src/chat_database.rs b/kordophone-db/src/chat_database.rs new file mode 100644 index 0000000..1ca973b --- /dev/null +++ b/kordophone-db/src/chat_database.rs @@ -0,0 +1,78 @@ +use microrm::prelude::*; +use microrm::Stored; +use crate::models::conversation::{self, Conversation, ConversationID}; +use std::error::Error; + +pub struct ChatDatabase { + db: DB, +} + +#[derive(Database)] +struct DB { + conversations: microrm::IDMap, +} + +impl ChatDatabase { + pub fn new_in_memory() -> Result> { + let db = DB::open_path(":memory:")?; + return Ok(Self { + db: db, + }) + } + + pub fn insert_conversation(&self, conversation: Conversation) -> Result { + // First see if conversation guid already exists, update it if so + let guid = &conversation.guid; + let mut existing = self.db.conversations + .with(Conversation::Guid, guid) + .get()?; + + if let Some(existing) = existing.first_mut() { + existing.display_name = conversation.display_name; + existing.sync(); + return Ok(existing.id()); + } else { + // Otherwise, insert. + return self.db.conversations.insert(conversation); + } + } + + pub fn get_conversation_by_id(&self, id: ConversationID) -> Result, microrm::Error> { + self.db.conversations + .by_id(id) + .map(|stored_conversation| stored_conversation + .map(|stored| stored.wrapped()) + ) + } + + pub fn get_conversation_by_guid(&self, guid: &str) -> Result, microrm::Error> { + self.db.conversations + .with(Conversation::Guid, guid) + .get() + .and_then(|v| Ok(v + .into_iter() + .map(|c| c.wrapped()) + .last() + )) + } + + pub fn all_conversations(&self) -> Result, microrm::Error> { + self.db.conversations + .get() + .map(|v| v + .into_iter() + .map(|c| c.wrapped()) + .collect() + ) + } + + fn stored_conversation_by_guid(&self, guid: &str) -> Result>, microrm::Error> { + self.db.conversations + .with(Conversation::Guid, guid) + .get() + .map(|v| v + .into_iter() + .last() + ) + } +} \ No newline at end of file diff --git a/kordophone-db/src/lib.rs b/kordophone-db/src/lib.rs new file mode 100644 index 0000000..1eca944 --- /dev/null +++ b/kordophone-db/src/lib.rs @@ -0,0 +1,69 @@ +pub mod models; +pub mod chat_database; + +#[cfg(test)] +mod tests { + use microrm::prelude::Queryable; + + use crate::{chat_database::{self, ChatDatabase}, models::conversation::{Conversation, ConversationBuilder, Participant}}; + + #[test] + fn test_database_init() { + let _ = ChatDatabase::new_in_memory().unwrap(); + } + + #[test] + fn test_add_conversation() { + let db = ChatDatabase::new_in_memory().unwrap(); + + let test_conversation = Conversation::builder() + .guid("test") + .unread_count(2) + .display_name("Test Conversation") + .build(); + + let id = db.insert_conversation(test_conversation.clone()).unwrap(); + + // Try to fetch with id now + let conversation = db.get_conversation_by_id(id).unwrap().unwrap(); + assert_eq!(conversation.guid, "test"); + + // Modify the conversation and update it + let modified_conversation = test_conversation.into_builder() + .display_name("Modified Conversation") + .build(); + + db.insert_conversation(modified_conversation.clone()).unwrap(); + + // Make sure we still only have one conversation. + let all_conversations = db.all_conversations().unwrap(); + assert_eq!(all_conversations.len(), 1); + + // And make sure the display name was updated + let conversation = db.get_conversation_by_id(id).unwrap().unwrap(); + assert_eq!(conversation.display_name.unwrap(), "Modified Conversation"); + } + + #[test] + fn test_conversation_participants() { + let db = ChatDatabase::new_in_memory().unwrap(); + + let participants: Vec = vec!["one".into(), "two".into()]; + let conversation = ConversationBuilder::new() + .display_name("Test") + .participant_display_names(participants.clone()) + .build(); + + let id = db.insert_conversation(conversation).unwrap(); + + let read_conversation = db.get_conversation_by_id(id).unwrap().unwrap(); + let read_participants: Vec = read_conversation.participant_display_names + .get() + .unwrap() + .into_iter() + .map(|p| p.wrapped().display_name) + .collect(); + + assert_eq!(participants, read_participants); + } +} diff --git a/kordophone-db/src/models/conversation.rs b/kordophone-db/src/models/conversation.rs new file mode 100644 index 0000000..3807d54 --- /dev/null +++ b/kordophone-db/src/models/conversation.rs @@ -0,0 +1,113 @@ +use microrm::prelude::*; +use chrono::{DateTime, Local, Utc}; +use time::OffsetDateTime; +use uuid::Uuid; + +use crate::models::date::Date; + +#[derive(Entity, Clone)] +pub struct Conversation { + #[unique] + pub guid: String, + + pub unread_count: i64, + pub display_name: Option, + pub last_message_preview: Option, + pub date: OffsetDateTime, + pub participant_display_names: microrm::RelationMap, +} + +#[derive(Entity, Clone)] +pub struct Participant { + #[unique] + pub display_name: String +} + +impl Into for String { + fn into(self) -> Participant { + Participant { display_name: self } + } +} + +impl Conversation { + pub fn builder() -> ConversationBuilder { + ConversationBuilder::new() + } + + pub fn into_builder(&self) -> ConversationBuilder { + ConversationBuilder { + guid: Some(self.guid.clone()), + date: Date::new(self.date), + participant_display_names: None, + unread_count: Some(self.unread_count), + last_message_preview: self.last_message_preview.clone(), + display_name: self.display_name.clone(), + } + } +} + +#[derive(Default)] +pub struct ConversationBuilder { + guid: Option, + date: Date, + unread_count: Option, + last_message_preview: Option, + participant_display_names: Option>, + display_name: Option, +} + +impl ConversationBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn guid(mut self, guid: &str) -> Self { + self.guid = Some(guid.into()); + self + } + + pub fn date(mut self, date: Date) -> Self { + self.date = date; + self + } + + pub fn unread_count(mut self, unread_count: i64) -> Self { + self.unread_count = Some(unread_count); + self + } + + pub fn last_message_preview(mut self, last_message_preview: &str) -> Self { + self.last_message_preview = Some(last_message_preview.into()); + self + } + + pub fn participant_display_names(mut self, participant_display_names: Vec) -> Self { + self.participant_display_names = Some(participant_display_names); + self + } + + pub fn display_name(mut self, display_name: &str) -> Self { + self.display_name = Some(display_name.into()); + self + } + + pub fn build(self) -> Conversation { + let result = Conversation { + guid: self.guid.unwrap_or(Uuid::new_v4().to_string()), + unread_count: self.unread_count.unwrap_or(0), + last_message_preview: self.last_message_preview, + display_name: self.display_name, + date: self.date.dt, + participant_display_names: Default::default(), + }; + + // TODO: this isn't right... this is crashing the test. + if let Some(participants) = self.participant_display_names { + for participant in participants { + result.participant_display_names.insert(participant.into()).unwrap(); + } + } + + result + } +} diff --git a/kordophone-db/src/models/date.rs b/kordophone-db/src/models/date.rs new file mode 100644 index 0000000..b4ffea7 --- /dev/null +++ b/kordophone-db/src/models/date.rs @@ -0,0 +1,18 @@ +use chrono::{DateTime, Local, Utc}; +use time::OffsetDateTime; + +pub struct Date { + pub dt: OffsetDateTime, +} + +impl Date { + pub fn new(dt: OffsetDateTime) -> Self { + Self { dt } + } +} + +impl Default for Date { + fn default() -> Self { + Self { dt: OffsetDateTime::now_utc() } + } +} \ No newline at end of file diff --git a/kordophone-db/src/models/mod.rs b/kordophone-db/src/models/mod.rs new file mode 100644 index 0000000..3181e39 --- /dev/null +++ b/kordophone-db/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod conversation; +pub mod date; \ No newline at end of file