1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
//! Koit is a simple, asynchronous, pure-Rust, structured, embedded database. //! //! # Examples //! //! ``` //! use std::default::Default; //! //! use koit::{FileDatabase, format::Json}; //! use serde::{Deserialize, Serialize}; //! //! #[derive(Default, Deserialize, Serialize)] //! struct Data { //! cats: u64, //! yaks: u64, //! } //! //! #[tokio::main] //! async fn main() -> Result<(), Box<dyn std::error::Error>> { //! let db = FileDatabase::<Data, Json>::load_from_path_or_default("./db.json").await?; //! //! db.write(|data| { //! data.cats = 10; //! data.yaks = 32; //! }).await; //! //! assert_eq!(db.read(|data| data.cats + data.yaks).await, 42); //! //! db.save().await?; //! //! Ok(()) //! } //! ``` //! //! # Features //! //! Koit comes with a [file-backed database](crate::FileDatabase) and [JSON](crate::format::Json) //! and [Bincode](crate::format::Bincode) formatters. You can also define your own storage //! [format](crate::format) or [backend](crate::backend). //! //! Note that the file-backed database requires the Tokio 0.3 runtime to function. #![cfg_attr(docsrs, feature(doc_cfg))] use std::future::Future; use std::marker::PhantomData; use tokio::sync::{Mutex, RwLock}; mod error; pub use error::KoitError; pub mod backend; pub use backend::Backend; pub mod format; pub use format::Format; /// The Koit database. /// /// The database provides reading, writing, saving and reloading functionality. /// It uses a reader-writer lock on the internal data structure, allowing /// concurrent access by readers, while writers are given exclusive access. /// /// It requires a [`Format`](crate::format::Format) marker type #[derive(Debug)] pub struct Database<D, B, F> { data: RwLock<D>, backend: Mutex<B>, _format: PhantomData<F>, } impl<D, B, F> Database<D, B, F> where B: Backend, F: Format<D>, { /// Create a database from its constituents. pub fn from_parts(data: D, backend: B) -> Self { Self { data: RwLock::new(data), backend: Mutex::new(backend), _format: PhantomData, } } /// Write to the data contained in the database. This gives exclusive access to the underlying /// data structure. The value your closure returns will be passed on as the return value of this /// function. /// /// This write-locks the data structure. pub async fn write<T, R>(&self, task: T) -> R where T: FnOnce(&mut D) -> R, { let mut data = self.data.write().await; task(&mut data) } /// Same as [`crate::Database::write`], except the task returns a future. pub async fn write_and_then<T, Fut, R>(&self, task: T) -> R where T: FnOnce(&mut D) -> Fut, Fut: Future<Output = R>, { let mut data = self.data.write().await; task(&mut data).await } /// Read the data contained in the database. Many readers can read in parallel. /// The value your closure returns will be passed on as the return value of this function. /// /// This read-locks the data structure. pub async fn read<T, R>(&self, task: T) -> R where T: FnOnce(&D) -> R, { let data = self.data.read().await; task(&data) } /// Same as [`crate::Database::read`], except the task returns a future. pub async fn read_and_then<T, Fut, R>(&self, task: T) -> R where T: FnOnce(&D) -> Fut, Fut: Future<Output = R>, { let data = self.data.read().await; task(&data).await } /// Replace the actual data in the database by the given data in the parameter, returning the /// old data. /// /// This write-locks the data structure. pub async fn replace(&self, data: D) -> D { self.write(|actual_data| std::mem::replace(actual_data, data)) .await } /// Returns a reference to the underlying data lock. /// /// It is recommended to use the `read` and `write` methods instead of this, to ensure /// locks are only held for as long as needed. /// /// # Examples /// /// ``` /// use koit::{Database, format::Json, backend::Memory}; /// /// type Messages = Vec<String>; /// let db: Database<_, _, Json> = Database::from_parts(1, Memory::default()); /// /// futures::executor::block_on(async move { /// let data_lock = db.get_data_lock(); /// let mut data = data_lock.write().await; /// *data = 42; /// drop(data); /// /// db.read(|n| assert_eq!(*n, 42)).await; /// }); /// ``` pub fn get_data_lock(&self) -> &RwLock<D> { &self.data } /// Returns a mutable reference to the underlying data. /// /// This borrows `Database` mutably; no locking takes place. /// /// # Examples /// /// ``` /// use koit::{Database, format::Json, backend::Memory}; /// /// let mut db: Database<_, _, Json> = Database::from_parts(1, Memory::default()); /// /// let n = db.get_data_mut(); /// *n += 41; /// /// futures::executor::block_on(db.read(|n| assert_eq!(*n, 42))); /// ``` pub fn get_data_mut(&mut self) -> &mut D { self.data.get_mut() } /// Flush the data contained in the database to the backend. /// /// This read-locks the data structure. /// /// # Errors /// /// - If the data in the database failed to be encoded by the format, an error variant is returned. /// - If the bytes failed to be written to the backend, an error variant is returned. This may mean /// the backend is now corrupted. /// /// # Panics /// /// Some back-ends (such as [`crate::backend::File`]) might panic on some async runtimes. pub async fn save(&self) -> Result<(), KoitError> { let mut backend = self.backend.lock().await; let data = self.data.read().await; backend .write(F::to_bytes(&data).map_err(|err| KoitError::ToFormat(err.into()))?) .await .map_err(|err| KoitError::BackendWrite(err.into()))?; Ok(()) } /// Load data from the backend. async fn load_from_backend(&self) -> Result<D, KoitError> { let mut backend = self.backend.lock().await; let bytes = backend .read() .await .map_err(|err| KoitError::BackendRead(err.into()))?; Ok(F::from_bytes(bytes).map_err(|err| KoitError::FromFormat(err.into()))?) } /// Update this database with data from the backend, returning the old data. /// /// This will write-lock the internal data structure. /// /// # Errors /// /// - If the bytes from teh backend failed to be decoded by the format, an error variant is returned. /// - If the bytes failed to be read by the backend, an error variant is returned. /// /// # Panics /// /// Some back-ends (such as [`crate::backend::File`]) might panic on some async runtimes. pub async fn reload(&self) -> Result<D, KoitError> { let new_data = self.load_from_backend().await?; Ok(self.replace(new_data).await) } /// Consume the database and return its data and backend. pub fn into_parts(self) -> (D, B) { (self.data.into_inner(), self.backend.into_inner()) } } /// A file-backed database. /// /// Note: this requires its futures to be executed on the Tokio 0.3 runtime. #[cfg(feature = "file-backend")] #[cfg_attr(docsrs, doc(cfg(feature = "file-backend")))] pub type FileDatabase<D, F> = Database<D, backend::File, F>; #[cfg(feature = "file-backend")] impl<D, F> FileDatabase<D, F> where F: Format<D>, { /// Construct the file-backed database from the given path. This attempts to load data /// from the given file. /// /// # Errors /// If the file cannot be read, or the [formatter](crate::format::Format) cannot decode the data, /// an error variant will be returned. pub async fn load_from_path<P>(path: P) -> Result<Self, KoitError> where P: AsRef<std::path::Path>, { let mut backend = backend::File::from_path(path) .await .map_err(|err| KoitError::BackendCreation(err.into()))?; let bytes = backend .read() .await .map_err(|err| KoitError::BackendRead(err.into()))?; let data = F::from_bytes(bytes).map_err(|err| KoitError::FromFormat(err.into()))?; Ok(Database { data: RwLock::new(data), backend: Mutex::new(backend), _format: PhantomData, }) } /// Construct the file-backed database from the given path. If the file does not exist, /// the file is created. Then `factory` is called and its return value is used as the initial value. /// This data is immediately and saved to file. pub async fn load_from_path_or_else<P, T>(path: P, factory: T) -> Result<Self, KoitError> where P: AsRef<std::path::Path>, T: FnOnce() -> D, { let (mut backend, exists) = backend::File::from_path_or_create(path) .await .map_err(|e| KoitError::BackendCreation(e.into()))?; let data = if exists { let bytes = backend .read() .await .map_err(|err| KoitError::BackendRead(err.into()))?; F::from_bytes(bytes).map_err(|err| KoitError::FromFormat(err.into()))? } else { factory() }; let db = Database { data: RwLock::new(data), backend: Mutex::new(backend), _format: PhantomData, }; db.save().await?; Ok(db) } /// Same as `load_from_path_or_else`, except it uses [`Default`](`std::default::Default`) instead of a factory. pub async fn load_from_path_or_default<P>(path: P) -> Result<Self, KoitError> where P: AsRef<std::path::Path>, D: std::default::Default, { Self::load_from_path_or_else(path, || std::default::Default::default()).await } }