diff options
author | Vika <vika@fireburn.ru> | 2023-06-15 17:02:39 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2023-06-15 17:02:39 +0300 |
commit | eca7687439c2b6f804603de75501b6737a82e5a2 (patch) | |
tree | a4715e0b8d3d63ee8b77830670c473b83a70031e /kittybox-rs/src/database | |
parent | 59f3e2d43d30642e4242039ce3ab934961e69602 (diff) | |
download | kittybox-eca7687439c2b6f804603de75501b6737a82e5a2.tar.zst |
Database: use newtypes to represent settings
This allows much for a cleaner and idiomatic settings interface.
Diffstat (limited to 'kittybox-rs/src/database')
-rw-r--r-- | kittybox-rs/src/database/file/mod.rs | 31 | ||||
-rw-r--r-- | kittybox-rs/src/database/memory.rs | 7 | ||||
-rw-r--r-- | kittybox-rs/src/database/mod.rs | 92 |
3 files changed, 91 insertions, 39 deletions
diff --git a/kittybox-rs/src/database/file/mod.rs b/kittybox-rs/src/database/file/mod.rs index bafb7bf..9319b67 100644 --- a/kittybox-rs/src/database/file/mod.rs +++ b/kittybox-rs/src/database/file/mod.rs @@ -1,5 +1,5 @@ //#![warn(clippy::unwrap_used)] -use crate::database::{filter_post, ErrorKind, Result, Settings, Storage, StorageError}; +use crate::database::{filter_post, ErrorKind, Result, settings, Storage, StorageError}; use async_trait::async_trait; use futures::{stream, StreamExt, TryStreamExt}; use serde_json::json; @@ -582,7 +582,7 @@ impl Storage for FileStorage { } #[tracing::instrument(skip(self))] - async fn get_setting(&self, setting: Settings, user: &'_ str) -> Result<String> { + async fn get_setting<S: settings::Setting<'a>, 'a>(&self, user: &'_ str) -> Result<S> { debug!("User for getting settings: {}", user); let url = axum::http::Uri::try_from(user).expect("Couldn't parse a URL"); let mut path = relative_path::RelativePathBuf::new(); @@ -591,22 +591,20 @@ impl Storage for FileStorage { let path = path.to_path(&self.root_dir); debug!("Getting settings from {:?}", &path); - let setting = setting.to_string(); + let mut file = File::open(path).await?; let mut content = String::new(); file.read_to_string(&mut content).await?; - let settings: HashMap<String, String> = serde_json::from_str(&content)?; - // XXX consider returning string slices instead of cloning a string every time - // it might come with a performance hit and/or memory usage inflation - settings - .get(&setting) - .cloned() - .ok_or_else(|| StorageError::new(ErrorKind::Backend, "Setting not set")) + let settings: HashMap<&str, serde_json::Value> = serde_json::from_str(&content)?; + match settings.get(S::ID) { + Some(value) => Ok(serde_json::from_value::<S>(value.clone())?), + None => Err(StorageError::new(ErrorKind::Backend, "Setting not set")) + } } #[tracing::instrument(skip(self))] - async fn set_setting(&self, setting: Settings, user: &'_ str, value: &'_ str) -> Result<()> { + async fn set_setting<S: settings::Setting<'a>, 'a>(&self, user: &'_ str, value: S::Data) -> Result<()> { let url = axum::http::Uri::try_from(user).expect("Couldn't parse a URL"); let mut path = relative_path::RelativePathBuf::new(); path.push(url.authority().unwrap().to_string()); @@ -620,33 +618,32 @@ impl Storage for FileStorage { tokio::fs::create_dir_all(path.parent().unwrap()).await?; } - let (setting, value) = (setting.to_string(), value.to_string()); - let mut tempfile = OpenOptions::new() .write(true) .create_new(true) .open(&temppath) .await?; - let mut settings: HashMap<String, String> = match File::open(&path).await { + let mut settings: HashMap<String, serde_json::Value> = match File::open(&path).await { Ok(mut f) => { let mut content = String::new(); f.read_to_string(&mut content).await?; if content.is_empty() { - HashMap::default() + Default::default() } else { serde_json::from_str(&content)? } } Err(err) => { if err.kind() == IOErrorKind::NotFound { - HashMap::default() + Default::default() } else { return Err(err.into()); } } }; - settings.insert(setting, value); + settings.insert(S::ID.to_owned(), serde_json::to_value(S::new(value))?); + tempfile .write_all(serde_json::to_string(&settings)?.as_bytes()) .await?; diff --git a/kittybox-rs/src/database/memory.rs b/kittybox-rs/src/database/memory.rs index c8cc125..2fb55e5 100644 --- a/kittybox-rs/src/database/memory.rs +++ b/kittybox-rs/src/database/memory.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -use crate::database::{ErrorKind, MicropubChannel, Result, Settings, Storage, StorageError}; +use crate::database::{ErrorKind, MicropubChannel, Result, settings, Storage, StorageError}; #[derive(Clone, Debug)] pub struct MemoryStorage { @@ -244,14 +244,15 @@ impl Storage for MemoryStorage { } #[allow(unused_variables)] - async fn get_setting(&self, setting: Settings, user: &'_ str) -> Result<String> { + async fn get_setting<S: settings::Setting<'a>, 'a>(&'_ self, user: &'_ str) -> Result<S> { todo!() } #[allow(unused_variables)] - async fn set_setting(&self, setting: Settings, user: &'_ str, value: &'_ str) -> Result<()> { + async fn set_setting<S: settings::Setting<'a>, 'a>(&self, user: &'_ str, value: S::Data) -> Result<()> { todo!() } + } impl Default for MemoryStorage { diff --git a/kittybox-rs/src/database/mod.rs b/kittybox-rs/src/database/mod.rs index 24cff38..2039ac0 100644 --- a/kittybox-rs/src/database/mod.rs +++ b/kittybox-rs/src/database/mod.rs @@ -1,6 +1,5 @@ #![warn(missing_docs)] use async_trait::async_trait; -use serde::{Deserialize, Serialize}; mod file; pub use crate::database::file::FileStorage; @@ -11,6 +10,8 @@ pub use crate::database::memory::MemoryStorage; pub use kittybox_util::MicropubChannel; +use self::settings::Setting; + /// Enum representing different errors that might occur during the database query. #[derive(Debug, Clone, Copy)] pub enum ErrorKind { @@ -33,17 +34,68 @@ pub enum ErrorKind { Other, } -/// Enum representing settings that might be stored in the site's database. -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -#[serde(rename_all = "snake_case")] -pub enum Settings { - /// The name of the website -- displayed in the header and the browser titlebar. - SiteName, -} +/// Settings that can be stored in the database. +pub mod settings { + mod private { + pub trait Sealed {} + } + /*#[derive(serde::Deserialize, serde::Serialize, Default)] + pub struct Settings { + pub site_name: SiteName, + pub webring: Webring + } + impl From<Settings> for SiteName { + fn from(settings: Settings) -> Self { + settings.site_name + } + }*/ + + /// A trait for various settings that should be contained here. + /// + /// **Note**: this trait is sealed to prevent external + /// implementations, as it wouldn't make sense to add new settings + /// that aren't used by Kittybox itself. + pub trait Setting<'de>: private::Sealed + std::fmt::Debug + Default + Clone + serde::Serialize + serde::de::DeserializeOwned + /*From<Settings> +*/ Send + Sync { + type Data: std::fmt::Debug + Send + Sync; + const ID: &'static str; + + /// Unwrap the setting type, returning owned data contained within. + fn into_inner(self) -> Self::Data; + /// Create a new instance of this type containing certain data. + fn new(data: Self::Data) -> Self; + } + + /// A website's title, shown in the header. + #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)] + pub struct SiteName(String); + impl Default for SiteName { + fn default() -> Self { + Self("Kittybox".to_string()) + } + } + impl AsRef<str> for SiteName { + fn as_ref(&self) -> &str { + self.0.as_str() + } + } + impl private::Sealed for SiteName {} + impl Setting<'_> for SiteName { + type Data = String; + const ID: &'static str = "site_name"; + + fn into_inner(self) -> String { + self.0 + } + fn new(data: Self::Data) -> Self { + Self(data) + } + } + impl SiteName { + fn from_str(data: &str) -> Self { + Self(data.to_owned()) + } + } -impl std::string::ToString for Settings { - fn to_string(&self) -> String { - serde_variant::to_variant_name(self).unwrap().to_string() } } @@ -248,14 +300,16 @@ pub trait Storage: std::fmt::Debug + Clone + Send + Sync { async fn delete_post(&self, url: &'_ str) -> Result<()>; /// Gets a setting from the setting store and passes the result. - async fn get_setting(&self, setting: Settings, user: &'_ str) -> Result<String>; + async fn get_setting<S: Setting<'a>, 'a>(&'_ self, user: &'_ str) -> Result<S>; /// Commits a setting to the setting store. - async fn set_setting(&self, setting: Settings, user: &'_ str, value: &'_ str) -> Result<()>; + async fn set_setting<S: Setting<'a>, 'a>(&self, user: &'_ str, value: S::Data) -> Result<()>; } #[cfg(test)] mod tests { + use super::settings; + use super::{MicropubChannel, Storage}; use serde_json::json; @@ -413,18 +467,18 @@ mod tests { async fn test_settings<Backend: Storage>(backend: Backend) { backend - .set_setting( - crate::database::Settings::SiteName, + .set_setting::<settings::SiteName>( "https://fireburn.ru/", - "Vika's Hideout", + "Vika's Hideout".to_owned() ) .await .unwrap(); assert_eq!( backend - .get_setting(crate::database::Settings::SiteName, "https://fireburn.ru/") - .await - .unwrap(), + .get_setting::<settings::SiteName>("https://fireburn.ru/") + .await + .unwrap() + .as_ref(), "Vika's Hideout" ); } |