├── .github └── workflows │ └── cross-compile.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── database.rs ├── error.rs ├── gddb.rs ├── lib.rs └── record.rs /.github/workflows/cross-compile.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Build 19 | run: cargo build --verbose 20 | - name: Run tests 21 | run: cargo test -v -- --test-threads=1 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | test.gddb 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gddb" 3 | description = "GDDB is a superfast in-memory database designed for use in Godot." 4 | version = "0.3.1" 5 | repository = "https://github.com/patchfx/gddb" 6 | license = "MIT" 7 | readme = "README.md" 8 | authors = ["Richard Patching "] 9 | edition = "2021" 10 | 11 | [lib] 12 | crate-type=["cdylib"] 13 | 14 | [dependencies] 15 | bincode = "1.3" 16 | gdnative = "0.10.0" 17 | serde_json = "1.0" 18 | uuid = { version = "1.0", features = ["v4"] } 19 | 20 | [dependencies.serde] 21 | version = "1.0" 22 | features = ["derive"] 23 | 24 | [dependencies.hashbrown] 25 | version = "0.12" 26 | features = ["serde"] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2022` `Richard Patching` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDDB 2 | 3 | [![crates.io](https://img.shields.io/crates/v/gddb.svg)](https://crates.io/crates/gddb) 4 | [![Cross-compile](https://github.com/patchfx/gddb/actions/workflows/cross-compile.yml/badge.svg)](https://github.com/patchfx/gddb/actions/workflows/cross-compile.yml) 5 | [![Documentation](https://docs.rs/gddb/badge.svg)](https://docs.rs/gddb) 6 | [![Version](https://img.shields.io/badge/rustc-1.56+-lightgray.svg)](https://blog.rust-lang.org/2021/11/01/Rust-1.56.1.html) 7 | ![License](https://img.shields.io/crates/l/gddb.svg) 8 | 9 | GDDB is a superfast in-memory database designed for use in Godot. 10 | 11 | This database aims to provide an easy frontend to an efficient in-memory database, that can be saved and reloaded. 12 | 13 | GDDB saves a Godot dictionary and provides an interface to create, update, retrieve (either single results or all items matching the search) and destroy records. 14 | 15 | GDDB started as a fork of [TinyDB](https://github.com/Owez/tinydb) with added functionality and a Godot wrapper. 16 | 17 | - [Documentation](https://docs.rs/gddb) 18 | - [Crates.io](https://crates.io/crates/gddb) 19 | 20 | ## Installation 21 | 22 | - git clone https://github.com/patchfx/gddb.git 23 | - cd gddb 24 | - cargo build 25 | - Copy the libgddb.(dll|so) to your Godot project 26 | - Create a new GDNativeLibrary and link to the lib 27 | - Create a new GDNativeScript filed with a class name of 'GDDB' 28 | - Attach the GDNativeLibrary to the GDNativeScript 29 | - Autoload the GDNativeScript 30 | 31 | ## Example 32 | 33 | ```gdscript 34 | extends Node 35 | 36 | func _ready(): 37 | var data = { "name": "Joe Bloggs" } 38 | var player_uuid = Database.create("Player", data) 39 | print(player_uuid) 40 | 41 | var record = Database.find(player_uuid) 42 | print(record.name) 43 | 44 | record.name = "John Doe" 45 | Database.update(record.uuid, record.model, record.attributes) 46 | 47 | var updated = Database.find(player_uuid) 48 | print(updated.name) 49 | ``` 50 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | /// The primary database structure, allowing storage of a generic type with 4 | /// dumping/saving options avalible. 5 | /// 6 | /// The generic type used should primarily be structures as they resemble a 7 | /// conventional database model and should implament [hash::Hash] and [Eq] for 8 | /// basic in-memory storage with [Serialize] and [Deserialize] being implamented 9 | /// for file operations involving the database (these are also required). 10 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 11 | pub struct Database { 12 | /// Friendly name for the database, preferibly in `slug-form-like-this` as 13 | /// this is the fallback path 14 | /// 15 | /// This is used when dumping the database without a [Database::save_path] 16 | /// being defined and a friendly way to order a database 17 | pub label: String, 18 | 19 | /// The overwrite path to save the database as, this is recommended otherwise 20 | /// it will end up as `./Hello\ There.gddb` if [Database::label] is "Hello 21 | /// There" 22 | /// 23 | /// Primarily used inside of [Database::dump_db]. 24 | pub save_path: Option, 25 | 26 | /// If the database should return an error if it tries to insert where an 27 | /// identical item already is. Setting this as `false` doesn't allow 28 | /// duplicates, it just doesn't flag an error. 29 | pub strict_dupes: bool, 30 | 31 | /// In-memory [HashSet] of all items 32 | pub items: HashSet, 33 | } 34 | 35 | impl Database { 36 | /// Creates a new database instance from given parameters. 37 | /// 38 | /// - To add a first item, use [Database::create]. 39 | /// - If you'd like to load a dumped database, use [Database::from] 40 | pub fn new( 41 | label: impl Into, 42 | save_path: impl Into>, 43 | strict_dupes: bool, 44 | ) -> Self { 45 | Database { 46 | label: label.into(), 47 | save_path: save_path.into(), 48 | strict_dupes, 49 | items: HashSet::new(), 50 | } 51 | } 52 | 53 | /// Creates a database from a `.gddb` file. 54 | /// 55 | /// This retrives a dump file (saved database) from the path given and loads 56 | /// it as the [Database] structure. 57 | /// 58 | /// # Examples 59 | /// 60 | /// ```rust 61 | /// use gddb::Database; 62 | /// use serde::{Serialize, Deserialize}; 63 | /// use std::path::PathBuf; 64 | /// 65 | /// 66 | /// /// Makes a small testing database. 67 | /// fn make_db() { 68 | /// let mut test_db: Database = Database::new("test", None, false); 69 | /// test_db.create(Record::new("Test".into())); 70 | /// test_db.dump_db(); 71 | /// } 72 | /// 73 | /// /// Get `test_db` defined in [make_db] and test. 74 | /// fn main() { 75 | /// make_db(); 76 | /// 77 | /// let db = Database::from( 78 | /// PathBuf::from("test.gddb") 79 | /// ).unwrap(); 80 | /// 81 | /// assert_eq!( 82 | /// db.len(), 83 | /// 1 84 | /// ); // Check that the database still has added [ExampleStruct]. 85 | /// } 86 | /// ``` 87 | pub fn from(path: impl Into) -> Result { 88 | let stream = get_stream_from_path(path.into())?; 89 | let decoded: Database = bincode::deserialize(&stream[..]).unwrap(); 90 | 91 | Ok(decoded) 92 | } 93 | 94 | /// Adds a new item to the in-memory database. 95 | /// 96 | /// If this is the first item added to the database, please ensure it's the 97 | /// only type you'd like to add. Due to generics, the first item you add 98 | /// will be set as the type to use (unless removed). 99 | pub fn create(&mut self, item: Record) -> Result<(), DatabaseError> { 100 | if self.strict_dupes { 101 | if self.items.contains(&item) { 102 | return Err(DatabaseError::DupeFound); 103 | } 104 | } 105 | 106 | self.items.insert(item); 107 | return Ok(()); 108 | } 109 | 110 | /// Replaces an item inside of the database with another 111 | /// item, used for updating/replacing items easily. 112 | /// 113 | /// [Database::update] can be used in conjunction to find and replace 114 | /// values individually if needed. 115 | pub fn update(&mut self, item: &Record, new: Record) -> Result<(), DatabaseError> { 116 | self.destroy(item)?; 117 | self.create(new)?; 118 | 119 | Ok(()) 120 | } 121 | 122 | /// Loads database from existant path or creates a new one if it doesn't already 123 | /// exist. 124 | /// 125 | /// This is the recommended way to use gddb if you are wanting to easily 126 | /// setup an entire database instance in a short, consise manner. Similar to 127 | /// [Database::new] and [Database::from], this function will also have to be 128 | /// given a strict type argument and you will still have to provide `script_dupes` 129 | /// even if the database is likely to load an existing one. 130 | /// 131 | /// This function does make some assumptions about the database name and uses 132 | /// the 2nd to last part before a `.`. This means that `x.y.z` will have the 133 | /// name of `y`, not `x` so therefore it is recommended to have a database 134 | /// path with `x.gddb` or `x.db` only. 135 | /// 136 | /// # Examples 137 | /// 138 | /// ```rust 139 | /// use gddb::*; 140 | /// use std::path::PathBuf; 141 | /// use serde::{Serialize, Deserialize}; 142 | /// 143 | /// fn main() { 144 | /// let dummy_db: Database = Database::new("cool", None, false); // create demo db for `db_from` 145 | /// 146 | /// let db_from_path = PathBuf::from("cool.gddb"); 147 | /// let db_from: Database = Database::auto_from(db_from_path, false).unwrap(); // automatically load it 148 | /// 149 | /// let db_new_path = PathBuf::from("xyz.gddb"); 150 | /// let db_new: Database = Database::auto_from(db_new_path, false).unwrap(); // automatically create new as "xyz" doesn't exist 151 | /// } 152 | /// ``` 153 | pub fn auto_from(path: impl Into, strict_dupes: bool) -> Result { 154 | let path_into = path.into(); 155 | 156 | if path_into.exists() { 157 | Database::from(path_into) 158 | } else { 159 | let db_name = match path_into.file_stem() { 160 | Some(x) => match x.to_str() { 161 | Some(y) => String::from(y), 162 | None => return Err(DatabaseError::BadDbName), 163 | }, 164 | None => return Err(DatabaseError::BadDbName), 165 | }; 166 | 167 | Ok(Database::new(db_name, Some(path_into), strict_dupes)) 168 | } 169 | } 170 | 171 | /// Removes an item from the database. 172 | /// 173 | /// See [Database::update] if you'd like to update/replace an item easily, 174 | /// rather than individually deleting and adding. 175 | /// 176 | /// # Errors 177 | /// 178 | /// Will return [DatabaseError::ItemNotFound] if the item that is attempting 179 | /// to be deleted was not found. 180 | pub fn destroy(&mut self, item: &Record) -> Result<(), DatabaseError> { 181 | if self.items.remove(item) { 182 | Ok(()) 183 | } else { 184 | Err(DatabaseError::ItemNotFound) 185 | } 186 | } 187 | 188 | /// Dumps/saves database to a binary file. 189 | /// 190 | /// # Saving path methods 191 | /// 192 | /// The database will usually save as `\[label\].gddb` where `\[label\]` 193 | /// is the defined [Database::label] (path is reletive to where gddb was 194 | /// executed). 195 | /// 196 | /// You can also overwrite this behaviour by defining a [Database::save_path] 197 | /// when generating the database inside of [Database::new]. 198 | pub fn dump_db(&self) -> Result<(), DatabaseError> { 199 | let mut dump_file = self.open_db_path()?; 200 | bincode::serialize_into(&mut dump_file, self).unwrap(); 201 | 202 | Ok(()) 203 | } 204 | 205 | /// Query the database for a specific item. 206 | /// 207 | /// # Syntax 208 | /// 209 | /// ```none 210 | /// self.find(|[p]| [p].[field], [query]); 211 | /// ``` 212 | /// 213 | /// - `[p]` The closure (Will be whatever the database currently is saving as a schema). 214 | /// - `[field]` The exact field of `p`. If the database doesn't contain structures, don't add the `.[field]`. 215 | /// - `[query]` Item to query for. This is a generic and can be of any reasonable type. 216 | /// 217 | /// # Examples 218 | /// 219 | /// ```rust 220 | /// use serde::{Serialize, Deserialize}; 221 | /// use gddb::Database; 222 | /// 223 | /// #[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Clone)] 224 | /// struct ExampleStruct { 225 | /// my_age: i32 226 | /// } 227 | /// 228 | /// fn main() { 229 | /// let my_struct = ExampleStruct { my_age: 329 }; 230 | /// let mut my_db = Database::new("query_test", None, false); 231 | /// 232 | /// my_db.create(my_struct.clone()); 233 | /// 234 | /// let results = my_db.find(|s: &ExampleStruct| &s.my_age, 329); 235 | /// 236 | /// assert_eq!(results.unwrap(), &my_struct); 237 | /// } 238 | /// ``` 239 | pub fn find &Q>( 240 | &self, 241 | value: V, 242 | query: Q, 243 | ) -> Result<&Record, DatabaseError> { 244 | for item in self.items.iter() { 245 | if value(item).eq(&query) { 246 | return Ok(item); 247 | } 248 | } 249 | 250 | Err(DatabaseError::ItemNotFound) 251 | } 252 | 253 | /// Query the database for all matching items. 254 | /// 255 | /// # Syntax 256 | /// 257 | /// ```none 258 | /// self.query(|[p]| [p].[field], [query]); 259 | /// ``` 260 | /// 261 | /// - `[p]` The closure (Will be whatever the database currently is saving as a schema). 262 | /// - `[field]` The exact field of `p`. If the database doesn't contain structures, don't add the `.[field]`. 263 | /// - `[query]` Item to query for. This is a generic and can be of any reasonable type. 264 | /// 265 | /// # Examples 266 | /// 267 | /// ```rust 268 | /// use serde::{Serialize, Deserialize}; 269 | /// use gddb::Database; 270 | /// 271 | /// #[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Clone)] 272 | /// struct ExampleStruct { 273 | /// uuid: String, 274 | /// age: i32, 275 | /// } 276 | /// 277 | /// fn main() { 278 | /// let mut my_db = Database::new("query_test", None, false); 279 | /// 280 | /// my_db.create(ExampleStruct { uuid: "test1".into(), age: 20 }); 281 | /// my_db.create(ExampleStruct { uuid: "test2".into(), age: 20 }); 282 | /// my_db.create(ExampleStruct { uuid: "test3".into(), age: 18 }); 283 | /// 284 | /// let results = my_db.query(|s: &ExampleStruct| &s.age, 20); 285 | /// 286 | /// assert_eq!(results.unwrap().len(), 2); 287 | /// } 288 | /// ``` 289 | pub fn query &Q>( 290 | &self, 291 | value: V, 292 | query: Q, 293 | ) -> Result, DatabaseError> { 294 | let mut items: Vec<&Record> = vec![]; 295 | for item in self.items.iter() { 296 | if value(item) == &query { 297 | items.push(item); 298 | } 299 | } 300 | 301 | if items.len() > 0 { 302 | return Ok(items); 303 | } 304 | 305 | Err(DatabaseError::ItemNotFound) 306 | } 307 | 308 | /// Searches the database for a specific value. If it does not exist, this 309 | /// method will return [DatabaseError::ItemNotFound]. 310 | /// 311 | /// This is a wrapper around [HashSet::contains]. 312 | /// 313 | /// # Examples 314 | /// 315 | /// ```rust 316 | /// use gddb::Database; 317 | /// use serde::{Serialize, Deserialize}; 318 | /// 319 | /// #[derive(Hash, Eq, PartialEq, Serialize, Deserialize, Copy, Clone)] 320 | /// struct ExampleStruct { 321 | /// item: i32 322 | /// } 323 | /// 324 | /// fn main() { 325 | /// let exp_struct = ExampleStruct { item: 4942 }; 326 | /// let mut db = Database::new("Contains example", None, false); 327 | /// 328 | /// db.create(exp_struct.clone()); 329 | /// 330 | /// assert_eq!(db.contains(&exp_struct), true); 331 | /// } 332 | /// ``` 333 | pub fn contains(&self, query: &Record) -> bool { 334 | self.items.contains(query) 335 | } 336 | 337 | /// Returns the number of database entries 338 | /// method will return i32. 339 | /// 340 | /// 341 | /// # Examples 342 | /// 343 | /// ```rust 344 | /// use gddb::Database; 345 | /// use serde::{Serialize, Deserialize}; 346 | /// 347 | /// #[derive(Hash, Eq, PartialEq, Serialize, Deserialize, Copy, Clone)] 348 | /// struct ExampleStruct { 349 | /// item: i32 350 | /// } 351 | /// 352 | /// fn main() { 353 | /// let exp_struct = ExampleStruct { item: 4942 }; 354 | /// let mut db = Database::new("Contains example", None, false); 355 | /// 356 | /// db.create(exp_struct.clone()); 357 | /// 358 | /// assert_eq!(db.len(), 1); 359 | /// } 360 | /// ``` 361 | pub fn len(&self) -> i32 { 362 | self.items.len() as i32 363 | } 364 | 365 | /// Opens the path given in [Database::save_path] (or auto-generates a path). 366 | fn open_db_path(&self) -> Result { 367 | let definate_path = self.smart_path_get(); 368 | 369 | if definate_path.exists() { 370 | std::fs::remove_file(&definate_path)?; 371 | } 372 | 373 | Ok(File::create(&definate_path)?) 374 | } 375 | 376 | /// Automatically allocates a path for the database if [Database::save_path] 377 | /// is not provided. If it is, this function will simply return it. 378 | fn smart_path_get(&self) -> PathBuf { 379 | if self.save_path.is_none() { 380 | return PathBuf::from(format!("{}.gddb", self.label)); 381 | } 382 | 383 | PathBuf::from(self.save_path.as_ref().unwrap()) 384 | } 385 | } 386 | 387 | /// Reads a given path and converts it into a [Vec]<[u8]> stream. 388 | fn get_stream_from_path(path: PathBuf) -> Result, DatabaseError> { 389 | if !path.exists() { 390 | return Err(DatabaseError::DatabaseNotFound); 391 | } 392 | 393 | let mut file = File::open(path)?; 394 | let mut buffer = Vec::new(); 395 | 396 | file.read_to_end(&mut buffer)?; 397 | 398 | Ok(buffer) 399 | } 400 | 401 | #[cfg(test)] 402 | mod tests { 403 | use super::*; 404 | 405 | /// Tests addition to in-memory db 406 | #[test] 407 | fn item_add() -> Result<(), DatabaseError> { 408 | let mut my_db = Database::new("Adding test", None, true); 409 | 410 | my_db.create(Record::new("Test".into()))?; 411 | 412 | Ok(()) 413 | } 414 | 415 | /// Tests removal from in-memory db 416 | #[test] 417 | fn item_remove() -> Result<(), DatabaseError> { 418 | let mut my_db = Database::new("Removal test", None, true); 419 | 420 | let testing_struct = Record::new("Testing".into()); 421 | 422 | my_db.create(testing_struct.clone())?; 423 | my_db.destroy(&testing_struct)?; 424 | 425 | Ok(()) 426 | } 427 | 428 | #[test] 429 | fn item_update() -> Result<(), DatabaseError> { 430 | let mut db: Database = Database::new("Update test", None, true); 431 | 432 | let testing_struct = Record::new("Test".into()); 433 | 434 | db.create(testing_struct.clone())?; 435 | 436 | let mut updated_struct = testing_struct.clone(); 437 | updated_struct.attributes = "Testing".into(); 438 | db.update(&testing_struct, updated_struct)?; 439 | let record = db.find(|f| &f.uuid, testing_struct.uuid)?; 440 | let attributes: String = "Testing".into(); 441 | assert_eq!(attributes, record.attributes); 442 | Ok(()) 443 | } 444 | 445 | #[test] 446 | fn db_dump() -> Result<(), DatabaseError> { 447 | let mut my_db = Database::new( 448 | String::from("Dumping test"), 449 | Some(PathBuf::from("test.gddb")), 450 | true, 451 | ); 452 | 453 | my_db.create(Record::new("Testing".into()))?; 454 | my_db.create(Record::new("Testing".into()))?; 455 | 456 | my_db.dump_db()?; 457 | 458 | Ok(()) 459 | } 460 | /// Tests [Database::find] 461 | #[test] 462 | fn find_db() { 463 | let mut my_db = Database::new( 464 | String::from("Query test"), 465 | Some(PathBuf::from("test.gddb")), 466 | true, 467 | ); 468 | 469 | let staging = Record::new("Staging".into()); 470 | 471 | my_db.create(Record::new("Testing".into())).unwrap(); 472 | my_db.create(staging.clone()).unwrap(); 473 | my_db.create(Record::new("Production".into())).unwrap(); 474 | 475 | assert_eq!( 476 | my_db.find(|f| &f.model, "Staging".into()).unwrap(), 477 | &staging 478 | ); // Finds "Staging" by searching [DemoStruct::model] 479 | } 480 | 481 | /// Tests [Database::query] 482 | #[test] 483 | fn query_db() { 484 | let mut my_db = Database::new( 485 | String::from("Query test"), 486 | Some(PathBuf::from("test.gddb")), 487 | false, 488 | ); 489 | 490 | my_db.create(Record::new("Testing".into())).unwrap(); 491 | my_db.create(Record::new("Testing".into())).unwrap(); 492 | my_db.create(Record::new("Staging".into())).unwrap(); 493 | 494 | assert_eq!( 495 | my_db.query(|f| &f.model, "Testing".into()).unwrap().len(), 496 | 2 497 | ); // Finds "Testing" by searching [DemoStruct::model] 498 | } 499 | 500 | /// Tests a [Database::from] method call 501 | #[test] 502 | fn db_from() -> Result<(), DatabaseError> { 503 | let mut my_db = Database::new( 504 | String::from("Dumping test"), 505 | Some(PathBuf::from("test.gddb")), 506 | false, 507 | ); 508 | 509 | let demo_mock = Record::new("Testing".into()); 510 | 511 | my_db.create(demo_mock.clone()).unwrap(); 512 | 513 | my_db.dump_db()?; 514 | 515 | let db: Database = Database::from(PathBuf::from("test.gddb"))?; 516 | assert_eq!(db.label, String::from("Dumping test")); 517 | 518 | Ok(()) 519 | } 520 | 521 | /// Test if the database contains that exact item, related to 522 | /// [Database::contains]. 523 | #[test] 524 | fn db_contains() { 525 | let exp_struct = Record::new("Testing".into()); 526 | 527 | let mut db = Database::new(String::from("Contains example"), None, false); 528 | db.create(exp_struct.clone()).unwrap(); 529 | assert_eq!(db.contains(&exp_struct), true); 530 | } 531 | 532 | /// Tests [Database::auto_from]'s ability to create new databases and fetch 533 | /// already existing ones; an all-round test of its purpose. 534 | #[test] 535 | fn auto_from_creation() { 536 | let _dummy_db: Database = Database::new(String::from("alreadyexists"), None, false); 537 | 538 | let from_db_path = PathBuf::from("alreadyexists.gddb"); 539 | let _from_db: Database = Database::auto_from(from_db_path, false).unwrap(); 540 | 541 | let new_db_path = PathBuf::from("nonexistant.gddb"); 542 | let _net_db: Database = Database::auto_from(new_db_path, false).unwrap(); 543 | } 544 | 545 | /// Tests [Database::len] returns the number of database entries 546 | #[test] 547 | fn len() { 548 | let mut db: Database = Database::new( 549 | String::from("Query test"), 550 | Some(PathBuf::from("test.gddb")), 551 | true, 552 | ); 553 | 554 | let demo_mock = Record::new("Testing".into()); 555 | 556 | db.create(demo_mock.clone()).unwrap(); 557 | 558 | assert_eq!(db.len(), 1); 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Contains various items related to errors inside of GDDB. 2 | 3 | /// An error enum for the possible faliure states of the [crate::Database] structure. 4 | #[derive(Debug)] 5 | pub enum DatabaseError { 6 | /// When the item queried for was not found 7 | ItemNotFound, 8 | 9 | /// A duplicate value was found when adding to the database with 10 | /// [crate::Database::strict_dupes] allowed. 11 | DupeFound, 12 | /// When [crate::Database::save_path] is required but is not found. This commonly 13 | /// happens when loading or dumping a database with [crate::Database::save_path] 14 | /// being [Option::None]. 15 | SavePathRequired, 16 | 17 | /// Misc [std::io::Error] that could not be properly handled. 18 | IOError(std::io::Error), 19 | 20 | /// When the database could not be found. This is typically raised inside of 21 | /// [crate::Database::from] when it tries to retrieve the path to the database. 22 | DatabaseNotFound, 23 | 24 | /// When the given database name to an assumption-making function like 25 | /// [crate::Database::auto_from] does not have a valid file stem or could not 26 | /// convert from an [std::ffi::OsString] to a [String]. 27 | BadDbName, 28 | } 29 | 30 | impl From for DatabaseError { 31 | fn from(e: std::io::Error) -> Self { 32 | DatabaseError::IOError(e) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/gddb.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | /// The primary Godot interface to the database. 4 | #[derive(NativeClass)] 5 | #[inherit(Node)] 6 | pub struct GDDB { 7 | storage: Database, 8 | } 9 | 10 | #[methods] 11 | impl GDDB { 12 | fn new(_owner: &Node) -> Self { 13 | let db: Database = Database::new("GAME", None, false); 14 | Self { storage: db } 15 | } 16 | 17 | // Creates a database record 18 | #[export] 19 | pub fn create(&mut self, _owner: &Node, model: String, attributes: Dictionary) -> String { 20 | let mut record = Record::new(model); 21 | let uuid = record.uuid.clone(); 22 | record.attributes = attributes.to_json().to_string(); 23 | 24 | self.storage.create(record).unwrap(); 25 | 26 | uuid 27 | } 28 | 29 | // Finds a database record given a uuid 30 | #[export] 31 | pub fn find(&mut self, _owner: &Node, uuid: String) -> GodotString { 32 | let record = self 33 | .storage 34 | .find(|f| &f.uuid, uuid) 35 | .expect("Could not find record"); 36 | 37 | let data = Dictionary::new(); 38 | 39 | data.insert("uuid", record.uuid.clone()); 40 | data.insert("model", record.model.clone()); 41 | data.insert("attributes", record.attributes.clone()); 42 | 43 | data.to_json() 44 | } 45 | 46 | // Updates a record 47 | #[export] 48 | pub fn update(&mut self, _owner: &Node, uuid: String, model: String, attributes: String) { 49 | let new = Record { 50 | uuid, 51 | model, 52 | attributes, 53 | }; 54 | 55 | let uuid = new.uuid.clone(); 56 | let original = self 57 | .storage 58 | .find(|f| &f.uuid, uuid) 59 | .expect("Could not find record to update") 60 | .clone(); 61 | 62 | self.storage 63 | .update(&original, new.clone()) 64 | .expect("Cannot update record"); 65 | } 66 | 67 | // Removes a record 68 | #[export] 69 | pub fn destroy(&mut self, _owner: &Node, uuid: String, model: String, attributes: String) { 70 | let record = Record { 71 | uuid, 72 | model, 73 | attributes, 74 | }; 75 | 76 | self.storage.destroy(&record).expect("Cannot remove record"); 77 | } 78 | 79 | #[export] 80 | pub fn all(&self, _owner: &Node) -> Vec { 81 | let mut records = vec![]; 82 | 83 | for record in self.storage.items.iter() { 84 | let data = Dictionary::new(); 85 | 86 | data.insert("uuid", record.uuid.clone()); 87 | data.insert("model", record.model.clone()); 88 | data.insert("attributes", record.attributes.clone()); 89 | 90 | records.push(data.to_json()); 91 | } 92 | 93 | records 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # GDDB 2 | //! 3 | //! GDDB is a superfast in-memory database designed for use in Godot. 4 | //! 5 | //! This database aims to provide an easy frontend to an efficient in-memory database, that can be saved and reloaded. 6 | //! 7 | //! GDDB saves a Godot dictionary and provides an interface to create, update, retrieve (either single results or all items matching the search) and destroy records. 8 | //! 9 | //! GDDB started as a fork of [TinyDB](https://github.com/Owez/tinydb) with added functionality and a Godot wrapper. 10 | //! 11 | //! - [Documentation](https://docs.rs/gddb) 12 | //! - [Crates.io](https://crates.io/crates/gddb) 13 | //! 14 | //! ## Rust Example 🚀 15 | //! 16 | //! An example of utilising GDDB within your Rust library. 17 | //! 18 | //! ```rust 19 | //! use serde::{Serialize, Deserialize}; 20 | //! use gddb::Database; 21 | //! 22 | //! #[derive(Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Clone)] 23 | //! struct PlayerStruct { 24 | //! name: String 25 | //! } 26 | //! 27 | //! fn main() { 28 | //! let player = PlayerStruct { name: "Joe Bloggs".into() }; 29 | //! let mut db = Database::new("GAME", None, false); 30 | //! 31 | //! db.create(my_struct.clone()); 32 | //! 33 | //! let results = db.find(|s: &PlayerStruct| &s.name, "Joe Bloggs".into()); 34 | //! 35 | //! assert_eq!(results.unwrap(), &player); 36 | //! } 37 | //! ``` 38 | //! 39 | //! # Installation 40 | //! 41 | //! Simply add the following to your `Cargo.toml` file: 42 | //! 43 | //! ```toml 44 | //! [dependencies] 45 | //! gddb = "0.3.0" 46 | //! ``` 47 | //! # Implementation notes 48 | //! 49 | //! - This database does not save 2 duplicated items, either ignoring or raising an 50 | //! error depending on end-user preference. 51 | //! - This project is not intended to be used inside of any critical systems due to 52 | //! the nature of dumping/recovery. If you are using this crate as a temporary and 53 | //! in-memory only database, it should preform at a reasonable speed (as it uses 54 | //! [HashSet] underneath). 55 | //! 56 | //! # Essential operations 57 | //! 58 | //! Some commonly-used operations for the [Database] structure. 59 | //! 60 | //! | Operation | Implamentation | 61 | //! |-----------------------------------------|-----------------------| 62 | //! | Create database | [Database::new] | 63 | //! | Create database from file | [Database::from] | 64 | //! | Load database or create if non-existant | [Database::auto_from] | 65 | //! | Query all matching items | [Database::query] | 66 | //! | Query for item | [Database::find] | 67 | //! | Contains specific item | [Database::contains] | 68 | //! | Update/replace item | [Database::update] | 69 | //! | Delete item | [Database::destroy] | 70 | //! | Dump database | [Database::dump_db] | 71 | 72 | pub mod database; 73 | pub mod error; 74 | pub mod gddb; 75 | pub mod record; 76 | use gdnative::prelude::*; 77 | 78 | mod prelude { 79 | pub use crate::database::*; 80 | pub use crate::error::*; 81 | pub use crate::gddb::*; 82 | pub use crate::record::*; 83 | 84 | pub use core::fmt::Display; 85 | pub use gdnative::prelude::*; 86 | pub use hashbrown::HashSet; 87 | pub use serde::{de::DeserializeOwned, Deserialize, Serialize}; 88 | pub use std::fs::File; 89 | pub use std::hash; 90 | pub use std::io::prelude::*; 91 | pub use std::path::PathBuf; 92 | pub use uuid::Uuid; 93 | } 94 | 95 | use prelude::*; 96 | 97 | fn init(handle: InitHandle) { 98 | handle.add_class::(); 99 | } 100 | 101 | godot_init!(init); 102 | -------------------------------------------------------------------------------- /src/record.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | pub trait RecordCheck: PartialEq + Default + Display {} 4 | impl RecordCheck for T where T: PartialEq + Default + Display {} 5 | 6 | #[derive(Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize)] 7 | pub struct Record { 8 | pub uuid: String, 9 | pub model: String, 10 | pub attributes: String, 11 | } 12 | 13 | impl Record { 14 | pub fn new(model: String) -> Self { 15 | let uuid = Uuid::new_v4().to_string(); 16 | 17 | Self { 18 | uuid, 19 | model, 20 | attributes: "".into(), 21 | } 22 | } 23 | } 24 | --------------------------------------------------------------------------------