use crate::store::StoreError; use chrono::{DateTime, Utc}; use sled::{Db, Transactional, Tree}; use uuid::Uuid; #[derive(Clone, Debug)] pub struct Server { id: Uuid, domain: String, created: DateTime, title: Option, title_source: Option, description: Option, description_source: Option, } impl Server { pub fn id(&self) -> Uuid { self.id } pub fn domain(&self) -> &str { &self.domain } pub fn title(&self) -> Option<&str> { self.title.as_deref() } pub fn title_source(&self) -> Option<&str> { self.title_source.as_deref() } pub fn description(&self) -> Option<&str> { self.description.as_deref() } pub fn description_source(&self) -> Option<&str> { self.description_source.as_deref() } pub fn created(&self) -> DateTime { self.created } pub(crate) fn changes(&self) -> ServerChanges { ServerChanges { id: self.id, title: None, title_source: None, description: None, description_source: None, } } } pub struct ServerChanges { id: Uuid, title: Option, title_source: Option, description: Option, description_source: Option, } impl ServerChanges { pub(crate) fn title(&mut self, title: &str) -> &mut Self { self.title = Some(title.to_owned()); self } pub(crate) fn title_source(&mut self, title_source: &str) -> &mut Self { self.title_source = Some(title_source.to_owned()); self } pub(crate) fn description(&mut self, description: &str) -> &mut Self { self.description = Some(description.to_owned()); self } pub(crate) fn description_source(&mut self, description_source: &str) -> &mut Self { self.description_source = Some(description_source.to_owned()); self } pub(crate) fn any_changes(&self) -> bool { self.title.is_some() || self.description.is_some() } } #[derive(Clone)] pub struct Store { created_id: Tree, domain_id: Tree, id_domain: Tree, id_created: Tree, id_title: Tree, id_title_source: Tree, id_description: Tree, id_description_source: Tree, self_server: Tree, } impl Store { pub(super) fn build(db: &Db) -> Result { let created_id = db.open_tree("/profiles/servers/created_id")?; let domain_id = db.open_tree("/profiles/servers/domain_id")?; let id_domain = db.open_tree("/profiles/servers/id_domain")?; let id_created = db.open_tree("/profiles/servers/id_created")?; let id_title = db.open_tree("/profiles/servers/id_title")?; let id_title_source = db.open_tree("/profiles/servers/id_title_source")?; let id_description = db.open_tree("/profiles/servers/id_description")?; let id_description_source = db.open_tree("/profiles/servers/id_description_source")?; let self_server = db.open_tree("/profiles/servers/self_server")?; Ok(Store { created_id, domain_id, id_domain, id_created, id_title, id_title_source, id_description, id_description_source, self_server, }) } pub(crate) fn self_exists(&self) -> bool { self.self_server.iter().next().is_some() } pub(crate) fn set_self(&self, id: Uuid) -> Result<(), StoreError> { self.self_server.insert("self", id.as_bytes())?; Ok(()) } pub(crate) fn is_self(&self, id: Uuid) -> Result { Ok(self .self_server .get("self")? .and_then(uuid_from_ivec) .map(|self_id| self_id == id) .unwrap_or(false)) } pub(crate) fn create( &self, domain: &str, created: DateTime, ) -> Result { let mut id; while { id = Uuid::new_v4(); self.id_domain .compare_and_swap( id.as_bytes(), None as Option<&[u8]>, Some(domain.as_bytes()), )? .is_err() } {} let domain_clone = domain.to_owned(); let res = [&self.created_id, &self.domain_id, &self.id_created].transaction(move |trees| { let created_id = &trees[0]; let domain_id = &trees[1]; let id_created = &trees[2]; created_id.insert(created.to_rfc3339().as_bytes(), id.as_bytes())?; domain_id.insert(domain_clone.as_bytes(), id.as_bytes())?; id_created.insert(id.as_bytes(), created.to_rfc3339().as_bytes())?; Ok(()) }); if let Err(e) = res { self.id_domain.remove(id.as_bytes())?; return Err(e.into()); } Ok(Server { id, domain: domain.to_owned(), title: None, title_source: None, description: None, description_source: None, created, }) } pub(crate) fn update(&self, changes: &ServerChanges) -> Result { let domain = match self .id_domain .get(changes.id.as_bytes())? .map(string_from_ivec) { Some(domain) => domain, None => return Err(StoreError::Missing), }; let created = match self .id_created .get(changes.id.as_bytes())? .and_then(date_from_ivec) { Some(date) => date, None => return Err(StoreError::Missing), }; if let Some(title) = &changes.title { self.id_title .insert(changes.id.as_bytes(), title.as_bytes())?; } if let Some(title_source) = &changes.title_source { self.id_title_source .insert(changes.id.as_bytes(), title_source.as_bytes())?; } if let Some(description) = &changes.description { self.id_description .insert(changes.id.as_bytes(), description.as_bytes())?; } if let Some(description_source) = &changes.description_source { self.id_description_source .insert(changes.id.as_bytes(), description_source.as_bytes())?; } Ok(Server { id: changes.id, domain, created, title: changes.title.clone(), title_source: changes.title_source.clone(), description: changes.description.clone(), description_source: changes.description_source.clone(), }) } pub fn get_self(&self) -> Result, StoreError> { Ok(self.self_server.get("self")?.and_then(uuid_from_ivec)) } pub fn by_id(&self, id: Uuid) -> Result, StoreError> { let domain = self.id_domain.get(id.as_bytes())?.map(string_from_ivec); let created = self.id_created.get(id.as_bytes())?.and_then(date_from_ivec); let title = self.id_title.get(id.as_bytes())?.map(string_from_ivec); let title_source = self .id_title_source .get(id.as_bytes())? .map(string_from_ivec); let description = self .id_description .get(id.as_bytes())? .map(string_from_ivec); let description_source = self .id_description_source .get(id.as_bytes())? .map(string_from_ivec); Ok(domain.and_then(|domain| { created.map(|created| Server { id, domain, title, title_source, description, description_source, created, }) })) } pub fn by_domain(&self, domain: &str) -> Result, StoreError> { Ok(self.domain_id.get(domain)?.and_then(uuid_from_ivec)) } pub fn known(&self) -> impl DoubleEndedIterator { self.created_id .iter() .values() .filter_map(|res| res.ok()) .filter_map(uuid_from_ivec) } } fn string_from_ivec(ivec: sled::IVec) -> String { String::from_utf8_lossy(&ivec).to_string() } fn uuid_from_ivec(ivec: sled::IVec) -> Option { Uuid::from_slice(&ivec).ok() } fn date_from_ivec(ivec: sled::IVec) -> Option> { String::from_utf8_lossy(&ivec).parse().ok() }