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