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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
//! > NOTE: This project is not affiliated with the Python [TinyDB](https://tinydb.readthedocs.io/en/latest/),
//! accidental naming error from when this project was started. See
//! [renaming](https://github.com/scOwez/tinydb/issues/3) for updates
//! 
//! TinyDB or `tinydb` is a small-footprint, superfast database designed to be
//! used in-memory and easily dumped/retrieved from a file when it's time to save
//! ✨
//! 
//! This database aims to provide an easy frontend to an efficiant in-memory
//! database (that can also be dumped to a file). It purposefully disallows
//! duplicate items to be sorted due to constraints with hash tables.
//! 
//! ## Example 🚀
//! 
//! A simple example of adding a structure then querying for it:
//! 
//! ```rust
//! use serde::{Serialize, Deserialize};
//! use tinydb::Database;
//! 
//! #[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Clone)]
//! struct ExampleStruct {
//!     my_age: i32
//! }
//! 
//! fn main() {
//!     let my_struct = ExampleStruct { my_age: 329 };
//!     let mut my_db = Database::new("query_test", None, false);
//! 
//!     my_db.add_item(my_struct.clone());
//! 
//!     let results = my_db.query_item(|s: &ExampleStruct| &s.my_age, 329);
//! 
//!     assert_eq!(results.unwrap(), &my_struct);
//! }
//! ```
//! 
//! # Installation
//! 
//! Simply add the following to your `Cargo.toml` file:
//! 
//! ```toml
//! [dependencies]
//! tinydb = "1"
//! ```
//! # Implementation notes
//!
//! - This database does not save 2 duplicated items, either ignoring or raising an
//! error depending on end-user preference.
//! - This project is not intended to be used inside of any critical systems due to
//! the nature of dumping/recovery. If you are using this crate as a temporary and
//! in-memory only database, it should preform at a reasonable speed (as it uses
//! [HashSet] underneath).
//!
//! # Essential operations
//!
//! Some commonly-used operations for the [Database] structure.
//!
//! | Operation                               | Implamentation          |
//! |-----------------------------------------|-------------------------|
//! | Create database                         | [Database::new]         |
//! | Create database from file               | [Database::from]        |
//! | Load database or create if non-existant | [Database::auto_from]   |
//! | Query for item                          | [Database::query_item]  |
//! | Contains specific item                  | [Database::contains]    |
//! | Update/replace item                     | [Database::update_item] |
//! | Delete item                             | [Database::remove_item] |
//! | Dump database                           | [Database::dump_db]     |

#![doc(
    html_logo_url = "https://github.com/Owez/tinydb/raw/master/logo.png",
    html_favicon_url = "https://github.com/Owez/tinydb/raw/master/logo.png"
)]

use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::File;
use std::hash;
use std::io::prelude::*;
use std::path::PathBuf;

pub mod error;

/// The primary database structure, allowing storage of a generic type with
/// dumping/saving options avalible.
///
/// The generic type used should primarily be structures as they resemble a
/// conventional database model and should implament [hash::Hash] and [Eq] for
/// basic in-memory storage with [Serialize] and [Deserialize] being implamented
/// for file operations involving the database (these are also required).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Database<T: hash::Hash + Eq> {
    /// Friendly name for the database, preferibly in `slug-form-like-this` as
    /// this is the fallback path
    ///
    /// This is used when dumping the database without a [Database::save_path]
    /// being defined and a friendly way to order a database
    pub label: String,

    /// The overwrite path to save the database as, this is recommended otherwise
    /// it will end up as `./Hello\ There.tinydb` if [Database::label] is "Hello
    /// There"
    ///
    /// Primarily used inside of [Database::dump_db].
    pub save_path: Option<PathBuf>,

    /// If the database should return an error if it tries to insert where an
    /// identical item already is. Setting this as `false` doesn't allow
    /// duplicates, it just doesn't flag an error.
    pub strict_dupes: bool,

    /// In-memory [HashSet] of all items
    pub items: HashSet<T>,
}

impl<T: hash::Hash + Eq + Serialize + DeserializeOwned> Database<T> {
    /// Creates a new database instance from given parameters.
    ///
    /// - To add a first item, use [Database::add_item].
    /// - If you'd like to load a dumped database, use [Database::from].
    pub fn new(label: impl Into<String>, save_path: impl Into<Option<PathBuf>>, strict_dupes: bool) -> Self {
        Database {
            label: label.into(),
            save_path: save_path.into(),
            strict_dupes,
            items: HashSet::new(),
        }
    }

    /// Creates a database from a `.tinydb` file.
    ///
    /// This retrives a dump file (saved database) from the path given and loads
    /// it as the [Database] structure.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tinydb::Database;
    /// use serde::{Serialize, Deserialize};
    /// use std::path::PathBuf;
    ///
    /// /// Small example structure to show.
    /// #[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
    /// struct ExampleStruct {
    ///    data: i32
    /// }
    ///
    /// /// Makes a small testing database.
    /// fn make_db() {
    ///     let mut test_db = Database::new("test", None, false);
    ///     test_db.add_item(ExampleStruct { data: 34 });
    ///     test_db.dump_db();
    /// }
    ///
    /// /// Get `test_db` defined in [make_db] and test.
    /// fn main() {
    ///     make_db();
    ///
    ///     let got_db = Database::from(
    ///         PathBuf::from("test.tinydb")
    ///     ).unwrap();
    ///
    ///     assert_eq!(
    ///         got_db.query_item(|s: &ExampleStruct| &s.data, 34).unwrap(),
    ///         &ExampleStruct { data: 34 }
    ///     ); // Check that the database still has added [ExampleStruct].
    /// }
    /// ```
    pub fn from(path: impl Into<PathBuf>) -> Result<Self, error::DatabaseError> {
        let stream = get_stream_from_path(path.into())?;
        let decoded: Database<T> = bincode::deserialize(&stream[..]).unwrap();

        Ok(decoded)
    }

    /// Loads database from existant path or creates a new one if it doesn't already
    /// exist.
    ///
    /// This is the recommended way to use TinyDB if you are wanting to easily
    /// setup an entire database instance in a short, consise manner. Similar to
    /// [Database::new] and [Database::from], this function will also have to be
    /// given a strict type argument and you will still have to provide `script_dupes`
    /// even if the database is likely to load an existing one.
    ///
    /// This function does make some assumptions about the database name and uses
    /// the 2nd to last part before a `.`. This means that `x.y.z` will have the
    /// name of `y`, not `x` so therefore it is recommended to have a database
    /// path with `x.tinydb` or `x.db` only.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tinydb::*;
    /// use std::path::PathBuf;
    /// use serde::{Serialize, Deserialize};
    ///
    /// /// Small example structure to show.
    /// #[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
    /// struct ExampleStruct {
    ///    data: i32
    /// }
    ///
    /// fn main() {
    ///     let dummy_db: Database<ExampleStruct> = Database::new("cool", None, false); // create demo db for `db_from`
    ///
    ///     let db_from_path = PathBuf::from("cool.tinydb");
    ///     let db_from: Database<ExampleStruct> = Database::auto_from(db_from_path, false).unwrap(); // automatically load it
    ///
    ///     let db_new_path = PathBuf::from("xyz.tinydb");
    ///     let db_new: Database<ExampleStruct> = Database::auto_from(db_new_path, false).unwrap(); // automatically create new as "xyz" doesn't exist
    /// }
    /// ```
    pub fn auto_from(path: impl Into<PathBuf>, strict_dupes: bool) -> Result<Self, error::DatabaseError> {
        let path_into = path.into();
        
        if path_into.exists() {
            Database::from(path_into)
        } else {
            let db_name = match path_into.file_stem() {
                Some(x) => match x.to_str() {
                    Some(y) => String::from(y),
                    None => return Err(error::DatabaseError::BadDbName),
                },
                None => return Err(error::DatabaseError::BadDbName),
            };

            Ok(Database::new(db_name, Some(path_into), strict_dupes))
        }
    }

    /// Adds a new item to the in-memory database.
    ///
    /// If this is the first item added to the database, please ensure it's the
    /// only type you'd like to add. Due to generics, the first item you add
    /// will be set as the type to use (unless removed).
    pub fn add_item(&mut self, item: T) -> Result<(), error::DatabaseError> {
        if self.strict_dupes {
            if self.items.contains(&item) {
                return Err(error::DatabaseError::DupeFound);
            }
        }

        self.items.insert(item);
        return Ok(());
    }

    /// Replaces an item inside of the database with another
    /// item, used for updating/replacing items easily.
    ///
    /// [Database::query_item] can be used in conjunction to find and replace
    /// values individually if needed.
    pub fn update_item(&mut self, item: &T, new: T) -> Result<(), error::DatabaseError> {
        self.remove_item(item)?;
        self.add_item(new)?;

        Ok(())
    }

    /// Removes an item from the database.
    ///
    /// See [Database::update_item] if you'd like to update/replace an item easily,
    /// rather than individually deleting and adding.
    ///
    /// # Errors
    ///
    /// Will return [error::DatabaseError::ItemNotFound] if the item that is attempting
    /// to be deleted was not found.
    pub fn remove_item(&mut self, item: &T) -> Result<(), error::DatabaseError> {
        if self.items.remove(item) {
            Ok(())
        } else {
            Err(error::DatabaseError::ItemNotFound)
        }
    }

    /// Dumps/saves database to a binary file.
    ///
    /// # Saving path methods
    ///
    /// The database will usually save as `\[label\].tinydb` where `\[label\]`
    /// is the defined [Database::label] (path is reletive to where tinydb was
    /// executed).
    ///
    /// You can also overwrite this behaviour by defining a [Database::save_path]
    /// when generating the database inside of [Database::new].
    pub fn dump_db(&self) -> Result<(), error::DatabaseError> {
        let mut dump_file = self.open_db_path()?;
        bincode::serialize_into(&mut dump_file, self).unwrap();

        Ok(())
    }

    /// Query the database for a specific item.
    ///
    /// # Syntax
    ///
    /// ```none
    /// self.query_item(|[p]| [p].[field], [query]);
    /// ```
    ///
    /// - `[p]` The closure (Will be whatever the database currently is saving as a schema).
    /// - `[field]` The exact field of `p`. If the database doesn't contain structures, don't add the `.[field]`.
    /// - `[query]` Item to query for. This is a generic and can be of any reasonable type.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use serde::{Serialize, Deserialize};
    /// use tinydb::Database;
    ///
    /// #[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Clone)]
    /// struct ExampleStruct {
    ///     my_age: i32
    /// }
    ///
    /// fn main() {
    ///     let my_struct = ExampleStruct { my_age: 329 };
    ///     let mut my_db = Database::new("query_test", None, false);
    ///
    ///     my_db.add_item(my_struct.clone());
    ///
    ///     let results = my_db.query_item(|s: &ExampleStruct| &s.my_age, 329);
    ///
    ///     assert_eq!(results.unwrap(), &my_struct);
    /// }
    /// ```
    pub fn query_item<Q: PartialEq, V: Fn(&T) -> &Q>(
        &self,
        value: V,
        query: Q,
    ) -> Result<&T, error::DatabaseError> {
        for item in self.items.iter() {
            if value(item) == &query {
                return Ok(item);
            }
        }

        Err(error::DatabaseError::ItemNotFound)
    }

    /// Searches the database for a specific value. If it does not exist, this
    /// method will return [error::DatabaseError::ItemNotFound].
    ///
    /// This is a wrapper around [HashSet::contains].
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tinydb::Database;
    /// use serde::{Serialize, Deserialize};
    ///
    /// #[derive(Hash, Eq, PartialEq, Serialize, Deserialize, Copy, Clone)]
    /// struct ExampleStruct {
    ///     item: i32
    /// }
    ///
    /// fn main() {
    ///     let exp_struct = ExampleStruct { item: 4942 };
    ///     let mut db = Database::new("Contains example", None, false);
    ///
    ///     db.add_item(exp_struct.clone());
    ///
    ///     assert_eq!(db.contains(&exp_struct), true);
    /// }
    /// ```
    pub fn contains(&self, query: &T) -> bool {
        self.items.contains(query)
    }

    /// Opens the path given in [Database::save_path] (or auto-generates a path).
    fn open_db_path(&self) -> Result<File, error::DatabaseError> {
        let definate_path = self.smart_path_get();

        if definate_path.exists() {
            std::fs::remove_file(&definate_path)?;
        }

        Ok(File::create(&definate_path)?)
    }

    /// Automatically allocates a path for the database if [Database::save_path]
    /// is not provided. If it is, this function will simply return it.
    fn smart_path_get(&self) -> PathBuf {
        if self.save_path.is_none() {
            return PathBuf::from(format!("{}.tinydb", self.label));
        }

        PathBuf::from(self.save_path.as_ref().unwrap())
    }
}

/// Reads a given path and converts it into a [Vec]<[u8]> stream.
fn get_stream_from_path(path: PathBuf) -> Result<Vec<u8>, error::DatabaseError> {
    if !path.exists() {
        return Err(error::DatabaseError::DatabaseNotFound);
    }

    let mut file = File::open(path)?;
    let mut buffer = Vec::new();

    file.read_to_end(&mut buffer)?;

    Ok(buffer)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A dummy struct to use inside of tests
    #[derive(Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)]
    struct DemoStruct {
        name: String,
        age: i32,
    }

    /// Tests addition to in-memory db
    #[test]
    fn item_add() -> Result<(), error::DatabaseError> {
        let mut my_db = Database::new("Adding test", None, true);

        my_db.add_item(DemoStruct {
            name: String::from("John"),
            age: 16,
        })?;

        Ok(())
    }

    /// Tests removal from in-memory db
    #[test]
    fn item_remove() -> Result<(), error::DatabaseError> {
        let mut my_db = Database::new("Removal test", None, true);

        let testing_struct = DemoStruct {
            name: String::from("Xander"),
            age: 33,
        };

        my_db.add_item(testing_struct.clone())?;
        my_db.remove_item(&testing_struct)?;

        Ok(())
    }

    #[test]
    fn db_dump() -> Result<(), error::DatabaseError> {
        let mut my_db = Database::new(
            String::from("Dumping test"),
            Some(PathBuf::from("test.tinydb")),
            true,
        );

        my_db.add_item(DemoStruct {
            name: String::from("Xander"),
            age: 33,
        })?;
        my_db.add_item(DemoStruct {
            name: String::from("John"),
            age: 54,
        })?;

        my_db.dump_db()?;

        Ok(())
    }
    /// Tests [Database::query_item]
    #[test]
    fn query_item_db() {
        let mut my_db = Database::new(
            String::from("Query test"),
            Some(PathBuf::from("test.tinydb")),
            true,
        );

        my_db
            .add_item(DemoStruct {
                name: String::from("Rimmer"),
                age: 5,
            })
            .unwrap();
        my_db
            .add_item(DemoStruct {
                name: String::from("Cat"),
                age: 10,
            })
            .unwrap();
        my_db
            .add_item(DemoStruct {
                name: String::from("Kryten"),
                age: 3000,
            })
            .unwrap();
        my_db
            .add_item(DemoStruct {
                name: String::from("Lister"),
                age: 62,
            })
            .unwrap();

        assert_eq!(
            my_db.query_item(|f| &f.age, 62).unwrap(),
            &DemoStruct {
                name: String::from("Lister"),
                age: 62,
            }
        ); // Finds "Lister" by searching [DemoStruct::age]
        assert_eq!(
            my_db.query_item(|f| &f.name, String::from("Cat")).unwrap(),
            &DemoStruct {
                name: String::from("Cat"),
                age: 10,
            }
        ); // Finds "Cat" by searching [DemoStruct::name]
    }

    /// Tests a [Database::from] method call
    #[test]
    fn db_from() -> Result<(), error::DatabaseError> {
        db_dump()?; // ensure database was dumped

        let my_db: Database<DemoStruct> = Database::from(PathBuf::from("test.tinydb"))?;

        assert_eq!(my_db.label, String::from("Dumping test"));

        Ok(())
    }

    /// Test if the database contains that exact item, related to
    /// [Database::contains].
    #[test]
    fn db_contains() {
        let exp_struct = DemoStruct {
            name: String::from("Xander"),
            age: 33,
        };

        let mut db = Database::new(String::from("Contains example"), None, false);
        db.add_item(exp_struct.clone()).unwrap();
        assert_eq!(db.contains(&exp_struct), true);
    }

    /// Tests [Database::auto_from]'s ability to create new databases and fetch
    /// already existing ones; an all-round test of its purpose.
    #[test]
    fn auto_from_creation() {
        let _dummy_db: Database<DemoStruct> =
            Database::new(String::from("alreadyexists"), None, false);

        let from_db_path = PathBuf::from("alreadyexists.tinydb");
        let _from_db: Database<DemoStruct> = Database::auto_from(from_db_path, false).unwrap();

        let new_db_path = PathBuf::from("nonexistant.tinydb");
        let _net_db: Database<DemoStruct> = Database::auto_from(new_db_path, false).unwrap();
    }
}