From 4568f0e69df148c6b7c804b5400db66c3163590a Mon Sep 17 00:00:00 2001 From: Caio Date: Fri, 29 Mar 2019 23:29:30 -0300 Subject: [PATCH] Add storage crate --- Cargo.toml | 3 +- crates/storage/Cargo.toml | 32 +++++ crates/storage/src/indexed_db.rs | 162 +++++++++++++++++++++++ crates/storage/src/json_storage.rs | 198 +++++++++++++++++++++++++++++ crates/storage/src/lib.rs | 99 +++++++++++++++ crates/storage/tests/web.rs | 19 +++ src/lib.rs | 1 + 7 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 crates/storage/Cargo.toml create mode 100644 crates/storage/src/indexed_db.rs create mode 100644 crates/storage/src/json_storage.rs create mode 100644 crates/storage/src/lib.rs create mode 100644 crates/storage/tests/web.rs diff --git a/Cargo.toml b/Cargo.toml index 8f4e7e55..d89bf233 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,9 @@ readme = "README.md" version = "0.1.0" [dependencies] -gloo-timers = { version = "0.1.0", path = "crates/timers" } gloo-console-timer = { version = "0.1.0", path = "crates/console-timer" } +gloo-storage = { version = "0.1.0", path = "crates/storage" } +gloo-timers = { version = "0.1.0", path = "crates/timers" } [features] default = [] diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml new file mode 100644 index 00000000..aca1ecc1 --- /dev/null +++ b/crates/storage/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "gloo-storage" +version = "0.1.0" +authors = ["Rust and WebAssembly Working Group"] +edition = "2018" + +[dependencies] +futures = "0.1.25" +js-sys = "0.3.17" +serde_json = { optional = true, version = "1.0" } +wasm-bindgen = "0.2.40" +wasm-bindgen-futures = "0.3.17" +web-sys = "0.3.17" + +[dev-dependencies] +wasm-bindgen-test = "0.2.37" + +[features] +default = ["indexed-db"] +indexed-db = [ + "web-sys/IdbDatabase", + "web-sys/IdbFactory", + "web-sys/IdbIndex", + "web-sys/IdbIndexParameters", + "web-sys/IdbObjectStore", + "web-sys/IdbOpenDbOptions", + "web-sys/IdbOpenDbRequest", + "web-sys/IdbRequest", + "web-sys/IdbTransaction", + "web-sys/Window" +] +json-storage = ["serde_json", "web-sys/Storage", "web-sys/Window"] diff --git a/crates/storage/src/indexed_db.rs b/crates/storage/src/indexed_db.rs new file mode 100644 index 00000000..5754d0e7 --- /dev/null +++ b/crates/storage/src/indexed_db.rs @@ -0,0 +1,162 @@ +use crate::{Index, Storage, TableDefinition, TableManager, Version}; +use wasm_bindgen::{prelude::*, JsCast}; +use web_sys::{IdbDatabase, IdbFactory, IdbIndexParameters, IdbObjectStore, IdbOpenDbOptions}; + +/// IndexedDB +#[derive(Debug)] +pub struct IndexedDb { + database_name: String, + factory: IdbFactory, +} + +impl IndexedDb { + /// Creates a new instance for database `name`. + pub fn new(name: I) -> Self + where + I: Into, + { + Self { + database_name: name.into(), + factory: web_sys::window() + .unwrap_throw() + .indexed_db() + .unwrap_throw() + .unwrap_throw(), + } + } +} + +impl Storage for IndexedDb { + type TableManager = IndexedDbTableManager; + type Version = IndexedDbVersion; + + fn add_version(&mut self, version: I, cb: F) + where + F: FnOnce(Self::Version) -> Self::Version + 'static, + I: Into, + { + let request = self + .factory + .open_with_idb_open_db_options( + &self.database_name, + IdbOpenDbOptions::new().version(version.into()), + ) + .unwrap_throw(); + let result = request.result(); + let closure = Closure::once(move || { + let database = result.unwrap_throw().unchecked_into::(); + cb(IndexedDbVersion { database }) + }); + let func = closure.as_ref().unchecked_ref::(); + request.set_onupgradeneeded(Some(func)); + } + + fn delete(&mut self) { + self.factory + .delete_database(&self.database_name) + .unwrap_throw(); + } + + fn name(&self) -> &str { + &self.database_name + } + + fn table_manager(&mut self, _: &str) -> Self::TableManager { + unimplemented!(); + } + + fn transaction(&mut self) { + unimplemented!(); + } +} + +/// IndexedDB table definition +#[derive(Debug)] +pub struct IndexedDbTableDefinition { + object_store: IdbObjectStore, +} + +impl TableDefinition for IndexedDbTableDefinition { + fn add_row_with_index(self, name: &str, index: Index) -> Self { + let mut params = IdbIndexParameters::new(); + match index { + Index::MultiEntry => { + params.multi_entry(true); + } + Index::Unique => { + params.unique(true); + } + _ => {} + }; + self.object_store + .create_index_with_str_and_optional_parameters(name, name, ¶ms) + .unwrap_throw(); + self + } + + fn remove_old_row(self, name: &str) -> Self { + self.object_store.delete(&name.into()).unwrap_throw();; + self + } +} + +/// IndexedDB table manager +#[derive(Debug)] +pub struct IndexedDbTableManager(); + +impl TableManager for IndexedDbTableManager { + fn get_all() { + unimplemented!(); + } + + fn push() { + unimplemented!(); + } +} + +/// IndexedDB version +#[wasm_bindgen] +#[derive(Debug)] +pub struct IndexedDbVersion { + database: web_sys::IdbDatabase, +} + +impl Version for IndexedDbVersion { + type TableDefinition = IndexedDbTableDefinition; + + fn add_table(self, name: &str) -> Self { + self.database.create_object_store(name).unwrap_throw(); + self + } + + fn add_and_update_table(self, name: &str, cb: F) -> Self + where + F: FnMut(Self::TableDefinition) -> Self::TableDefinition, + { + self.add_table(name).update_table(name, cb) + } + + fn remove_table(self, name: &str) -> Self { + self.database.delete_object_store(name).unwrap_throw(); + self + } + + fn update_table(self, name: &str, mut cb: F) -> Self + where + F: FnMut(Self::TableDefinition) -> Self::TableDefinition, + { + cb(IndexedDbTableDefinition { + object_store: self + .database + .transaction_with_str(name) + .unwrap_throw() + .object_store(name) + .unwrap(), + }); + self + } + + fn update_version(self) -> Self { + unimplemented!(); + } +} diff --git a/crates/storage/src/json_storage.rs b/crates/storage/src/json_storage.rs new file mode 100644 index 00000000..b79c64d5 --- /dev/null +++ b/crates/storage/src/json_storage.rs @@ -0,0 +1,198 @@ +use crate::{Index, Storage, TableDefinition, TableManager, Version}; +use serde_json::{Map, Value}; +use wasm_bindgen::prelude::*; + +/// Common storage wrapper for both local and session storage. +#[derive(Debug)] +pub struct JsonStorage { + database_json: Value, + database_key: String, + storage: web_sys::Storage, +} + +impl JsonStorage { + /// Creates a new instance with local storage. + pub fn with_local_storage(name: S) -> Self + where + S: Into, + { + let storage = web_sys::window() + .unwrap_throw() + .local_storage() + .unwrap_throw() + .unwrap_throw(); + Self::instance(name.into(), storage) + } + + /// Creates a new instance with session storage. + pub fn with_session_storage(name: S) -> Self + where + S: Into, + { + let storage = web_sys::window() + .unwrap_throw() + .session_storage() + .unwrap_throw() + .unwrap_throw(); + Self::instance(name.into(), storage) + } + + fn instance(database_key: String, storage: web_sys::Storage) -> Self { + let opt_database_string = storage.get_item(&database_key).unwrap_throw(); + let database_json = if let Some(database_string) = opt_database_string { + let database_json: Value = database_string.into(); + assert!(database_json.is_object()); + database_json + } else { + Map::new().into() + }; + Self { + database_json, + database_key, + storage, + } + } +} + +impl Storage for JsonStorage { + type TableManager = JsonStorageTableManager; + type Version = JsonStorageVersion; + + fn add_version(&mut self, version: I, cb: F) + where + F: FnOnce(Self::Version) -> Self::Version + 'static, + I: Into, + { + let database_map = self.database_json.as_object_mut().unwrap(); + let version_f64 = version.into(); + let value_map = if database_map.is_empty() { + Map::new() + } else { + let mut iter = database_map.iter(); + let (mut previous_version_key, _) = iter.next().unwrap(); + let mut previous_version_key_f64 = previous_version_key.parse::().unwrap(); + for (key, _) in database_map.iter() { + let parsed_key = key.parse::().unwrap(); + if parsed_key < previous_version_key_f64 { + previous_version_key = key; + previous_version_key_f64 = parsed_key; + } + } + if previous_version_key_f64 > version_f64 { + panic!("A new version must have an id greater than all the other stored versions.") + } + database_map[previous_version_key] + .as_object() + .unwrap() + .clone() + }; + let jsv = cb(JsonStorageVersion { value_map }); + database_map.insert(version_f64.to_string(), jsv.value_map.into()); + } + + fn delete(&mut self) { + self.storage.remove_item(&self.database_key).unwrap_throw(); + } + + fn name(&self) -> &str { + &self.database_key + } + + fn table_manager(&mut self, _: &str) -> Self::TableManager { + unimplemented!(); + } + + fn transaction(&mut self) { + unimplemented!(); + } +} + +impl Drop for JsonStorage { + fn drop(&mut self) { + self.storage + .set_item(&self.database_key, &self.database_json.to_string()) + .unwrap_throw(); + } +} + +/// Storage table definition +#[derive(Debug)] +pub struct JsonStorageTableDefinition { + pub(crate) table_map: Value, +} + +impl TableDefinition for JsonStorageTableDefinition { + /// The index has no effect as `Storage` does not support indexes. + fn add_row_with_index(mut self, name: &str, _: Index) -> Self { + self.table_map + .as_object_mut() + .unwrap() + .insert(name.into(), Map::new().into()); + self + } + + fn remove_old_row(mut self, name: &str) -> Self { + self.table_map + .as_object_mut() + .unwrap() + .remove(name) + .unwrap(); + self + } +} + +/// Storage table manager +#[derive(Debug)] +pub struct JsonStorageTableManager {} + +impl TableManager for JsonStorageTableManager { + fn get_all() { + unimplemented!(); + } + + fn push() { + unimplemented!(); + } +} + +/// Storage version +#[derive(Debug)] +pub struct JsonStorageVersion { + pub(crate) value_map: Map, +} + +impl Version for JsonStorageVersion { + type TableDefinition = JsonStorageTableDefinition; + + fn add_table(mut self, name: &str) -> Self { + self.value_map.insert(name.into(), Map::new().into()); + self + } + + fn add_and_update_table(self, name: &str, cb: F) -> Self + where + F: FnMut(Self::TableDefinition) -> Self::TableDefinition, + { + self.add_table(name).update_table(name, cb) + } + + fn remove_table(mut self, name: &str) -> Self { + self.value_map.remove(name).unwrap(); + self + } + + fn update_table(mut self, name: &str, mut cb: F) -> Self + where + F: FnMut(Self::TableDefinition) -> Self::TableDefinition, + { + let jstd = cb(JsonStorageTableDefinition { + table_map: self.value_map[name].take(), + }); + *self.value_map.get_mut(name).unwrap() = jstd.table_map; + self + } + + fn update_version(self) -> Self { + unimplemented!(); + } +} diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs new file mode 100644 index 00000000..3c3d3856 --- /dev/null +++ b/crates/storage/src/lib.rs @@ -0,0 +1,99 @@ +//! AA + +#![deny(missing_docs, missing_debug_implementations)] + +#[cfg(feature = "indexed-db")] +pub(crate) mod indexed_db; +#[cfg(feature = "json-storage")] +pub(crate) mod json_storage; + +#[cfg(feature = "indexed-db")] +pub use indexed_db::*; +#[cfg(feature = "json-storage")] +pub use json_storage::*; + +/// Row index. +#[derive(Debug)] +pub enum Index { + /// Auto increment + AutoIncrement, + /// Compound + Compound, + /// Multi entry + MultiEntry, + /// Unique + Unique, +} + +/// Storage. +pub trait Storage { + /// Table manager. + type TableManager; + /// Version. + type Version; + + /// Adds a new version into the current database. + fn add_version(&mut self, version: I, cb: F) + where + F: FnOnce(Self::Version) -> Self::Version + 'static, + I: Into; + + /// Delets this database. + fn delete(&mut self); + + /// The name of this database. + fn name(&self) -> &str; + + /// Data manipulation for the most recent version. + fn table_manager(&mut self, table_name: &str) -> Self::TableManager; + + /// Transaction + fn transaction(&mut self); +} + +/// Table definition. +/// +/// You don't need to define an ordinary row because they are automatically included +/// in a DML operation. +pub trait TableDefinition { + /// Defines a new row with index. + fn add_row_with_index(self, name: &str, index: Index) -> Self; + + /// Removes a row from previous versions. + fn remove_old_row(self, name: &str) -> Self; +} + +/// Table manager. +/// +/// Provides methods for DML (Data Manipulation Language). +pub trait TableManager { + /// Gets all records. + fn get_all(); + /// Pushes a new record. + fn push(); +} + +/// Defines the state of a database. +pub trait Version { + /// Table definition + type TableDefinition; + + /// Adds a new table `name`. + fn add_table(self, name: &str) -> Self; + + /// Adds and updates the table `name`. + fn add_and_update_table(self, name: &str, cb: F) -> Self + where + F: FnMut(Self::TableDefinition) -> Self::TableDefinition; + + /// Removes a table `name` + fn remove_table(self, name: &str) -> Self; + + /// Updates the table `name`. + fn update_table(self, name: &str, cb: F) -> Self + where + F: FnMut(Self::TableDefinition) -> Self::TableDefinition; + + /// Updates this version against the previous version. + fn update_version(self) -> Self; +} diff --git a/crates/storage/tests/web.rs b/crates/storage/tests/web.rs new file mode 100644 index 00000000..de4e6a8e --- /dev/null +++ b/crates/storage/tests/web.rs @@ -0,0 +1,19 @@ +#![cfg(target_arch = "wasm32")] +#![cfg(feature = "json-storage")] + +use gloo_storage::*; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn local_storage() { + let mut s = JsonStorage::with_local_storage("some-storage"); + s.add_version(1, |v| { + v.add_and_update_table("first-table", |t| t.add_row_with_index("id", Index::Unique)) + }); + s.add_version(2, |v| { + v.add_table("second-table") + .update_table("first-table", |t| t.remove_old_row("id")) + }); +} diff --git a/src/lib.rs b/src/lib.rs index 52c3710b..62bc24ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,5 @@ // Re-exports of toolkit crates. pub use gloo_console_timer as console_timer; +pub use gloo_storage as storage; pub use gloo_timers as timers;