diff --git a/Cargo.lock b/Cargo.lock index 01e1e95..cacdc82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1158,6 +1158,7 @@ version = "0.1.0" dependencies = [ "reqwest", "serde", + "sqlx", "thiserror", "tokio", "uuid", diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..884e3de --- /dev/null +++ b/client/.gitignore @@ -0,0 +1 @@ +test.db \ No newline at end of file diff --git a/client/Cargo.toml b/client/Cargo.toml index c2651b0..bd8aae3 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] reqwest = { version = "0.12.2", features = ["json"] } serde = { version = "1.0.197", features = ["derive"] } +sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] } thiserror = "1.0.58" tokio = { version = "1.37.0", features = ["full"] } uuid = "1.8.0" diff --git a/client/src/lib.rs b/client/src/lib.rs index 65db2ff..1109edb 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,5 +1,6 @@ -mod error; -mod session; +pub mod error; +pub mod session; +pub mod storage; pub use session::Session; diff --git a/client/src/storage/mod.rs b/client/src/storage/mod.rs new file mode 100644 index 0000000..95833c4 --- /dev/null +++ b/client/src/storage/mod.rs @@ -0,0 +1,29 @@ +pub mod standard; + +#[derive(Debug, thiserror::Error, Clone)] +pub enum StorageError { + #[error("There is already an entry with the id '{0}'")] + IdCollision(String), + + #[error("The id '{0}' could not be found in the storage system")] + IdNotFound(String), + + #[error("Unknown error in storage system: {0}")] + Unknown(String), +} + +/// This trait is implemented by storage systems. It provides the interface for a simple key-value +/// lookup storage. The data should be stored persistent. +#[allow(async_fn_in_trait)] +pub trait Storage: Sized { + /// Save binary data in the storage. The data is identified by `id` and can be retrieved again + /// using [Storage::pull]. + async fn push(&mut self, id: &str, data: Vec) -> Result<(), StorageError>; + + /// Reads binary data identified by `id` from the storage. Returns an error if `id` is unknown. + async fn pull(&mut self, id: &str) -> Result<&Vec, StorageError>; + + /// Deletes the entry identified by `id` from the storage. Implementations should also destroy + /// the entries content by overwriting it. + async fn erase(&mut self, id: &str) -> Result<(), StorageError>; +} diff --git a/client/src/storage/standard.rs b/client/src/storage/standard.rs new file mode 100644 index 0000000..c3d40b7 --- /dev/null +++ b/client/src/storage/standard.rs @@ -0,0 +1,111 @@ +use crate::{ + storage::{Storage, StorageError}, + Session, +}; +use sqlx::SqlitePool; +use std::collections::HashMap; + +#[derive(Debug, sqlx::FromRow)] +struct EntryRow { + id: String, + data: Vec, +} + +/// This is the standard implementation of [Storage] +pub struct StandardStorage { + pool: SqlitePool, + entries: HashMap>, +} + +impl StandardStorage { + /// Creates a new instance of [StandardStorage]. The parameter `db` has to specify a connection + /// to a sqlite database in the SQLx format. + /// # Example + /// ``` + /// use icrc_client::storage::standard::StandardStorage; + /// + /// let storage = StandardStorage::new("sqlite:/path/to/database.db"); + /// ``` + pub async fn new(db: &str) -> Result { + let pool = SqlitePool::connect(db).await?; + + sqlx::query( + r#"CREATE TABLE IF NOT EXISTS Entries ( + id TEXT NOT NULL PRIMARY KEY, + data BLOB NOT NULL + );"#, + ) + .execute(&pool) + .await?; + + let mut entries = HashMap::new(); + let rows: Vec = sqlx::query_as(r#"SELECT * FROM Entries;"#) + .fetch_all(&pool) + .await?; + for row in rows { + entries.insert(row.id, row.data); + } + + Ok(Self { pool, entries }) + } +} + +impl Storage for StandardStorage { + async fn push(&mut self, id: &str, data: Vec) -> Result<(), StorageError> { + if self.entries.contains_key(id) { + return Err(StorageError::IdCollision(id.to_string())); + } + + sqlx::query(r#"INSERT INTO Entries VALUES (?, ?);"#) + .bind(id) + .bind(&data) + .execute(&self.pool) + .await + .map_err(|e| StorageError::Unknown(e.to_string()))?; + + self.entries.insert(id.to_string(), data); + + Ok(()) + } + + async fn pull(&mut self, id: &str) -> Result<&Vec, StorageError> { + if let Some(data) = self.entries.get(id) { + Ok(data) + } else { + Err(StorageError::IdNotFound(id.to_string())) + } + } + + async fn erase(&mut self, id: &str) -> Result<(), StorageError> { + if !self.entries.contains_key(id) { + return Err(StorageError::IdNotFound(id.to_string())); + } + + sqlx::query(r#"DELETE FROM Entries WHERE id = ?;"#) + .bind(id) + .execute(&self.pool) + .await + .map_err(|e| StorageError::Unknown(e.to_string()))?; + + self.entries.remove(id); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn standard_storage() { + let mut storage = StandardStorage::new("sqlite:test.db").await.unwrap(); + let data = vec![0xAC, 0xAB]; + storage.push("test", data).await.unwrap(); + + let pulled_data = storage.pull("test").await.unwrap(); + assert!(pulled_data[0] == 0xAC && pulled_data[1] == 0xAB); + + storage.erase("test").await.unwrap(); + } +}