-
Notifications
You must be signed in to change notification settings - Fork 34
Open
Labels
enhancementNew feature or requestNew feature or request
Description
The Problem / Limitation
The existing storage APIs, like use_synced_storage
do not appear to to let the user control the encoding.
I care a lot about ensuring my actual persisted data format is human readable and human editable so I have been using pretty printed JSON as my data format.
I also like to fully cleanup the local storage entry (via removeItem) when not using it.
As far as I can tell neither of these is possible with use_synced_storage
as it appears to unconditionally encode using postcard, compressed with yazi then run through some custom logic to ensure its a valid string, and there is no way to express and empty/cleared state.
Proposed Solutions
I think separating the storage location from the encoding would be a good way to do this.
Something like this could work:
/// A trait for a storage backing.
///
/// Unchanged.
pub trait StorageBacking: Clone + 'static {
/// The key type used to store data in storage
type Key: PartialEq + Clone + Debug + Send + Sync + 'static;
/// Gets a value from storage for the given key.
///
/// Question: Does None here indicate the storage was empty, or an error?
fn get<T: DeserializeOwned + Clone + 'static>(key: &Self::Key) -> Option<T>;
/// Sets a value in storage for the given key
fn set<T: Serialize + Send + Sync + Clone + 'static>(key: Self::Key, value: &T);
}
/// A trait for the persistence portion of StorageBacking.
pub trait StoragePersistence: Clone + 'static {
/// The key type used to store data in storage
type Key: PartialEq + Clone + Debug + Send + Sync + 'static;
/// The type of value which can be stored.
type Value;
/// Gets a value from storage for the given key
fn store(key: &Self::Key) -> Self::Value;
/// Sets a value in storage for the given key
fn load(key: Self::Key, value: &Self::Value);
}
/// LocalStorage stores Option<String>.
impl StoragePersistence for LocalStorage {
type Key = String;
type Value = Option<String>;
fn store(key: &Self::Key) -> Self::Value {
// Use existing logic from storage code here.
// Use the second half of https://github.com/DioxusLabs/sdk/blob/adf59211ea3caaaa404503660693f2cd9bb44968/packages/storage/src/client_storage/web.rs#L111
todo!()
}
fn load(key: Self::Key, value: &Self::Value) {
// Use existing logic from storage code here.
// Use the second half of https://github.com/DioxusLabs/sdk/blob/adf59211ea3caaaa404503660693f2cd9bb44968/packages/storage/src/client_storage/web.rs#L119
todo!()
}
}
/// New trait which can be implemented to define a data format for storage.
pub trait StorageEncoder: Clone + 'static {
/// The type of value which can be stored.
type Value;
fn deserialize<T: DeserializeOwned + Clone + 'static>(loaded: &Self::Value) -> T;
fn serialize<T: Serialize + Send + Sync + Clone + 'static>(value: &T) -> Self::Value;
}
/// A way to create a StorageEncoder out of the two layers.
///
/// I'm not sure if this is the best way to abstract that.
#[derive(Clone)]
pub struct LayeredStorage<Persistence: StoragePersistence, Encoder: StorageEncoder> {
persistence: PhantomData<Persistence>,
encoder: PhantomData<Encoder>,
}
/// StorageBacking for LayeredStorage.
impl<Value, P: StoragePersistence<Value = Option<Value>>, E: StorageEncoder<Value = Value>>
StorageBacking for LayeredStorage<P, E>
{
type Key = P::Key;
fn get<T: DeserializeOwned + Clone + 'static>(key: &Self::Key) -> Option<T> {
let loaded = P::store(key);
match loaded {
Some(t) => E::deserialize(&t),
None => None,
}
}
fn set<T: Serialize + Send + Sync + Clone + 'static>(key: Self::Key, value: &T) {
P::load(key, &Some(E::serialize(value)));
}
}
/// Since StorageEncoder does not provide a way to clear, implement some options
impl<Value, P: StoragePersistence<Value = Option<Value>>, E: StorageEncoder<Value = Value>>
LayeredStorage<P, E>
{
pub fn clear(key: P::Key) {
P::load(key, &None);
}
pub fn set_or_clear<T: Serialize + Send + Sync + Clone + 'static>(
key: P::Key,
value: &Option<T>,
) {
match value {
Some(t) => Self::set(key, t),
None => Self::clear(key),
}
}
}
#[derive(Clone)]
struct DefaultEncoder;
impl StorageEncoder for DefaultEncoder {
type Value = String;
fn deserialize<T: DeserializeOwned + Clone + 'static>(loaded: &Self::Value) -> T {
// Use existing logic from storage code here.
// Use the first half of https://github.com/DioxusLabs/sdk/blob/adf59211ea3caaaa404503660693f2cd9bb44968/packages/storage/src/client_storage/web.rs#L119
todo!()
}
fn serialize<T: Serialize + Send + Sync + Clone + 'static>(value: &T) -> Self::Value {
// Use existing logic from storage code here.
// Use the first half of https://github.com/DioxusLabs/sdk/blob/adf59211ea3caaaa404503660693f2cd9bb44968/packages/storage/src/client_storage/web.rs#L111
todo!()
}
}
/// StorageBacking using default encoder: handles LocalStorage and other built in storage implementations.
impl<P: StoragePersistence<Value = Option<String>>> StorageBacking for P {
type Key = P::Key;
fn get<T: DeserializeOwned + Clone + 'static>(key: &Self::Key) -> Option<T> {
LayeredStorage::<P, DefaultEncoder>::get(key)
}
fn set<T: Serialize + Send + Sync + Clone + 'static>(key: Self::Key, value: &T) {
LayeredStorage::<P, DefaultEncoder>::set(key, value)
}
}
type HumanReadableStorage<Storage: StoragePersistence> =
LayeredStorage<Storage, HumanReadableEncoding>;
#[derive(Clone)]
struct HumanReadableEncoding;
impl StorageEncoder for HumanReadableEncoding {
type Value = String;
fn deserialize<T: DeserializeOwned + Clone + 'static>(loaded: &Self::Value) -> T {
let parsed: Result<T, serde_json::Error> = serde_json::from_str(loaded);
// This design probably needs an error handling policy better than panic.
parsed.unwrap()
}
fn serialize<T: Serialize + Send + Sync + Clone + 'static>(value: &T) -> Self::Value {
serde_json::to_string_pretty(value).unwrap()
}
}
fn example() {
let s =
use_synced_storage::<HumanReadableStorage<LocalStorage>, isize>("demo".to_string(), || 0);
}
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request