diff --git a/plugins/store/README.md b/plugins/store/README.md index 3c3f7c7e..807fd072 100644 --- a/plugins/store/README.md +++ b/plugins/store/README.md @@ -61,6 +61,39 @@ const val = await store.get("some-key"); assert(val, { value: 5 }); ``` +## Usage from Rust + +You can also access Stores from Rust, you can create new stores: + +```rust +use tauri_plugin_store::store::StoreBuilder; +use serde_json::json; + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::default().build()) + .setup(|app| { + let mut store = StoreBuilder::new(app.handle(), "path/to/store.bin".parse()?).build(); + + store.insert("a".to_string(), json!("b")) // note that values must be serd_json::Value to be compatible with JS + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +As you may have noticed, the Store crated above isn't accessible to the frontend. To interoperate with stores created by JS use the exported `with_store` method: + +```rust +use tauri::Wry; +use tauri_plugin_store::with_store; + +let stores = app.state::>(); +let path = PathBuf::from("path/to/the/storefile"); + +with_store(app_handle, stores, path, |store| store.set("a".to_string(), json!("b"))) +``` + ## Contributing PRs accepted. Please make sure to read the Contributing Guide before making a pull request. diff --git a/plugins/store/src/error.rs b/plugins/store/src/error.rs index 6919827d..03d29182 100644 --- a/plugins/store/src/error.rs +++ b/plugins/store/src/error.rs @@ -10,9 +10,9 @@ use std::path::PathBuf; #[non_exhaustive] pub enum Error { #[error("Failed to serialize store. {0}")] - Serialize(Box), + Serialize(Box), #[error("Failed to deserialize store. {0}")] - Deserialize(Box), + Deserialize(Box), /// JSON error. #[error(transparent)] Json(#[from] serde_json::Error), @@ -22,6 +22,9 @@ pub enum Error { /// Store not found #[error("Store \"{0}\" not found")] NotFound(PathBuf), + /// Some Tauri API failed + #[error(transparent)] + Tauri(#[from] tauri::Error), } impl Serialize for Error { diff --git a/plugins/store/src/lib.rs b/plugins/store/src/lib.rs index 6c641dbc..26588b71 100644 --- a/plugins/store/src/lib.rs +++ b/plugins/store/src/lib.rs @@ -5,251 +5,201 @@ pub use error::Error; use log::warn; use serde::Serialize; -use serde_json::Value as JsonValue; -use std::{collections::HashMap, path::PathBuf, sync::Mutex}; +pub use serde_json::Value as JsonValue; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Mutex, +}; pub use store::{Store, StoreBuilder}; use tauri::{ plugin::{self, TauriPlugin}, - AppHandle, Manager, RunEvent, Runtime, State, Window, + AppHandle, Manager, RunEvent, Runtime, State, }; mod error; mod store; #[derive(Serialize, Clone)] -struct ChangePayload { - path: PathBuf, - key: String, - value: JsonValue, +struct ChangePayload<'a> { + path: &'a Path, + key: &'a str, + value: &'a JsonValue, } #[derive(Default)] -struct StoreCollection { - stores: Mutex>, +pub struct StoreCollection { + stores: Mutex>>, frozen: bool, } -fn with_store Result>( - app: &AppHandle, - collection: State<'_, StoreCollection>, - path: PathBuf, +pub fn with_store) -> Result>( + app: AppHandle, + collection: State<'_, StoreCollection>, + path: impl AsRef, f: F, ) -> Result { let mut stores = collection.stores.lock().expect("mutex poisoned"); - if !stores.contains_key(&path) { + let path = path.as_ref(); + if !stores.contains_key(path) { if collection.frozen { - return Err(Error::NotFound(path)); + return Err(Error::NotFound(path.to_path_buf())); } - let mut store = StoreBuilder::new(path.clone()).build(); + let mut store = StoreBuilder::new(app, path.to_path_buf()).build(); // ignore loading errors, just use the default - if let Err(err) = store.load(app) { + if let Err(err) = store.load() { warn!( "Failed to load store {:?} from disk: {}. Falling back to default values.", path, err ); } - stores.insert(path.clone(), store); + stores.insert(path.to_path_buf(), store); } f(stores - .get_mut(&path) + .get_mut(path) .expect("failed to retrieve store. This is a bug!")) } #[tauri::command] async fn set( app: AppHandle, - window: Window, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, key: String, value: JsonValue, ) -> Result<(), Error> { - with_store(&app, stores, path.clone(), |store| { - store.cache.insert(key.clone(), value.clone()); - let _ = window.emit("store://change", ChangePayload { path, key, value }); - Ok(()) - }) + with_store(app, stores, path, |store| store.insert(key, value)) } #[tauri::command] async fn get( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, key: String, ) -> Result, Error> { - with_store(&app, stores, path, |store| { - Ok(store.cache.get(&key).cloned()) - }) + with_store(app, stores, path, |store| Ok(store.get(key).cloned())) } #[tauri::command] async fn has( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, key: String, ) -> Result { - with_store(&app, stores, path, |store| { - Ok(store.cache.contains_key(&key)) - }) + with_store(app, stores, path, |store| Ok(store.has(key))) } #[tauri::command] async fn delete( app: AppHandle, - window: Window, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, key: String, ) -> Result { - with_store(&app, stores, path.clone(), |store| { - let flag = store.cache.remove(&key).is_some(); - if flag { - let _ = window.emit( - "store://change", - ChangePayload { - path, - key, - value: JsonValue::Null, - }, - ); - } - Ok(flag) - }) + with_store(app, stores, path, |store| store.delete(key)) } #[tauri::command] async fn clear( app: AppHandle, - window: Window, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, ) -> Result<(), Error> { - with_store(&app, stores, path.clone(), |store| { - let keys = store.cache.keys().cloned().collect::>(); - store.cache.clear(); - for key in keys { - let _ = window.emit( - "store://change", - ChangePayload { - path: path.clone(), - key, - value: JsonValue::Null, - }, - ); - } - Ok(()) - }) + with_store(app, stores, path, |store| store.clear()) } #[tauri::command] async fn reset( app: AppHandle, - window: Window, - collection: State<'_, StoreCollection>, + collection: State<'_, StoreCollection>, path: PathBuf, ) -> Result<(), Error> { - let has_defaults = collection - .stores - .lock() - .expect("mutex poisoned") - .get(&path) - .map(|store| store.defaults.is_some()); - - if Some(true) == has_defaults { - with_store(&app, collection, path.clone(), |store| { - if let Some(defaults) = &store.defaults { - for (key, value) in &store.cache { - if defaults.get(key) != Some(value) { - let _ = window.emit( - "store://change", - ChangePayload { - path: path.clone(), - key: key.clone(), - value: defaults.get(key).cloned().unwrap_or(JsonValue::Null), - }, - ); - } - } - store.cache = defaults.clone(); - } - Ok(()) - }) - } else { - clear(app, window, collection, path).await - } + with_store(app, collection, path, |store| store.reset()) } #[tauri::command] async fn keys( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, ) -> Result, Error> { - with_store(&app, stores, path, |store| { - Ok(store.cache.keys().cloned().collect()) + with_store(app, stores, path, |store| { + Ok(store.keys().cloned().collect()) }) } #[tauri::command] async fn values( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, ) -> Result, Error> { - with_store(&app, stores, path, |store| { - Ok(store.cache.values().cloned().collect()) + with_store(app, stores, path, |store| { + Ok(store.values().cloned().collect()) }) } #[tauri::command] async fn entries( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, ) -> Result, Error> { - with_store(&app, stores, path, |store| { - Ok(store.cache.clone().into_iter().collect()) + with_store(app, stores, path, |store| { + Ok(store + .entries() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect()) }) } #[tauri::command] async fn length( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, ) -> Result { - with_store(&app, stores, path, |store| Ok(store.cache.len())) + with_store(app, stores, path, |store| Ok(store.len())) } #[tauri::command] async fn load( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, ) -> Result<(), Error> { - with_store(&app, stores, path, |store| store.load(&app)) + with_store(app, stores, path, |store| store.load()) } #[tauri::command] async fn save( app: AppHandle, - stores: State<'_, StoreCollection>, + stores: State<'_, StoreCollection>, path: PathBuf, ) -> Result<(), Error> { - with_store(&app, stores, path, |store| store.save(&app)) + with_store(app, stores, path, |store| store.save()) } -#[derive(Default)] -pub struct Builder { - stores: HashMap, +// #[derive(Default)] +pub struct Builder { + stores: HashMap>, frozen: bool, } -impl Builder { +impl Default for Builder { + fn default() -> Self { + Self { + stores: Default::default(), + frozen: false, + } + } +} + +impl Builder { /// Registers a store with the plugin. /// /// # Examples @@ -265,7 +215,7 @@ impl Builder { /// # Ok(()) /// # } /// ``` - pub fn store(mut self, store: Store) -> Self { + pub fn store(mut self, store: Store) -> Self { self.stores.insert(store.path.clone(), store); self } @@ -285,7 +235,7 @@ impl Builder { /// # Ok(()) /// # } /// ``` - pub fn stores>(mut self, stores: T) -> Self { + pub fn stores>>(mut self, stores: T) -> Self { self.stores = stores .into_iter() .map(|store| (store.path.clone(), store)) @@ -331,7 +281,7 @@ impl Builder { /// # Ok(()) /// # } /// ``` - pub fn build(mut self) -> TauriPlugin { + pub fn build(mut self) -> TauriPlugin { plugin::Builder::new("store") .invoke_handler(tauri::generate_handler![ set, get, has, delete, clear, reset, keys, values, length, entries, load, save @@ -339,7 +289,7 @@ impl Builder { .setup(move |app_handle| { for (path, store) in self.stores.iter_mut() { // ignore loading errors, just use the default - if let Err(err) = store.load(app_handle) { + if let Err(err) = store.load() { warn!( "Failed to load store {:?} from disk: {}. Falling back to default values.", path, err @@ -356,10 +306,10 @@ impl Builder { }) .on_event(|app_handle, event| { if let RunEvent::Exit = event { - let collection = app_handle.state::(); + let collection = app_handle.state::>(); for store in collection.stores.lock().expect("mutex poisoned").values() { - if let Err(err) = store.save(app_handle) { + if let Err(err) = store.save() { eprintln!("failed to save store {:?} with error {:?}", store.path, err); } } diff --git a/plugins/store/src/store.rs b/plugins/store/src/store.rs index af106e30..5ca2f8e9 100644 --- a/plugins/store/src/store.rs +++ b/plugins/store/src/store.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::Error; +use crate::{ChangePayload, Error}; use serde_json::Value as JsonValue; use std::{ collections::HashMap, @@ -10,25 +10,28 @@ use std::{ io::Write, path::PathBuf, }; -use tauri::{AppHandle, Runtime}; +use tauri::{AppHandle, Manager, Runtime}; -type SerializeFn = fn(&HashMap) -> Result, Box>; -type DeserializeFn = fn(&[u8]) -> Result, Box>; +type SerializeFn = + fn(&HashMap) -> Result, Box>; +type DeserializeFn = + fn(&[u8]) -> Result, Box>; fn default_serialize( cache: &HashMap, -) -> Result, Box> { +) -> Result, Box> { Ok(serde_json::to_vec(&cache)?) } fn default_deserialize( bytes: &[u8], -) -> Result, Box> { +) -> Result, Box> { serde_json::from_slice(bytes).map_err(Into::into) } /// Builds a [`Store`] -pub struct StoreBuilder { +pub struct StoreBuilder { + app: AppHandle, path: PathBuf, defaults: Option>, cache: HashMap, @@ -36,7 +39,7 @@ pub struct StoreBuilder { deserialize: DeserializeFn, } -impl StoreBuilder { +impl StoreBuilder { /// Creates a new [`StoreBuilder`]. /// /// # Examples @@ -49,8 +52,9 @@ impl StoreBuilder { /// # Ok(()) /// # } /// ``` - pub fn new(path: PathBuf) -> Self { + pub fn new(app: AppHandle, path: PathBuf) -> Self { Self { + app, path, defaults: None, cache: Default::default(), @@ -147,8 +151,9 @@ impl StoreBuilder { /// /// # Ok(()) /// # } - pub fn build(self) -> Store { + pub fn build(self) -> Store { Store { + app: self.app, path: self.path, defaults: self.defaults, cache: self.cache, @@ -159,18 +164,20 @@ impl StoreBuilder { } #[derive(Clone)] -pub struct Store { +pub struct Store { + app: AppHandle, pub(crate) path: PathBuf, - pub(crate) defaults: Option>, - pub(crate) cache: HashMap, + defaults: Option>, + cache: HashMap, serialize: SerializeFn, deserialize: DeserializeFn, } -impl Store { +impl Store { /// Update the store from the on-disk state - pub fn load(&mut self, app: &AppHandle) -> Result<(), Error> { - let app_dir = app + pub fn load(&mut self) -> Result<(), Error> { + let app_dir = self + .app .path_resolver() .app_data_dir() .expect("failed to resolve app dir"); @@ -184,8 +191,9 @@ impl Store { } /// Saves the store to disk - pub fn save(&self, app: &AppHandle) -> Result<(), Error> { - let app_dir = app + pub fn save(&self) -> Result<(), Error> { + let app_dir = self + .app .path_resolver() .app_data_dir() .expect("failed to resolve app dir"); @@ -199,9 +207,107 @@ impl Store { Ok(()) } + + pub fn insert(&mut self, key: String, value: JsonValue) -> Result<(), Error> { + self.cache.insert(key.clone(), value.clone()); + self.app.emit_all( + "store://change", + ChangePayload { + path: &self.path, + key: &key, + value: &value, + }, + )?; + + Ok(()) + } + + pub fn get(&self, key: impl AsRef) -> Option<&JsonValue> { + self.cache.get(key.as_ref()) + } + + pub fn has(&self, key: impl AsRef) -> bool { + self.cache.contains_key(key.as_ref()) + } + + pub fn delete(&mut self, key: impl AsRef) -> Result { + let flag = self.cache.remove(key.as_ref()).is_some(); + if flag { + self.app.emit_all( + "store://change", + ChangePayload { + path: &self.path, + key: key.as_ref(), + value: &JsonValue::Null, + }, + )?; + } + Ok(flag) + } + + pub fn clear(&mut self) -> Result<(), Error> { + let keys: Vec = self.cache.keys().cloned().collect(); + self.cache.clear(); + for key in keys { + self.app.emit_all( + "store://change", + ChangePayload { + path: &self.path, + key: &key, + value: &JsonValue::Null, + }, + )?; + } + Ok(()) + } + + pub fn reset(&mut self) -> Result<(), Error> { + let has_defaults = self.defaults.is_some(); + + if has_defaults { + if let Some(defaults) = &self.defaults { + for (key, value) in &self.cache { + if defaults.get(key) != Some(value) { + let _ = self.app.emit_all( + "store://change", + ChangePayload { + path: &self.path, + key, + value: defaults.get(key).unwrap_or(&JsonValue::Null), + }, + ); + } + } + self.cache = defaults.clone(); + } + Ok(()) + } else { + self.clear() + } + } + + pub fn keys(&self) -> impl Iterator { + self.cache.keys() + } + + pub fn values(&self) -> impl Iterator { + self.cache.values() + } + + pub fn entries(&self) -> impl Iterator { + self.cache.iter() + } + + pub fn len(&self) -> usize { + self.cache.len() + } + + pub fn is_empty(&self) -> bool { + self.cache.is_empty() + } } -impl std::fmt::Debug for Store { +impl std::fmt::Debug for Store { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Store") .field("path", &self.path)