1use std::fmt;
9use std::marker::PhantomData;
10
11use serde::de::DeserializeOwned;
12use serde::Serialize;
13
14#[derive(Debug)]
16pub enum StorageError {
17 Unavailable,
21 Serialize(serde_json::Error),
23 Deserialize(serde_json::Error),
25 Browser(String),
28}
29
30impl fmt::Display for StorageError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 match self {
33 Self::Unavailable => f.write_str("browser localStorage is unavailable"),
34 Self::Serialize(err) => write!(f, "could not serialize localStorage value: {err}"),
35 Self::Deserialize(err) => {
36 write!(f, "could not deserialize localStorage value: {err}")
37 }
38 Self::Browser(err) => write!(f, "browser localStorage error: {err}"),
39 }
40 }
41}
42
43impl std::error::Error for StorageError {
44 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
45 match self {
46 Self::Serialize(err) | Self::Deserialize(err) => Some(err),
47 Self::Unavailable | Self::Browser(_) => None,
48 }
49 }
50}
51
52#[derive(Clone, Debug)]
58pub struct LocalStorage<T> {
59 key: String,
60 _marker: PhantomData<fn() -> T>,
61}
62
63impl<T> LocalStorage<T> {
64 pub fn new(key: impl Into<String>) -> Self {
66 Self {
67 key: key.into(),
68 _marker: PhantomData,
69 }
70 }
71
72 pub fn key(&self) -> &str {
74 &self.key
75 }
76
77 pub fn remove(&self) -> Result<(), StorageError> {
79 remove_local_storage_item(&self.key)
80 }
81}
82
83impl<T> LocalStorage<T>
84where
85 T: DeserializeOwned,
86{
87 pub fn get(&self) -> Result<Option<T>, StorageError> {
93 let Some(value) = get_local_storage_item(&self.key)? else {
94 return Ok(None);
95 };
96 serde_json::from_str(&value)
97 .map(Some)
98 .map_err(StorageError::Deserialize)
99 }
100}
101
102impl<T> LocalStorage<T>
103where
104 T: Serialize,
105{
106 pub fn set(&self, value: &T) -> Result<(), StorageError> {
108 let value = serde_json::to_string(value).map_err(StorageError::Serialize)?;
109 set_local_storage_item(&self.key, &value)
110 }
111}
112
113#[cfg(target_arch = "wasm32")]
114fn get_local_storage_item(key: &str) -> Result<Option<String>, StorageError> {
115 storage()?
116 .get_item(key)
117 .map_err(|err| StorageError::Browser(js_error_message(err)))
118}
119
120#[cfg(not(target_arch = "wasm32"))]
121fn get_local_storage_item(_: &str) -> Result<Option<String>, StorageError> {
122 Err(StorageError::Unavailable)
123}
124
125#[cfg(target_arch = "wasm32")]
126fn set_local_storage_item(key: &str, value: &str) -> Result<(), StorageError> {
127 storage()?
128 .set_item(key, value)
129 .map_err(|err| StorageError::Browser(js_error_message(err)))
130}
131
132#[cfg(not(target_arch = "wasm32"))]
133fn set_local_storage_item(_: &str, _: &str) -> Result<(), StorageError> {
134 Err(StorageError::Unavailable)
135}
136
137#[cfg(target_arch = "wasm32")]
138fn remove_local_storage_item(key: &str) -> Result<(), StorageError> {
139 storage()?
140 .remove_item(key)
141 .map_err(|err| StorageError::Browser(js_error_message(err)))
142}
143
144#[cfg(not(target_arch = "wasm32"))]
145fn remove_local_storage_item(_: &str) -> Result<(), StorageError> {
146 Err(StorageError::Unavailable)
147}
148
149#[cfg(target_arch = "wasm32")]
150fn storage() -> Result<web_sys::Storage, StorageError> {
151 let window = web_sys::window().ok_or(StorageError::Unavailable)?;
152 window
153 .local_storage()
154 .map_err(|err| StorageError::Browser(js_error_message(err)))?
155 .ok_or(StorageError::Unavailable)
156}
157
158#[cfg(target_arch = "wasm32")]
159fn js_error_message(err: wasm_bindgen::JsValue) -> String {
160 err.as_string().unwrap_or_else(|| format!("{err:?}"))
161}
162
163#[cfg(all(test, not(target_arch = "wasm32")))]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn local_storage_reports_unavailable_on_host() {
169 let storage = LocalStorage::<String>::new("pocopine.test");
170 assert!(matches!(storage.get(), Err(StorageError::Unavailable)));
171 assert!(matches!(
172 storage.set(&"value".to_string()),
173 Err(StorageError::Unavailable)
174 ));
175 assert!(matches!(storage.remove(), Err(StorageError::Unavailable)));
176 }
177}