221 lines
7.5 KiB
Rust
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);
|
|
}
|