From 0bcb28bc47b7eacdfd3b0cee01037770994a725e Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Wed, 10 Mar 2021 18:31:50 -0600 Subject: [PATCH] Initial database implementation + Using sled as a database + Added functions for inserting and fetching models from the database --- .gitignore | 1 + ARCHITECTURE.md | 2 + Cargo.lock | 186 +++++++++++++++++++++++++++ Cargo.toml | 3 +- figures/db_arch.svg | 3 + geoffrey_db/Cargo.toml | 17 +++ geoffrey_db/src/database.rs | 91 +++++++++++++ geoffrey_db/src/error.rs | 31 +++++ geoffrey_db/src/lib.rs | 11 ++ geoffrey_models/src/lib.rs | 14 ++ geoffrey_models/src/models/farm.rs | 8 ++ geoffrey_models/src/models/item.rs | 8 ++ geoffrey_models/src/models/market.rs | 6 + geoffrey_models/src/models/meta.rs | 23 ++++ geoffrey_models/src/models/mod.rs | 40 ++++-- geoffrey_models/src/models/player.rs | 27 +++- geoffrey_models/src/models/shop.rs | 13 -- geoffrey_models/src/models/token.rs | 84 ++++++++++++ geoffrey_models/src/models/town.rs | 7 + 19 files changed, 551 insertions(+), 24 deletions(-) create mode 100644 figures/db_arch.svg create mode 100644 geoffrey_db/Cargo.toml create mode 100644 geoffrey_db/src/database.rs create mode 100644 geoffrey_db/src/error.rs create mode 100644 geoffrey_db/src/lib.rs create mode 100644 geoffrey_models/src/models/farm.rs create mode 100644 geoffrey_models/src/models/market.rs create mode 100644 geoffrey_models/src/models/meta.rs create mode 100644 geoffrey_models/src/models/token.rs create mode 100644 geoffrey_models/src/models/town.rs diff --git a/.gitignore b/.gitignore index 3a8cabc..7b881bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .idea +database/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5b80663..2aabc82 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -8,6 +8,8 @@ The database is the core storage of Geoffrey. All models will be stored here. It is tightly coupled with the GeoffreyAPI. +![DB Architecture](figures/db_arch.svg) + ## Geoffrey API The API provides the main logic to how models are manipulated in the Database. It is split into two parts, the Command API and Model API. diff --git a/Cargo.lock b/Cargo.lock index 5eed643..c9e1162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,24 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "chrono" version = "0.4.19" @@ -20,6 +38,70 @@ dependencies = [ "winapi", ] +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "geoffrey_db" +version = "0.1.0" +dependencies = [ + "byteorder", + "geoffrey_models", + "lazy_static", + "serde", + "serde_json", + "sled", +] + [[package]] name = "geoffrey_models" version = "0.1.0" @@ -29,18 +111,60 @@ dependencies = [ "serde_json", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a" +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memoffset" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87" +dependencies = [ + "autocfg", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -60,6 +184,31 @@ dependencies = [ "autocfg", ] +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + [[package]] name = "proc-macro2" version = "1.0.24" @@ -78,12 +227,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + [[package]] name = "ryu" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.123" @@ -115,6 +279,28 @@ dependencies = [ "serde", ] +[[package]] +name = "sled" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d0132f3e393bcb7390c60bb45769498cf4550bcb7a21d7f95c02b69f6362cdc" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + [[package]] name = "syn" version = "1.0.61" diff --git a/Cargo.toml b/Cargo.toml index e442523..5d7f8dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] members = [ - "geoffrey_models" + "geoffrey_models", + "geoffrey_db" ] \ No newline at end of file diff --git a/figures/db_arch.svg b/figures/db_arch.svg new file mode 100644 index 0000000..14613fc --- /dev/null +++ b/figures/db_arch.svg @@ -0,0 +1,3 @@ + + +
Types
Types
User
User
Location
Location
API Token
API Token
Shop
Shop
Towns
Towns
Base
Base
Item Listing
Item Listi...
Residents
Residents
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/geoffrey_db/Cargo.toml b/geoffrey_db/Cargo.toml new file mode 100644 index 0000000..0190f89 --- /dev/null +++ b/geoffrey_db/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "geoffrey_db" +version = "0.1.0" +authors = ["Joey Hines "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sled = "0.34.6" +serde = "1.0" +serde_json = "1.0" +geoffrey_models = { path = "../geoffrey_models" } +byteorder = "1.4.2" + +[dev-dependencies] +lazy_static = "1.4.0" diff --git a/geoffrey_db/src/database.rs b/geoffrey_db/src/database.rs new file mode 100644 index 0000000..73175b9 --- /dev/null +++ b/geoffrey_db/src/database.rs @@ -0,0 +1,91 @@ +use crate::error::Result; +use std::path::Path; +use geoffrey_models::GeoffreyDatabaseModel; +use crate::u64_to_bytes; + +pub struct Database { + db: sled::Db, +} + +impl Database { + pub fn new(db_path: &Path) -> Result { + let db = sled::open(db_path)?; + + Ok(Self { + db, + }) + } + + fn get_tree(&self) -> Result where T: GeoffreyDatabaseModel { + Ok(self.db.open_tree::(T::tree())?) + } + + pub fn insert<'a, T>(&self, mut model: T) -> Result where T: GeoffreyDatabaseModel { + let id = self.db.generate_id()?; + let tree = self.get_tree::()?; + let data = serde_json::to_vec(&model).unwrap(); + let id_bytes = u64_to_bytes(id); + + tree.insert(id_bytes, data)?; + + model.set_id(id); + + Ok(model) + } + + + pub fn get(&self, id: u64) -> Result> where T: GeoffreyDatabaseModel { + let tree = self.get_tree::()?; + let id_bytes = u64_to_bytes(id); + + if let Some(bytes) = tree.get(id_bytes)? { + Ok(Some(T::try_from_bytes(&bytes).unwrap())) + } + else { + Ok(None) + } + + } +} + +#[cfg(test)] +mod tests { + use crate::database::{Database}; + use std::path::Path; + use geoffrey_models::models::player::{Player, UserID}; + use lazy_static::lazy_static; + + + lazy_static! { + static ref DB: Database = Database::new(Path::new("../database")).unwrap(); + } + + fn cleanup() { + DB.db.clear().unwrap(); + } + + #[test] + fn test_insert() { + let player = Player::new("CoolZero123", UserID::DiscordUUID(0)); + + let p2 = DB.insert::(player.clone()).unwrap(); + + assert!(p2.id.is_some()); + assert_eq!(player.name, p2.name); + cleanup(); + } + + #[test] + fn test_get() { + let player = Player::new("CoolZero123", UserID::DiscordUUID(0)); + + let p2 = DB.insert::(player.clone()).unwrap(); + + let p3 = DB.get::(p2.id.unwrap()).unwrap(); + + assert!(p3.is_some()); + assert_eq!(p3.unwrap().name, player.name); + cleanup(); + } +} + diff --git a/geoffrey_db/src/error.rs b/geoffrey_db/src/error.rs new file mode 100644 index 0000000..4d54e49 --- /dev/null +++ b/geoffrey_db/src/error.rs @@ -0,0 +1,31 @@ + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum GeoffreyDBError { + SledError(sled::Error), + SerdeJsonError(serde_json::Error), +} + +impl std::error::Error for GeoffreyDBError {} + +impl std::fmt::Display for GeoffreyDBError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GeoffreyDBError::SledError(e) => write!(f, "Sled Error: {}", e), + GeoffreyDBError::SerdeJsonError(e) => write!(f, "Serde JSON Error: {}", e) + } + } +} + +impl From for GeoffreyDBError { + fn from(e: sled::Error) -> Self { + GeoffreyDBError::SledError(e) + } +} + +impl From for GeoffreyDBError { + fn from(e: serde_json::Error) -> Self { + GeoffreyDBError::SerdeJsonError(e) + } +} diff --git a/geoffrey_db/src/lib.rs b/geoffrey_db/src/lib.rs new file mode 100644 index 0000000..17b1537 --- /dev/null +++ b/geoffrey_db/src/lib.rs @@ -0,0 +1,11 @@ +use byteorder::{WriteBytesExt, BigEndian}; + +pub mod database; +pub mod error; + +pub fn u64_to_bytes(n: u64) -> Vec { + let mut id_bytes= vec![0u8; 8]; + id_bytes.write_u64::(n).unwrap(); + + id_bytes +} \ No newline at end of file diff --git a/geoffrey_models/src/lib.rs b/geoffrey_models/src/lib.rs index 9ead192..a54ff69 100644 --- a/geoffrey_models/src/lib.rs +++ b/geoffrey_models/src/lib.rs @@ -1,3 +1,17 @@ #[allow(dead_code)] +use serde::{Serialize}; +use serde::de::DeserializeOwned; + pub mod models; + +pub trait GeoffreyDatabaseModel: Serialize + DeserializeOwned { + fn id(&self) -> Option; + fn set_id(&mut self, id: u64); + fn tree() -> String; + + fn try_from_bytes(v: &[u8]) -> Result { + Ok(serde_json::from_slice::(v)?) + } +} + diff --git a/geoffrey_models/src/models/farm.rs b/geoffrey_models/src/models/farm.rs new file mode 100644 index 0000000..188169f --- /dev/null +++ b/geoffrey_models/src/models/farm.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use crate::models::item::Item; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FarmData { + pub items_produced: Vec +} + diff --git a/geoffrey_models/src/models/item.rs b/geoffrey_models/src/models/item.rs index 851680c..fc02125 100644 --- a/geoffrey_models/src/models/item.rs +++ b/geoffrey_models/src/models/item.rs @@ -5,3 +5,11 @@ pub struct Item { pub name: String, } +impl Item { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string() + } + } +} + diff --git a/geoffrey_models/src/models/market.rs b/geoffrey_models/src/models/market.rs new file mode 100644 index 0000000..50008ce --- /dev/null +++ b/geoffrey_models/src/models/market.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MarketData { + pub shops: Vec, +} diff --git a/geoffrey_models/src/models/meta.rs b/geoffrey_models/src/models/meta.rs new file mode 100644 index 0000000..25804cf --- /dev/null +++ b/geoffrey_models/src/models/meta.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use crate::GeoffreyDatabaseModel; + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Meta { + id: Option, + version: u64, + database_version: u64, +} + +impl GeoffreyDatabaseModel for Meta { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id); + } + + fn tree() -> String { + "meta".to_string() + } +} \ No newline at end of file diff --git a/geoffrey_models/src/models/mod.rs b/geoffrey_models/src/models/mod.rs index 4f2ddf1..36d8319 100644 --- a/geoffrey_models/src/models/mod.rs +++ b/geoffrey_models/src/models/mod.rs @@ -1,19 +1,28 @@ use serde::{Deserialize, Serialize}; use crate::models::player::Player; use crate::models::shop::ShopData; +use crate::models::town::TownData; +use crate::models::market::MarketData; +use crate::models::farm::FarmData; +use crate::GeoffreyDatabaseModel; pub mod player; pub mod shop; pub mod item; +pub mod town; +pub mod farm; +pub mod market; +pub mod token; +mod meta; #[derive(Serialize, Deserialize, Debug, Clone)] pub enum LocationData { Base, Shop(ShopData), - Town, - Market, + Town(TownData), + Market(MarketData), Attraction, - PublicFarm, + PublicFarm(FarmData), } #[derive(Serialize, Deserialize, Debug, Copy, Clone)] @@ -52,7 +61,7 @@ pub struct Tunnel { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Location { - id: u64, + id: Option, name: String, position: Position, owners: Vec, @@ -61,11 +70,10 @@ pub struct Location { } - impl Location { - fn new (id: u64, name: &str, position: Position, owners: Vec, tunnel: Option, loc_data: LocationData) -> Self { + pub fn new (name: &str, position: Position, owners: Vec, tunnel: Option, loc_data: LocationData) -> Self { Location { - id, + id: None, name: name.to_string(), position, owners, @@ -75,6 +83,20 @@ impl Location { } } +impl GeoffreyDatabaseModel for Location { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id); + } + + fn tree() -> String { + "location".to_string() + } +} + #[cfg(test)] mod tests { use crate::models::{Location, Position, Dimension, LocationData}; @@ -82,8 +104,8 @@ mod tests { #[test] fn test_new_base() { - let player = Player {name: "CoolZero123".to_string(), user_ids: vec![UserID::DiscordUUID(0)]}; - Location::new(0, "test", Position {x: 0, y: 0, dimension: Dimension::Overworld}, vec![player], None, LocationData::Base); + let player = Player::new("CoolZero123", UserID::DiscordUUID(0)); + Location::new("test", Position {x: 0, y: 0, dimension: Dimension::Overworld}, vec![player], None, LocationData::Base); } } diff --git a/geoffrey_models/src/models/player.rs b/geoffrey_models/src/models/player.rs index 294c606..151a69b 100644 --- a/geoffrey_models/src/models/player.rs +++ b/geoffrey_models/src/models/player.rs @@ -1,14 +1,39 @@ use serde::{Deserialize, Serialize}; +use crate::GeoffreyDatabaseModel; #[derive(Serialize, Deserialize, Debug, Clone)] pub enum UserID { DiscordUUID(u64), MinecraftUUID(String), - GeoffreyID(u64) } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Player { + pub id: Option, pub name: String, pub user_ids: Vec +} + +impl Player { + pub fn new(name: &str, user_id: UserID) -> Self { + Self { + id: None, + name: name.to_string(), + user_ids: vec![user_id] + } + } +} + +impl GeoffreyDatabaseModel for Player { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id); + } + + fn tree() -> String { + "player".to_string() + } } \ No newline at end of file diff --git a/geoffrey_models/src/models/shop.rs b/geoffrey_models/src/models/shop.rs index d354a2f..93fab0b 100644 --- a/geoffrey_models/src/models/shop.rs +++ b/geoffrey_models/src/models/shop.rs @@ -27,16 +27,3 @@ pub struct ShopData { pub item_listings: Vec } -impl ShopData { - fn add_item(&mut self, item_listing: ItemListing) { - self.item_listings.push(item_listing); - } - - fn filter_items(&self, filter: F) -> Vec where F: FnMut(&&ItemListing) -> bool { - self.item_listings.iter().filter(filter).cloned().collect() - } - - fn remove_item(&mut self, item_name: &str) { - self.item_listings.retain(|listing| &listing.item.name != item_name) - } -} diff --git a/geoffrey_models/src/models/token.rs b/geoffrey_models/src/models/token.rs new file mode 100644 index 0000000..04a1410 --- /dev/null +++ b/geoffrey_models/src/models/token.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use crate::GeoffreyDatabaseModel; + +pub enum Permissions { + ModelGet = 0, + ModelPost, + Command, + ModCommand, + Admin, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct Token { + pub id: Option, + permission: u64, + pub created: DateTime, + pub modified: DateTime +} + +impl Token { + pub fn new() -> Self { + Self { + id: None, + permission: 0, + created: Utc::now(), + modified: Utc::now() + } + } + + pub fn set_permission(&mut self, permission: Permissions) { + self.permission = self.permission | (1u64 << permission as u32); + } + + pub fn clear_permission(&mut self, permission: Permissions) { + self.permission = self.permission & !(1u64 << permission as u32); + } + + pub fn check_permission(&self, permission: Permissions) -> bool { + (self.permission >> permission as u32) & 0x1u64 == 0x1u64 + } + + pub fn clear_all_permission(&mut self) { + self.permission = 0x0u64; + } +} + +impl GeoffreyDatabaseModel for Token { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id); + } + + fn tree() -> String { + "token".to_string() + } +} + +impl PartialEq for Token { + fn eq(&self, other: &Self) -> bool { + self.permission == other.permission + } +} + +#[cfg(test)] +mod tests { + use crate::models::token::{Token, Permissions}; + + #[test] + fn test_token() { + let mut token = Token::new(0); + + token.set_permission(Permissions::ModelGet); + assert_eq!(token.permission, 0x1u64); + + token.set_permission(Permissions::ModelPost); + assert_eq!(token.permission, 0x3u64); + + } +} + diff --git a/geoffrey_models/src/models/town.rs b/geoffrey_models/src/models/town.rs new file mode 100644 index 0000000..ab67e78 --- /dev/null +++ b/geoffrey_models/src/models/town.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +pub struct TownData { + pub resident: Vec +} +