Avarice/src/database/file.rs
2020-11-28 04:43:36 +07:00

221 lines
7.5 KiB
Rust

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`)
/// and providing several convenient accessor methods.
#[derive(Debug)]
pub struct 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.
pub fn empty() -> File {
File {
contents: json!({}),
}
}
/// Loads JSON value from the specified file.
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.
pub fn root(&self) -> &serde_json::Value {
&self.contents
}
/// Attempts to return JSON value, corresponding to the given JSON pointer.
/// `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.
///
/// If array needs to be expanded, - missing values will be filled
/// with `json!(null)`, i.e. inserting `7` at index `5` in array `[1, 2, 3]`
/// 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.
/// If it did not exist - returns `None`.
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)`).
/// 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
vec.resize(variable_index + 1, json!(null));
}
_ => {
return Err(IncorrectPointer {
pointer: pointer.to_owned(),
})
}
};
Ok(())
}
/// Helper method, - converts JSON pointer into auxiliary `ValueReference` enum.
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)
// 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
// 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.
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);
}