[WIP] feature_database #8
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"
|
||||
|
@ -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
Normal file
@ -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
Normal file
@ -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
Normal file
@ -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
Normal file
@ -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
|
||||
|
||||
/// 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`)
|
||||
Ggg_123
commented
"Stores database in a JSON file"? "Stores database in a JSON file"?
|
||||
/// and providing several convenient accessor methods.
|
||||
#[derive(Debug)]
|
||||
pub struct File {
|
||||
Ggg_123
commented
Rename to JsonFile? 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.
|
||||
Ggg_123
commented
Empty file cannot contain anything. Empty file cannot contain anything.
|
||||
pub fn empty() -> File {
|
||||
File {
|
||||
contents: json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads JSON value from the specified file.
|
||||
Ggg_123
commented
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.
|
||||
Ggg_123
commented
,-? 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.
|
||||
Ggg_123
commented
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.
|
||||
Ggg_123
commented
raised raised
|
||||
///
|
||||
/// If array needs to be expanded, - missing values will be filled
|
||||
Ggg_123
commented
,-? ,-?
|
||||
/// with `json!(null)`, i.e. inserting `7` at index `5` in array `[1, 2, 3]`
|
||||
Ggg_123
commented
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.
|
||||
Ggg_123
commented
Missing space Missing space
|
||||
/// If it did not exist - returns `None`.
|
||||
Ggg_123
commented
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)`).
|
||||
Ggg_123
commented
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
|
||||
Ggg_123
commented
"too short" "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.
|
||||
Ggg_123
commented
,-? ,-?
|
||||
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)
|
||||
Ggg_123
commented
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
|
||||
Ggg_123
commented
"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.
|
||||
Ggg_123
commented
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
Normal file
@ -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:
|
||||
Ggg_123
commented
helper->Helper 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()
|
||||
Ggg_123
commented
Wont this silently cause errors by replacing given path that cannot be converted with empty strings? 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
Normal file
@ -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;
|
||||
Ggg_123
commented
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.
|
||||
Ggg_123
commented
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.
|
||||
Ggg_123
commented
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.
|
||||
Ggg_123
commented
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).
|
||||
Ggg_123
commented
Braces aren't needed here. Braces aren't needed here.
|
||||
/// Will produce error if file does not exist.
|
||||
Ggg_123
commented
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`.
|
||||
Ggg_123
commented
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`
|
||||
Ggg_123
commented
(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)
|
||||
}
|
||||
|
||||
Ggg_123
commented
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(),
|
||||
});
|
||||
}
|
374
src/database/tests.rs
Normal file
@ -0,0 +1,374 @@
|
||||
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);
|
||||
Ggg_123
commented
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 {
|
||||
Ggg_123
commented
Remove commented code? Remove commented code?
|
||||
Ok(db) => print!("{}", db),
|
||||
Err(error) => println!("OH NO: {}", error),
|
||||
}*/
|
||||
}
|
||||
|
Hard to read/understand. Possible improvements:
aan enum?is used internally?
inside JSON objects/arrays, using serde?
inside
ofJSON object?What is a "failed state"?