[WIP] feature_database #8

Open
dkanus wants to merge 2 commits from feature_database into master
  1. 169
      Cargo.lock
  2. 4
      Cargo.toml
  3. 13
      fixtures/database/administration/registered.json
  4. 12
      fixtures/database/game/general.json
  5. 12
      fixtures/database/game/perks.json
  6. 220
      src/database/file.rs
  7. 240
      src/database/io.rs
  8. 263
      src/database/mod.rs
  9. 380
      src/database/tests.rs
  10. 15
      src/main.rs

169
Cargo.lock generated

@ -1,6 +1,175 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "avarice"
version = "0.1.0"
dependencies = [
"custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)",
"num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "custom_error"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "itoa"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "log"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_json"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "simplelog"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"checksum chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
"checksum custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51ac5e99a7fea3ee8a03fa4721a47e2efd3fbb38358fc61192a54d4c6f866c12"
"checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
"checksum libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
"checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
"checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
"checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
"checksum serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)" = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
"checksum serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
"checksum simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2736f58087298a448859961d3f4a0850b832e72619d75adc69da7993c2cd3c"
"checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
"checksum time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
"checksum wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

4
Cargo.toml

@ -7,3 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
simplelog = "0.8"
log = "0.4"
serde_json = "1.0"
custom_error = "1.8.0"

13
fixtures/database/administration/registered.json

@ -0,0 +1,13 @@
{
"76561198025127722": {
"allowed_ips": ["127.0.0.1", "192.168.0.100"],
"groups": ["admin"],
"ip_lock": true,
"password_hash": "fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d"
},
"76561198044316328": {
"groups": ["admin"],
"ip_lock": false,
"password_hash": "fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d"
}
}

12
fixtures/database/game/general.json

@ -0,0 +1,12 @@
{
"76561198025127722": {
"walked": 1073,
"dosh_thrown": 483482,
"achievements": ["kf:LabCleaner", "kf:ChickenFarmer", "scrn:playedscrn"]
},
"76561198044316328": {
"walked": 1693,
"dosh_thrown": 527624,
"achievements": ["kf:PubCrawl", "kf:FascistDietitian", "kf:GimliThatAxe!", "scrn:playedscrn"]
}
}

12
fixtures/database/game/perks.json

@ -0,0 +1,12 @@
{
"76561198025127722": {
"headshots": 582,
"assault_rifle_damage": 9067,
"stalker_kills": 143
},
"76561198044316328": {
"explosive_damage": 19674,
"shotgun_damage": 3835,
"welded_amount": 1
}
}

220
src/database/file.rs

@ -0,0 +1,220 @@
use serde_json;
use serde_json::json;
use std::error::Error;
extern crate custom_error;
use custom_error::custom_error;
const JSON_POINTER_SEPARATOR: &str = "/";
custom_error! { pub IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}" }
/// This is a enum that used internally to refer to values inside of
Review

Hard to read/understand. Possible improvements:
aan enum?
is used internally?
inside JSON objects/arrays, using serde?
inside of JSON object?
What is a "failed state"?

Hard to read/understand. Possible improvements: ~~a~~an enum? *is* used internally? inside JSON objects/arrays, using serde? inside ~~of~~ JSON object? What is a "failed state"?
/// JSON (their serde implementation) objects and arrays.
/// This enum helps to simplify module's code.
///
/// For values inside of JSON object it stores object's
/// `Map<String, serde_json::Value>` and name of referred value.
///
/// For values inside JSON arrays it stores array's
/// `Vec<serde_json::Value>` and referred index.
///
/// `Invalid` can be used to return a failed state.
enum ValueReference<'a> {
Object(&'a mut serde_json::Map<String, serde_json::Value>, String),
Array(&'a mut Vec<serde_json::Value>, usize),
Invalid,
}
/// Implements database's file by wrapping JSON value (`serde_json::Value`)
Review

"Stores database in a JSON file"?

"Stores database in a JSON file"?
/// and providing several convenient accessor methods.
#[derive(Debug)]
pub struct File {
Review

Rename to JsonFile?
Also, it's not even a file.

Rename to JsonFile? Also, it's not even a file.
/// File's full contents, normally a JSON object.
contents: serde_json::Value,
}
impl ToString for File {
fn to_string(&self) -> String {
self.contents.to_string()
}
}
impl File {
/// Creates an empty file that will contain an empty JSON object.
Review

Empty file cannot contain anything.

Empty file cannot contain anything.
pub fn empty() -> File {
File {
contents: json!({}),
}
}
/// Loads JSON value from the specified file.
Review

But it accepts file_contents, not file/filepath?

But it accepts file_contents, not file/filepath?
pub fn load(file_contents: String) -> Result<File, Box<dyn Error>> {
Ok(File {
contents: serde_json::from_str(&file_contents)?,
})
}
/// Returns file's "root", - JSON value contained inside it.
Review

,-? Hard to understand.

,-? Hard to understand.
pub fn root(&self) -> &serde_json::Value {
&self.contents
}
/// Attempts to return JSON value, corresponding to the given JSON pointer.
Review

Remove "attempts"?

Remove "attempts"?
/// `None` if the value is missing.
pub fn get(&self, pointer: &str) -> Option<&serde_json::Value> {
self.contents.pointer(pointer)
}
/// Checks if values at a given JSON pointer exists.
/// Returns `true` if it does.
pub fn contains(&self, pointer: &str) -> bool {
self.get(pointer) != None
}
/// Inserts new JSON value inside this file
/// (possibly in some sub-object/array).
///
/// Given pointer must point at new value:
/// 1. If it already exists, - it will be overwritten.
/// 2. If it does not exist, but it's parent object/array does -
/// it will be added.
/// 3. Otherwise an error will be raise.
Review

raised

raised
///
/// If array needs to be expanded, - missing values will be filled
Review

,-?

,-?
/// with `json!(null)`, i.e. inserting `7` at index `5` in array `[1, 2, 3]`
Review

Sounds like a hack/bug - why is it like this?

Sounds like a hack/bug - why is it like this?
/// will produce `[1, 2, 3, null, null, 7]`.
pub fn insert(
&mut self,
pointer: &str,
new_value: serde_json::Value,
) -> Result<(), IncorrectPointer> {
self.touch(pointer)?;
match self.contents.pointer_mut(pointer) {
Some(v) => *v = new_value,
_ => {
// If after `touch()` call we still don't have an existing value -
// something is wrong with the `pointer`
return Err(IncorrectPointer {
pointer: pointer.to_owned(),
});
}
};
Ok(())
}
/// Removes (and returns) value specified by theJSON pointer.
Review

Missing space

Missing space
/// If it did not exist - returns `None`.
Review

Remove 2nd line? A simple "if it exists" should be enough.

Remove 2nd line? A simple "if it exists" should be enough.
pub fn remove(&mut self, pointer: &str) -> Option<serde_json::Value> {
match self.pointer_to_reference(pointer) {
ValueReference::Object(map, variable_name) => map.remove(&variable_name),
ValueReference::Array(vec, variable_index) => {
if variable_index < vec.len() {
return Some(vec.remove(variable_index));
}
None
}
_ => None,
}
}
/// Helper method to create a value if missing (as `json!(null)`).
Review

What's missing?

What's missing?
/// Can only be done if parent container already exists.
///
/// For specifics refer to `insert()` method.
fn touch(&mut self, pointer: &str) -> Result<(), IncorrectPointer> {
// If value is present - we're done
if pointer.is_empty() || self.contents.pointer_mut(pointer).is_some() {
return Ok(());
}
// Otherwise - try to create it
match self.pointer_to_reference(pointer) {
ValueReference::Object(map, variable_name) => {
map.insert(variable_name, json!(null));
}
ValueReference::Array(vec, variable_index) => {
// We've checked at the beginning of this method that value
// at `variable_index` does not exist, which guarantees
// that array is to short and we won't shrink it
Review

"too short"
Also, the " We've checked ... we won't shrink it" part is hard to understand - checked what where? Why can't we shrink it? How are those 2 things connected?

"too short" Also, the " We've checked ... we won't shrink it" part is hard to understand - checked what where? Why can't we shrink it? How are those 2 things connected?
vec.resize(variable_index + 1, json!(null));
}
_ => {
return Err(IncorrectPointer {
pointer: pointer.to_owned(),
})
}
};
Ok(())
}
/// Helper method, - converts JSON pointer into auxiliary `ValueReference` enum.
Review

,-?

,-?
fn pointer_to_reference<'a>(&'a mut self, pointer: &str) -> ValueReference<'a> {
if pointer.is_empty() {
return ValueReference::Invalid;
}
// Extract variable name (that `pointer` points to)
Review

Need a link to https://tools.ietf.org/html/rfc6901 somewhere to explain what exactly "JSON pointer" is.

Need a link to https://tools.ietf.org/html/rfc6901 somewhere to explain what exactly "JSON pointer" is.
// and reference to it's container
//
// i.e. given file with '{"obj":{"arr":[1,3,5,2,4]}}',
// for pointer `/obj/arr/5`,
// it will return, basically, `(&[1,3,5,2,4], "5")`
let container_variable_pair =
pop_json_pointer(pointer).and_then(move |(path, variable_name)| {
match self.contents.pointer_mut(&path) {
Some(v) => Some((v, variable_name)),
_ => None,
}
});
let (json_container, variable_name) = match container_variable_pair {
Some(v) => v,
_ => return ValueReference::Invalid,
};
// For arrays we also need to confirm validity of the variable name
Review

"validity of the variable name" - which means?..

"validity of the variable name" - which means?..
// and convert it into `usize`
match json_container {
serde_json::Value::Object(map) => ValueReference::Object(map, variable_name),
serde_json::Value::Array(vec) => {
let index: usize = match variable_name.parse() {
Ok(v) => v,
_ => return ValueReference::Invalid,
};
ValueReference::Array(vec, index)
}
_ => ValueReference::Invalid,
}
}
}
// Helper function to disassemble JSON path.
Review

Into what?

Into what?
fn pop_json_pointer(pointer: &str) -> Option<(String, String)> {
let mut pointer = pointer.to_string();
let last_separator_index = match pointer.rfind(JSON_POINTER_SEPARATOR) {
Some(v) => v,
_ => {
return None;
}
};
if last_separator_index >= pointer.len() {
pointer.pop();
return Some((pointer, String::new()));
}
let var_name = pointer.split_off(last_separator_index + 1);
pointer.pop();
Some((pointer, var_name))
}
#[test]
fn test_pop_json_pointer() {
assert_eq!(
pop_json_pointer("/a/b/c/d"),
Some(("/a/b/c".to_owned(), "d".to_owned()))
);
assert_eq!(pop_json_pointer("/"), Some(("".to_owned(), "".to_owned())));
assert_eq!(
pop_json_pointer("/a/b/"),
Some(("/a/b".to_owned(), "".to_owned()))
);
assert_eq!(pop_json_pointer(""), None);
// This pointer is incorrect
assert_eq!(pop_json_pointer("var"), None);
}

240
src/database/io.rs

@ -0,0 +1,240 @@
use super::*;
use log::{error, info, warn};
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::path;
use std::path::Path;
extern crate custom_error;
use custom_error::custom_error;
const JSON_EXTENSION: &str = "json";
custom_error! { pub IOError
NotDirectory{path: String} = "Path to the database should point at a directory: {path}",
}
/// Reads database data from a directory at the specified path
/// as a vector of `Group`s.
pub fn read(db_path: &path::Path) -> Result<Vec<Group>, Box<dyn Error>> {
if !db_path.is_dir() {
error!(
"Loading database from a non-directory {} was attempted.",
db_path.display()
);
return Err(Box::new(IOError::NotDirectory {
path: db_path.display().to_string(),
}));
}
info!("Loading database from {}.", db_path.display());
let mut groups = Vec::new();
for entry in fs::read_dir(db_path)? {
let path = match entry {
Ok(r) => r,
_ => continue,
}
.path();
if !check_valid_group_dir(path.as_path()) {
continue;
}
match read_group(&path)? {
Some(g) => groups.push(g),
_ => (),
}
}
info!("Correctly finished leading database.");
Ok(groups)
}
/// Writes data of the given database into the directory specified by the path.
/// Does not clear it from any previously existing files,
/// you can use `clear_dir()` for that.
pub fn write(db_path: &path::Path, db: &Database) -> Result<(), Box<dyn Error>> {
if db_path.exists() && !db_path.is_dir() {
error!(
"Cannot write database into a non-directory {}",
db_path.display()
);
return Err(Box::new(IOError::NotDirectory {
path: db_path.display().to_string(),
}));
}
fs::create_dir(db_path)?;
for group in db.group_names().iter() {
let group_path = db_path.join(group);
if !group_path.exists() || !group_path.is_dir() {
fs::create_dir(group_path.clone())?;
}
for file in db.file_names_in(group).iter() {
let file_path = group_path.join(format!("{}.{}", file, JSON_EXTENSION));
match db.file(group, file) {
Some(file) => fs::write(file_path, file.to_string())?,
_ => (),
}
}
}
Ok(())
}
/// Given a path to directory that was used for database storage -
/// clears it's data.
///
/// This means removing any '.json' files from all immediate subdirectories and
/// then removing any subdirectories that were or became empty.
pub fn clear_dir(db_path: &path::Path) -> Result<(), Box<dyn Error>> {
info!(
"Clearing directory {} from database files.",
db_path.display()
);
if !db_path.exists() {
info!("Directory not found, nothing to do.");
return Ok(());
}
for entry in fs::read_dir(db_path)? {
let dir_path = match entry {
Ok(r) => r,
_ => continue,
}
.path();
if !check_valid_group_dir(dir_path.as_path()) {
continue;
}
for entry in fs::read_dir(dir_path.clone())? {
let file_path = match entry {
Ok(r) => r,
_ => continue,
}
.path();
if !check_valid_data_file(file_path.as_path()) {
continue;
}
fs::remove_file(file_path)?;
}
let _ = fs::remove_dir(dir_path);
}
let _ = fs::remove_dir(db_path);
info!("Correctly finished clearing database files.");
Ok(())
}
/// helper function to read a group from a given subdirectory:
Review

helper->Helper
Containing what?

helper->Helper Containing what?
/// loads data from all containing '.json' files.
fn read_group(group_path: &path::Path) -> Result<Option<Group>, Box<dyn Error>> {
let mut files = HashMap::new();
for entry in fs::read_dir(group_path)? {
let path = match entry {
Ok(r) => r,
_ => continue,
}
.path();
if !check_valid_data_file(path.as_path()) {
continue;
}
let file_name = get_file_name(path.as_path());
let file_contents = fs::read_to_string(&path)?;
files.insert(file_name, File::load(file_contents)?);
}
if files.len() > 0 {
return Ok(Some(Group {
name: get_file_name(group_path),
files,
}));
}
Ok(None)
}
/// Checks if given path points at a directory that can represent
/// a database group (has a valid name).
fn check_valid_group_dir(dir_path: &path::Path) -> bool {
if !dir_path.is_dir() {
warn!(
r#"Skipping {}, because only directories are expected in database's root."#,
dir_path.display()
);
return false;
}
if !is_name_valid(&get_file_name(dir_path)) {
warn!(
r#"Skipping directory {}, because it does not have a valid name."#,
dir_path.display()
);
return false;
}
true
}
/// Checks if given path points at a file that can represent
/// a database group (has a valid name).
fn check_valid_data_file(file_path: &path::Path) -> bool {
if file_path.is_dir() {
warn!(
r#"Skipping directory {}, because group directories are only\
supposed to contain files."#,
file_path.display()
);
return false;
}
let name = get_file_name(file_path);
if !is_name_valid(&name) {
warn!(
r#"Skipping file {}, because it does not have a valid name."#,
file_path.display()
);
return false;
}
let extension = get_file_extension(file_path);
if !JSON_EXTENSION.eq_ignore_ascii_case(&extension) {
warn!(
r#"Skipping file {}, because it does not have "json" extension."#,
file_path.display()
);
return false;
}
true
}
fn get_file_name(path: &path::Path) -> String {
path.file_stem()
.and_then(|x| x.to_str())
.unwrap_or_default()
Review

Wont this silently cause errors by replacing given path that cannot be converted with empty strings?
Same thing in fn below this one.

Wont this silently cause errors by replacing given path that cannot be converted with empty strings? Same thing in fn below this one.
.to_string()
}
fn get_file_extension(path: &path::Path) -> String {
path.extension()
.and_then(|x| x.to_str())
.unwrap_or_default()
.to_string()
}
#[test]
fn test_file_name_extension_extraction() {
assert_eq!(get_file_name(Path::new("/dir/file")), "file".to_owned());
assert_eq!(
get_file_name(Path::new("/dir/sub_dir/some.ext")),
"some".to_owned()
);
assert_eq!(
get_file_name(Path::new("/dir/sub_dir/.ext")),
".ext".to_owned()
);
assert_eq!(
get_file_name(Path::new("/dir/sub_dir/thing.")),
"thing".to_owned()
);
assert_eq!(get_file_extension(Path::new("/dir/file")), "".to_owned());
assert_eq!(
get_file_extension(Path::new("/dir/sub_dir/some.ext")),
"ext".to_owned()
);
assert_eq!(
get_file_extension(Path::new("/dir/sub_dir/.ext")),
"".to_owned()
);
assert_eq!(
get_file_extension(Path::new("/dir/sub_dir/thing.")),
"".to_owned()
);
}

263
src/database/mod.rs

@ -0,0 +1,263 @@
#[cfg(test)]
mod tests;
use serde_json;
use std::collections::HashMap;
use std::error::Error;
use std::path;
extern crate custom_error;
use custom_error::custom_error;
pub mod file;
pub use file::File;
pub mod io;
custom_error! { pub DBError
InvalidEntityName{entity_name: String} = r#"Cannot use {entity_name} for file or group"#,
NoGroup{group_name: String} = r#"Group "{group_name}" does not exist"#,
NoFile{group_name: String, file_name: String} = r#"There is no "{file_name}" file in group\
"{group_name}""#,
GroupAlreadyExists{group_name: String} = r#"Group "{group_name}" already exists"#,
FileAlreadyExists{group_name: String, file_name: String} = r#"File "{file_name}" already exists\
in the group "{group_name}""#,
}
/// Avarice database is a collection of named JSON values (by default objects),
/// separated into different names groups. Names of such groups and JSON values must only contain
/// numbers and latin letters (ASCII subset).
///
/// This database is only supposed to hold a relatively small amount of data
/// that:
///
/// 1. can be freely and full loaded into memory;
Review

full -> fully

full -> fully
/// 2. then saved all at once.
///
/// Database is loaded and saved on the disk as a directory,
/// that contains subdirectories (with valid names) for each group;
/// those subdirectories in turn must contain "*.json" files (with valid names)
/// that correspond to the stored JSON values.
///
/// Database directory should not contain any other files, but their presence
/// should not prevent database from loading (such files should be ignored).
pub struct Database {
/// Path to the database's directory
storage_path: path::PathBuf,
/// Collection of groups (of JSON values) inside of database
groups: Vec<Group>,
}
/// Represents a database's group: a ste of named files
pub struct Group {
/// Name of the group
name: String,
/// Maps file names with their contents (as file::File structures)
files: HashMap<String, File>,
}
impl Database {
/// Creates new database by loading it from the specified directory.
/// Directory must contain valid database and be readable.
pub fn load(storage_path: &path::Path) -> Result<Database, Box<dyn Error>> {
Ok(Database {
storage_path: storage_path.to_path_buf(),
groups: io::read(storage_path)?,
})
}
/// Removes all data from the database.
pub fn clear(&mut self) {
self.groups = Vec::new();
}
/// Returns path from which this database was loaded (can be changed with `change_path()`).
pub fn path(&mut self) -> path::PathBuf {
self.storage_path.clone()
}
/// Changes current path of this database. All operations with files will use this path,
/// unless stated otherwise.
/// Directory must contain valid database and be readable.
///
/// This method will also remove all data from the current database's path
/// and fail if it can't.
Review

fail -> will fail

fail -> will fail
pub fn change_path(&mut self, new_path: &path::Path) -> Result<(), Box<dyn Error>> {
self.write_copy(new_path)?;
io::clear_dir(&self.storage_path)?;
self.storage_path = new_path.to_path_buf();
Ok(())
}
/// Erases database's data on disk.
pub fn erase(self) -> Result<(), Box<dyn Error>> {
io::clear_dir(&self.storage_path)?;
Ok(())
}
/// Writes copy of the current database into specified directory.
/// Erases any preexisting database files.
///
/// Empty group won't be saved.
pub fn write_copy(&self, new_path: &path::Path) -> Result<(), Box<dyn Error>> {
io::clear_dir(new_path)?;
io::write(new_path, &self)?;
Ok(())
}
/// Writes current state of the database on the disk.
///
/// Empty group won't be saved.
pub fn save(&self) -> Result<(), Box<dyn Error>> {
self.write_copy(&self.storage_path)?;
Ok(())
}
/// Returns names of all the groups in the database.
pub fn group_names(&self) -> Vec<String> {
self.groups.iter().map(|x| x.name.clone()).collect()
}
/// Returns names of all the files in a particular group.
pub fn file_names_in(&self, group_name: &str) -> Vec<String> {
match self.groups.iter().find(|x| x.name.eq(group_name)) {
Some(group) => group.files.keys().map(|x| x.clone()).collect(),
None => Vec::new(),
}
}
/// Checks if specified group exists in the database
pub fn contains_group(&self, group_name: &str) -> bool {
self.group_index(group_name).is_ok()
}
/// Creates a new empty group.
/// Will produce error if group already exists.
Review

Produce -> return

Produce -> return
pub fn create_group(&mut self, group_name: &str) -> Result<(), DBError> {
assert_name_is_valid(group_name)?;
self.assert_no_group(group_name)?;
self.groups.push(Group {
name: group_name.to_owned(),
files: HashMap::new(),
});
Ok(())
}
/// Removes specified group.
/// Will produce error if group does not exist.
pub fn remove_group(&mut self, group_name: &str) -> Result<(), DBError> {
let index = self.group_index(group_name)?;
self.groups.remove(index);
Ok(())
}
/// Checks if specified file (in a specified group) is contained in the database.
Review

contained -> stored

contained -> stored
pub fn contains_file(&self, group_name: &str, file_name: &str) -> bool {
self.group_files(group_name)
.and_then(|x| Ok(x.contains_key(&file_name.to_owned())))
.unwrap_or(false)
}
/// Creates new file in the specified group that will contain an empty JSON object.
/// Will produce error if file already exists.
pub fn create_file(&mut self, group_name: &str, file_name: &str) -> Result<&mut File, DBError> {
assert_name_is_valid(&file_name)?;
let files = self.group_files_mut(group_name)?;
if files.contains_key(&file_name.to_owned()) {
return Err(DBError::FileAlreadyExists {
group_name: group_name.to_owned(),
file_name: file_name.to_owned(),
});
}
let new_file = File::empty();
files.insert(file_name.to_owned(), new_file);
Ok(files
.get_mut(file_name)
.expect("Missing value that was just inserted."))
}
/// Removes specified file (in a specified group).
Review

Braces aren't needed here.

Braces aren't needed here.
/// Will produce error if file does not exist.
Review

produce -> return

produce -> return
pub fn remove_file(&mut self, group_name: &str, file_name: &str) -> Result<(), DBError> {
if self
.group_files_mut(group_name)?
.remove(file_name)
.is_none()
{
return Err(DBError::NoFile {
group_name: group_name.to_owned(),
file_name: file_name.to_owned(),
});
}
Ok(())
}
/// Returns immutable reference to the specified file (in a specified group) as `file::File`.
Review

Braces aren't needed here.

Braces aren't needed here.
/// `None` if file does not exist.
pub fn file_mut(&mut self, group_name: &str, file_name: &str) -> Option<&mut File> {
match self.group_files_mut(group_name) {
Ok(files) => files.get_mut(&file_name.to_owned()),
_ => None,
}
}
/// Returns mutable reference to the specified file (in a specified group) as `file::File`.
/// `None` if file does not exist.
pub fn file(&self, group_name: &str, file_name: &str) -> Option<&File> {
match self.group_files(group_name) {
Ok(files) => files.get(&file_name.to_owned()),
_ => None,
}
}
/// Helper method that raises error if specified group exists.
fn assert_no_group(&self, group_name: &str) -> Result<(), DBError> {
if self.group_index(group_name).is_ok() {
return Err(DBError::GroupAlreadyExists {
group_name: group_name.to_owned(),
});
}
Ok(())
}
/// Returns current index of the specified group in`groups` vector.
fn group_index(&self, group_name: &str) -> Result<usize, DBError> {
match self.groups.iter().position(|x| x.name.eq(group_name)) {
Some(index) => Ok(index),
_ => {
return Err(DBError::NoGroup {
group_name: group_name.to_owned(),
})
}
}
}
// Helper methods that return (im)mutable reference to `HashMap`
Review

(im)? Why?

(im)? Why?
// (of 'file_name -> file' map) for a particular group.
fn group_files_mut(&mut self, group_name: &str) -> Result<&mut HashMap<String, File>, DBError> {
let group_index = self.group_index(group_name)?;
Ok(&mut (&mut self.groups[group_index]).files)
}
Review

Missing doc.

Missing doc.
fn group_files(&self, group_name: &str) -> Result<&HashMap<String, File>, DBError> {
let group_index = self.group_index(group_name)?;
Ok(&self.groups[group_index].files)
}
}
/// Name validity check (for groups and files)
fn is_name_valid(entity_name: &str) -> bool {
entity_name
.chars()
.all(|x| x.is_ascii_alphabetic() || x.is_ascii_digit())
}
/// Helper function that raises error if passed name is invalid
fn assert_name_is_valid(entity_name: &str) -> Result<(), DBError> {
if is_name_valid(entity_name) {
return Ok(());
}
return Err(DBError::InvalidEntityName {
entity_name: entity_name.to_owned(),
});
}

380
src/database/tests.rs

@ -0,0 +1,380 @@
use super::*;
use serde_json::json;
use std::fs;
use std::path;
const TEST_DB_PATH: &str = "./fixtures/database";
const TEST_DB_MOVED_PATH: &str = "./fixtures/moved";
const NO_DB_MESSAGE: &str = "Can not find/load test database";
struct TestCleanup {
path: String,
clear_moved: bool,
}
impl Drop for TestCleanup {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
Review

Why _?

Why _?
if self.clear_moved {
let _ = fs::remove_dir_all(TEST_DB_MOVED_PATH);
}
}
}
fn prepare_db_copy(copy_id: &str, clear_moved: bool) -> (String, TestCleanup) {
let path = format!("{}_{}", TEST_DB_PATH, copy_id);
let original_db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
original_db
.write_copy(path::Path::new(&path))
.expect("Should be able to create a new copy of the fixture database.");
(
path.clone(),
TestCleanup {
path: path.to_owned(),
clear_moved: clear_moved,
},
)
}
#[test]
fn db_path() {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
assert!(db.path() == path::Path::new(TEST_DB_PATH));
}
#[test]
fn db_clear() {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
db.clear();
assert!(!db.contains_group("game"));
assert!(!db.contains_file("administration", "registered"));
let names = db.group_names();
assert_eq!(names.len(), 0);
}
#[test]
fn db_group_names() {
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
let names = db.group_names();
assert!(names.contains(&"administration".to_owned()));
assert!(names.contains(&"game".to_owned()));
assert_eq!(names.len(), 2);
}
#[test]
fn db_file_names() {
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
let names_admin = db.file_names_in("administration");
let names_game = db.file_names_in("game");
assert!(names_admin.contains(&"registered".to_owned()));
assert!(names_game.contains(&"general".to_owned()));
assert!(names_game.contains(&"perks".to_owned()));
assert_eq!(names_admin.len(), 1);
assert_eq!(names_game.len(), 2);
}
#[test]
fn db_group_check() {
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
assert!(db.contains_group("game"));
assert!(db.contains_group("administration"));
assert!(!db.contains_group("perks"));
assert!(!db.contains_group("7random7"));
assert!(!db.contains_group(""));
}
#[test]
fn db_group_remove() -> Result<(), Box<dyn Error>> {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
// Success
db.remove_group("administration")?;
db.remove_group("game")?;
assert!(!db.contains_group("administration"));
assert!(!db.contains_group("game"));
// Failure
db.remove_group("test")
.expect_err("Testing whether removing non-existent groups with incorrect ASCII characters causes errors.");
db.remove_group("administration")
.expect_err("Testing whether removing non-existent groups with incorrect ASCII characters causes errors.");
db.remove_group("game group").expect_err(
"Testing whether removing non-existent groups with whitespace characters causes errors.",
);
Ok(())
}
#[test]
fn db_group_create() -> Result<(), Box<dyn Error>> {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
// Success
db.create_group("7random7")?;
assert!(db.contains_group("7random7"));
assert!(db.contains_group("game"));
// Failure
db.create_group("my_group").expect_err(
"Testing whether creating groups with incorrect ASCII characters causes errors.",
);
db.create_group("my group")
.expect_err("Testing whether creating groups with whitespace characters causes errors.");
db.create_group("Жgroup")
.expect_err("Testing whether creating groups with non-ASCII characters causes errors.");
// Create after removal
db.remove_group("game")?;
assert!(!db.contains_group("game"));
db.create_group("game")?;
assert!(db.contains_group("game"));
assert!(db.file_names_in("game").is_empty());
Ok(())
}
#[test]
fn db_file_check() {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
// Success
assert!(db.contains_file("administration", "registered"));
assert!(db.file("game", "general").is_some());
assert!(db.file_mut("game", "perks").is_some());
// Failure
assert!(!db.contains_file("game", "perk"));
assert!(db.file("games", "perks").is_none());
assert!(db.file_mut("random", "rnd_file").is_none());
}
#[test]
fn db_file_create() -> Result<(), Box<dyn Error>> {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
// Success
let file = db.create_file("administration", "secrets")?;
assert_eq!(file.to_string(), "{}");
assert!(db.contains_file("administration", "secrets"));
assert!(db.contains_file("game", "perks"));
// Failure
db.create_file("administration", "secrets")
.expect_err("Testing whether creating existing file causes errors.");
db.create_file("none", "secrets")
.expect_err("Testing whether creating existing file in non-existent group causes errors.");
db.create_file("game", "sec_rets")
.expect_err("Testing whether creating existing file with invalid name causes errors.");
Ok(())
}
#[test]
fn db_file_remove() -> Result<(), Box<dyn Error>> {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
// Success
db.remove_file("administration", "registered")?;
assert!(!db.contains_file("administration", "registered"));
assert!(db.contains_group("administration"));
db.remove_file("game", "perks")?;
assert_eq!(db.file_names_in("game").len(), 1);
// Failure
db.remove_file("administration", "registered")
.expect_err("Testing whether removing non-existent files causes errors.");
db.remove_file("administration", "never")
.expect_err("Testing whether removing non-existent files causes errors.");
db.remove_file("never", "file")
.expect_err("Testing whether removing non-existent files causes errors.");
Ok(())
}
#[test]
fn file_json_contents() {
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
let registered = db.file("administration", "registered").unwrap().root();
let user_map = registered
.as_object()
.expect("Read value is not an object.");
assert_eq!(user_map.len(), 2);
assert!(user_map.contains_key("76561198025127722"));
let user_record = user_map
.get("76561198044316328")
.unwrap()
.as_object()
.unwrap();
assert_eq!(user_record.len(), 3);
assert_eq!(user_record.get("ip_lock").unwrap().as_bool(), Some(false));
assert_eq!(
user_record.get("password_hash").unwrap().as_str(),
Some("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d")
);
let groups_arrays = user_record.get("groups").unwrap().as_array().unwrap();
assert_eq!(groups_arrays.len(), 1);
assert_eq!(groups_arrays[0], "admin");
}
#[test]
fn file_json_get() {
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
// Test empty path
let file = db.file("administration", "registered").unwrap();
assert_eq!(file.root(), file.get("").unwrap());
// Test complex path
let expected = file.get("/76561198025127722/allowed_ips/1").unwrap();
assert_eq!(expected.as_str().unwrap(), "192.168.0.100");
// Test bad paths
assert!(file.get("/777") == None);
assert!(file.get("/76561198025127722/allowed_ips/2") == None);
}
#[test]
fn file_contains_check() {
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
let registered_file = db.file("administration", "registered").unwrap();
let perks_file = db.file("game", "perks").unwrap();
// These exist
assert!(registered_file.contains("/76561198025127722/password_hash"));
assert!(registered_file.contains("/76561198044316328/groups/0"));
assert!(perks_file.contains("/76561198025127722/headshots"));
assert!(perks_file.contains("/76561198044316328"));
// These do not exist
assert!(!registered_file.contains("/76561198025127722/password/"));
assert!(!registered_file.contains("/76561198044316328/groups/2"));
assert!(!perks_file.contains("/76561198025127722/assault_rifle_damage/9067"));
assert!(!perks_file.contains("/76561198044316328/headshots"));
}
#[test]
fn db_insert_success() -> Result<(), Box<dyn Error>> {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
let registered_file = db.file_mut("administration", "registered").unwrap();
// Modify existing
registered_file.insert("/76561198025127722/ip_lock", json!(false))?;
assert_eq!(
registered_file
.get("/76561198025127722/ip_lock")
.unwrap()
.to_string(),
"false"
);
registered_file.insert("/76561198044316328/password_hash", json!({"var":13524}))?;
assert_eq!(
registered_file
.get("/76561198044316328/password_hash")
.unwrap()
.to_string(),
r#"{"var":13524}"#
);
// Reset whole file
registered_file.insert("", json!({}))?;
assert_eq!(registered_file.root().to_string(), "{}");
// Add new values
registered_file.insert("/new_var", json!([42, {"word":"life"}, null]))?;
assert_eq!(
registered_file.root().to_string(),
r#"{"new_var":[42,{"word":"life"},null]}"#
);
let general_file = db.file_mut("game", "general").unwrap();
general_file.insert("/76561198025127722/achievements/5", json!("kf:bugged"))?;
assert_eq!(
general_file
.get("/76561198025127722/achievements")
.unwrap()
.to_string(),
r#"["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn",null,null,"kf:bugged"]"#
);
Ok(())
}
#[test]
fn db_set_failure() {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
let file = db.file_mut("administration", "registered").unwrap();
file.insert("/76561198025127722/dir/var", json!(null))
.expect_err("Testing panic at trying to set a value in non-existing object/array.");
file.insert("/76561198044316328/groups/d", json!(null))
.expect_err("Testing panic at trying to set a value at non-numeric index in an array.");
file.insert("/76561198044316328/groups/-1", json!(null))
.expect_err("Testing panic at trying to set a value at negative index in an array.");
}
#[test]
fn db_remove_value() {
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE);
let file = db.file_mut("administration", "registered").unwrap();
// Removing non-existent value
assert_eq!(file.remove("/76561198025127722/something"), None);
// Remove simple value
assert_eq!(
file.remove("/76561198025127722/password_hash").unwrap(),
json!("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d")
);
assert!(!file.contains("/76561198025127722/password_hash"));
// Remove complex value (array)
assert_eq!(
file.remove("/76561198044316328/groups").unwrap(),
json!(["admin"])
);
assert!(!file.contains("/76561198044316328/groups/0"));
assert!(!file.contains("/76561198044316328/groups"));
// Remove array elements
assert_eq!(
file.remove("/76561198025127722/allowed_ips/0").unwrap(),
json!("127.0.0.1")
);
assert!(file.contains("/76561198025127722/allowed_ips/0"));
assert!(!file.contains("/76561198025127722/allowed_ips/1"));
assert_eq!(
file.remove("/76561198025127722/allowed_ips/0").unwrap(),
json!("192.168.0.100")
);
assert!(file.contains("/76561198025127722/allowed_ips"));
assert!(!file.contains("/76561198025127722/allowed_ips/0"));
}
#[test]
fn db_save() {
let (path, _cleanup) = prepare_db_copy("db_save", false);
// Change something up and save
let mut db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE);
db.remove_group("administration")
.expect(r#"Should be able to remove "administration" group"#);
db.save()
.expect("Should be able to save copy of the database.");
// Reload and check changes
let db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE);
assert_eq!(db.group_names().len(), 1);
assert_eq!(db.group_names().get(0), Some(&"game".to_owned()));
assert_eq!(db.file_names_in("game").len(), 2);
assert!(db.contains_file("game", "general"));
assert!(db.contains_file("game", "perks"));
}
#[test]
fn db_change_path() {
let (path, _cleanup) = prepare_db_copy("db_change_path", true);
// Change something up and move
let mut db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE);
db.remove_group("administration")
.expect(r#"Should be able to remove "administration" group"#);
db.file_mut("game", "perks")
.unwrap()
.insert("", json!({"var":7}))
.expect("Should be able to insert into root.");
db.change_path(path::Path::new(TEST_DB_MOVED_PATH))
.expect("Should be able to change database's path.");
assert!(!path::Path::new(&path).exists());
assert!(path::Path::new(TEST_DB_MOVED_PATH).exists());
// Reload and check the changes
let db = Database::load(path::Path::new(TEST_DB_MOVED_PATH)).expect(NO_DB_MESSAGE);
assert_eq!(db.group_names().len(), 1);
assert_eq!(db.group_names().get(0), Some(&"game".to_owned()));
assert_eq!(db.file_names_in("game").len(), 2);
assert!(db.contains_file("game", "general"));
assert!(db.contains_file("game", "perks"));
assert_eq!(
db.file("game", "perks").unwrap().root().to_string(),
r#"{"var":7}"#.to_owned()
);
}
#[test]
fn db_erase() {
let (path, _cleanup) = prepare_db_copy("db_erase", false);
let db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE);
db.erase().expect("Should be able to erase data.");
assert!(!path::Path::new(&path).exists());
}

15
src/main.rs

@ -1,13 +1,16 @@
use std::env;
use std::path::Path;
mod unreal_config;
mod database;
use simplelog::{Config, LevelFilter, SimpleLogger};
fn main() {
let _ = SimpleLogger::init(LevelFilter::Info, Config::default());
let args: Vec<String> = env::args().collect();
let filename = &args[1];
let config = unreal_config::load_file(Path::new(filename));
match config {
Ok(config) => print!("{}", config),
_ => (),
}
let db = database::Database::load(Path::new(filename));
/*match db {
Review

Remove commented code?

Remove commented code?
Ok(db) => print!("{}", db),
Err(error) => println!("OH NO: {}", error),
}*/
}

Loading…
Cancel
Save