Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| use js_sys::JSON; | |
| use wasm_bindgen::prelude::*; | |
| /// Stores items into localstorage | |
| pub struct Store { | |
| local_storage: web_sys::Storage, | |
| data: ItemList, | |
| name: String, | |
| } | |
| impl Store { | |
| /// Creates a new store with `name` as the localstorage value name | |
| pub fn new(name: &str) -> Option<Store> { | |
| let window = web_sys::window()?; | |
| if let Ok(Some(local_storage)) = window.local_storage() { | |
| let mut store = Store { | |
| local_storage, | |
| data: ItemList::new(), | |
| name: String::from(name), | |
| }; | |
| store.fetch_local_storage(); | |
| Some(store) | |
| } else { | |
| None | |
| } | |
| } | |
| /// Read the local ItemList from localStorage. | |
| /// Returns an &Option<ItemList> of the stored database | |
| /// Caches the store into `self.data` to reduce calls to JS | |
| /// | |
| /// Uses mut here as the return is something we might want to manipulate | |
| /// | |
| fn fetch_local_storage(&mut self) -> Option<()> { | |
| let mut item_list = ItemList::new(); | |
| // If we have an existing cached value, return early. | |
| if let Ok(Some(value)) = self.local_storage.get_item(&self.name) { | |
| let data = JSON::parse(&value).ok()?; | |
| let iter = js_sys::try_iter(&data).ok()??; | |
| for item in iter { | |
| let item = item.ok()?; | |
| let item_array: &js_sys::Array = wasm_bindgen::JsCast::dyn_ref(&item)?; | |
| let title = item_array.shift().as_string()?; | |
| let completed = item_array.shift().as_bool()?; | |
| let id = item_array.shift().as_string()?; | |
| let temp_item = Item { | |
| title, | |
| completed, | |
| id, | |
| }; | |
| item_list.push(temp_item); | |
| } | |
| } | |
| self.data = item_list; | |
| Some(()) | |
| } | |
| /// Write the local ItemList to localStorage. | |
| fn sync_local_storage(&mut self) { | |
| let array = js_sys::Array::new(); | |
| for item in self.data.iter() { | |
| let child = js_sys::Array::new(); | |
| let s = item.title.clone(); | |
| child.push(&JsValue::from(&s)); | |
| child.push(&JsValue::from(item.completed)); | |
| child.push(&JsValue::from(item.id.to_string())); | |
| array.push(&JsValue::from(child)); | |
| } | |
| if let Ok(storage_string) = JSON::stringify(&JsValue::from(array)) { | |
| let storage_string: String = storage_string.to_string().into(); | |
| self.local_storage | |
| .set_item(&self.name, storage_string.as_str()) | |
| .unwrap(); | |
| } | |
| } | |
| /// Find items with properties matching those on query. | |
| /// `ItemQuery` query Query to match | |
| /// | |
| /// ``` | |
| /// let data = db.find(ItemQuery::Completed {completed: true}); | |
| /// // data will contain items whose completed properties are true | |
| /// ``` | |
| pub fn find(&mut self, query: ItemQuery) -> Option<ItemListSlice<'_>> { | |
| Some( | |
| self.data | |
| .iter() | |
| .filter(|todo| query.matches(*todo)) | |
| .collect(), | |
| ) | |
| } | |
| /// Update an item in the Store. | |
| /// | |
| /// `ItemUpdate` update Record with an id and a property to update | |
| pub fn update(&mut self, update: ItemUpdate) { | |
| let id = update.id(); | |
| self.data.iter_mut().for_each(|todo| { | |
| if id == todo.id { | |
| todo.update(&update); | |
| } | |
| }); | |
| self.sync_local_storage(); | |
| } | |
| /// Insert an item into the Store. | |
| /// | |
| /// `Item` item Item to insert | |
| pub fn insert(&mut self, item: Item) { | |
| self.data.push(item); | |
| self.sync_local_storage(); | |
| } | |
| /// Remove items from the Store based on a query. | |
| /// query is an `ItemQuery` query Query matching the items to remove | |
| pub fn remove(&mut self, query: ItemQuery) { | |
| self.data.retain(|todo| !query.matches(todo)); | |
| self.sync_local_storage(); | |
| } | |
| /// Count total, active, and completed todos. | |
| pub fn count(&mut self) -> Option<(usize, usize, usize)> { | |
| self.find(ItemQuery::EmptyItemQuery).map(|data| { | |
| let total = data.length(); | |
| let mut completed = 0; | |
| for item in data.iter() { | |
| if item.completed { | |
| completed += 1; | |
| } | |
| } | |
| (total, total - completed, completed) | |
| }) | |
| } | |
| } | |
| /// Represents a todo item | |
| pub struct Item { | |
| pub id: String, | |
| pub title: String, | |
| pub completed: bool, | |
| } | |
| impl Item { | |
| pub fn update(&mut self, update: &ItemUpdate) { | |
| match update { | |
| ItemUpdate::Title { title, .. } => { | |
| self.title = title.to_string(); | |
| } | |
| ItemUpdate::Completed { completed, .. } => { | |
| self.completed = *completed; | |
| } | |
| } | |
| } | |
| } | |
| pub trait ItemListTrait<T> { | |
| fn new() -> Self; | |
| fn get(&self, i: usize) -> Option<&T>; | |
| fn length(&self) -> usize; | |
| fn push(&mut self, item: T); | |
| fn iter(&self) -> std::slice::Iter<'_, T>; | |
| } | |
| pub struct ItemList { | |
| list: Vec<Item>, | |
| } | |
| impl ItemList { | |
| fn retain<F>(&mut self, f: F) | |
| where | |
| F: FnMut(&Item) -> bool, | |
| { | |
| self.list.retain(f); | |
| } | |
| fn iter_mut(&mut self) -> std::slice::IterMut<'_, Item> { | |
| self.list.iter_mut() | |
| } | |
| } | |
| impl ItemListTrait<Item> for ItemList { | |
| fn new() -> ItemList { | |
| ItemList { list: Vec::new() } | |
| } | |
| fn get(&self, i: usize) -> Option<&Item> { | |
| self.list.get(i) | |
| } | |
| fn length(&self) -> usize { | |
| self.list.len() | |
| } | |
| fn push(&mut self, item: Item) { | |
| self.list.push(item) | |
| } | |
| fn iter(&self) -> std::slice::Iter<'_, Item> { | |
| self.list.iter() | |
| } | |
| } | |
| use std::iter::FromIterator; | |
| impl<'a> FromIterator<Item> for ItemList { | |
| fn from_iter<I: IntoIterator<Item = Item>>(iter: I) -> Self { | |
| let mut c = ItemList::new(); | |
| for i in iter { | |
| c.push(i); | |
| } | |
| c | |
| } | |
| } | |
| /// A borrowed set of Items filtered from the store | |
| pub struct ItemListSlice<'a> { | |
| list: Vec<&'a Item>, | |
| } | |
| impl<'a> ItemListTrait<&'a Item> for ItemListSlice<'a> { | |
| fn new() -> ItemListSlice<'a> { | |
| ItemListSlice { list: Vec::new() } | |
| } | |
| fn get(&self, i: usize) -> Option<&&'a Item> { | |
| self.list.get(i) | |
| } | |
| fn length(&self) -> usize { | |
| self.list.len() | |
| } | |
| fn push(&mut self, item: &'a Item) { | |
| self.list.push(item) | |
| } | |
| fn iter(&self) -> std::slice::Iter<'_, &'a Item> { | |
| self.list.iter() | |
| } | |
| } | |
| impl<'a> FromIterator<&'a Item> for ItemListSlice<'a> { | |
| fn from_iter<I: IntoIterator<Item = &'a Item>>(iter: I) -> Self { | |
| let mut c = ItemListSlice::new(); | |
| for i in iter { | |
| c.push(i); | |
| } | |
| c | |
| } | |
| } | |
| impl<'a> Into<ItemList> for ItemListSlice<'a> { | |
| fn into(self) -> ItemList { | |
| let mut i = ItemList::new(); | |
| let items = self.list.into_iter(); | |
| for j in items { | |
| // TODO neaten this cloning? | |
| let item = Item { | |
| id: j.id.clone(), | |
| completed: j.completed, | |
| title: j.title.clone(), | |
| }; | |
| i.push(item); | |
| } | |
| i | |
| } | |
| } | |
| /// Represents a search into the store | |
| pub enum ItemQuery { | |
| Id { id: String }, | |
| Completed { completed: bool }, | |
| EmptyItemQuery, | |
| } | |
| impl ItemQuery { | |
| fn matches(&self, item: &Item) -> bool { | |
| match *self { | |
| ItemQuery::EmptyItemQuery => true, | |
| ItemQuery::Id { ref id } => &item.id == id, | |
| ItemQuery::Completed { completed } => item.completed == completed, | |
| } | |
| } | |
| } | |
| pub enum ItemUpdate { | |
| Title { id: String, title: String }, | |
| Completed { id: String, completed: bool }, | |
| } | |
| impl ItemUpdate { | |
| fn id(&self) -> String { | |
| match self { | |
| ItemUpdate::Title { id, .. } => id.clone(), | |
| ItemUpdate::Completed { id, .. } => id.clone(), | |
| } | |
| } | |
| } |