#![warn(clippy::all)]
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
use serde_json::Value;
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
pub static ROOT_NAME: &str = "<root>";
#[derive(Debug, PartialEq)]
pub struct FlareTreeNode {
name: OsString,
is_file: bool,
children: Vec<FlareTreeNode>,
data: HashMap<String, Value>,
}
impl FlareTreeNode {
pub fn name(&self) -> &OsString {
&self.name
}
pub fn new<S: Into<OsString>>(name: S, is_file: bool) -> FlareTreeNode {
FlareTreeNode {
name: name.into(),
is_file,
children: Vec::new(),
data: HashMap::new(),
}
}
#[cfg(test)]
pub fn file<S: Into<OsString>>(name: S) -> Self {
Self::new(name, true)
}
#[cfg(test)]
pub fn dir<S: Into<OsString>>(name: S) -> Self {
Self::new(name, false)
}
pub fn add_data<S: Into<String>>(&mut self, key: S, value: Value) {
self.data.insert(key.into(), value); }
pub fn append_child(&mut self, child: FlareTreeNode) {
if self.is_file {
panic!("appending child to a directory: {:?}", self)
}
self.children.push(child); }
#[allow(dead_code)] pub fn get_in(&self, path: &mut std::path::Components) -> Option<&FlareTreeNode> {
match path.next() {
Some(first_name) => {
let dir_name = first_name.as_os_str();
if !self.is_file {
let first_match = self.children.iter().find(|c| dir_name == c.name)?;
return first_match.get_in(path);
}
None
}
None => Some(self),
}
}
pub fn get_in_mut(&mut self, path: &mut std::path::Components) -> Option<&mut FlareTreeNode> {
match path.next() {
Some(first_name) => {
let dir_name = first_name.as_os_str();
if !self.is_file {
let first_match = self.children.iter_mut().find(|c| dir_name == c.name)?;
return first_match.get_in_mut(path);
}
None
}
None => Some(self),
}
}
pub fn get_data(&self, key: &str) -> Option<&Value> {
self.data.get(key)
}
pub fn get_children(&self) -> &Vec<FlareTreeNode> {
&self.children
}
}
fn name_as_str<S: Serializer>(name: &OsStr) -> Result<&str, S::Error> {
name.to_str().ok_or_else(|| {
serde::ser::Error::custom(format!("name {:?} contains invalid UTF-8 characters", name))
})
}
impl Serialize for FlareTreeNode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("FlareTreeNode", 2)?;
let name = name_as_str::<S>(&self.name)?;
state.serialize_field("name", &name)?;
if !self.data.is_empty() {
state.serialize_field("data", &self.data)?
}
if !self.is_file {
state.serialize_field("children", &self.children)?;
}
state.end()
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::path::Path;
use test_shared::*;
#[test]
fn can_build_tree() {
let mut root = FlareTreeNode::dir("root");
root.append_child(FlareTreeNode::file("child"));
assert_eq!(
root,
FlareTreeNode {
name: OsString::from("root"),
is_file: false,
children: vec![FlareTreeNode {
name: OsString::from("child"),
is_file: true,
data: HashMap::new(),
children: Vec::new()
}],
data: HashMap::new()
}
)
}
fn build_test_tree() -> FlareTreeNode {
let mut root = FlareTreeNode::dir("root");
root.append_child(FlareTreeNode::file("root_file_1.txt"));
root.append_child(FlareTreeNode::file("root_file_2.txt"));
let mut child1 = FlareTreeNode::dir("child1");
child1.append_child(FlareTreeNode::file("child1_file_1.txt"));
let mut grand_child = FlareTreeNode::dir("grandchild");
grand_child.append_child(FlareTreeNode::file("grandchild_file.txt"));
child1.append_child(grand_child);
child1.append_child(FlareTreeNode::file("child1_file_2.txt"));
let mut child2 = FlareTreeNode::dir("child2");
child2.add_data("meta", json!("wibble"));
let mut child2_file = FlareTreeNode::file("child2_file.txt");
let widget_data = json!({
"sprockets": 7,
"flanges": ["Nigel, Sarah"]
});
child2_file.add_data("widgets", widget_data);
child2.append_child(child2_file);
root.append_child(child1);
root.append_child(child2);
root
}
#[test]
fn can_get_elements_from_tree() {
let tree = build_test_tree();
let mut path = std::path::Path::new("child1/grandchild/grandchild_file.txt").components();
let grandchild = tree.get_in(&mut path);
assert_eq!(
grandchild.expect("Grandchild not found!").name(),
"grandchild_file.txt"
);
}
#[test]
fn can_get_top_level_element_from_tree() {
let tree = build_test_tree();
let mut path = std::path::Path::new("child1").components();
let child1 = tree.get_in(&mut path);
assert_eq!(child1.expect("child1 not found!").name(), "child1");
let mut path2 = std::path::Path::new("root_file_1.txt").components();
let child2 = tree.get_in(&mut path2);
assert_eq!(
child2.expect("root_file_1 not found!").name(),
"root_file_1.txt"
);
}
#[test]
fn getting_missing_elements_returns_none() {
let tree = build_test_tree();
let mut path = std::path::Path::new("child1/grandchild/nonesuch").components();
let missing = tree.get_in(&mut path);
assert_eq!(missing.is_none(), true);
let mut path2 =
Path::new("child1/grandchild/grandchild_file.txt/files_have_no_kids").components();
let missing2 = tree.get_in(&mut path2);
assert_eq!(missing2.is_none(), true);
let mut path3 = Path::new("no_file_at_root").components();
let missing3 = tree.get_in(&mut path3);
assert_eq!(missing3.is_none(), true);
}
#[test]
fn can_get_mut_elements_from_tree() {
let mut tree = build_test_tree();
let grandchild = tree
.get_in_mut(&mut Path::new("child1/grandchild/grandchild_file.txt").components())
.expect("Grandchild not found!");
assert_eq!(grandchild.name(), "grandchild_file.txt");
grandchild.name = OsString::from("fish");
let grandchild2 = tree.get_in_mut(&mut Path::new("child1/grandchild/fish").components());
assert_eq!(grandchild2.expect("fish not found!").name(), "fish");
let grandchild_dir = tree
.get_in_mut(&mut Path::new("child1/grandchild").components())
.expect("Grandchild dir not found!");
assert_eq!(grandchild_dir.name(), "grandchild");
grandchild_dir.append_child(FlareTreeNode::file("new_kid_on_the_block.txt"));
let new_kid = tree
.get_in_mut(&mut Path::new("child1/grandchild/new_kid_on_the_block.txt").components())
.expect("New kid not found!");
assert_eq!(new_kid.name(), "new_kid_on_the_block.txt");
}
#[test]
fn can_get_json_payloads_from_tree() {
let tree = build_test_tree();
let file = tree
.get_in(&mut Path::new("child2/child2_file.txt").components())
.unwrap();
assert_eq!(file.name(), "child2_file.txt");
let expected = json!({
"sprockets": 7,
"flanges": ["Nigel, Sarah"]
});
assert_eq!(&file.data["widgets"], &expected);
}
#[test]
fn can_serialize_directory_to_json() {
let root = FlareTreeNode::dir("root");
assert_eq_json_str(
&root,
r#"{
"name":"root",
"children": []
}"#,
)
}
#[test]
fn can_serialize_dir_with_data_to_json() {
let mut dir = FlareTreeNode::dir("foo");
dir.add_data("wibble", json!("fnord"));
assert_eq_json_str(
&dir,
r#"{
"name":"foo",
"data": {"wibble":"fnord"},
"children": []
}"#,
)
}
#[test]
fn can_serialize_file_to_json() {
let file = FlareTreeNode::file("foo.txt");
assert_eq_json_str(
&file,
r#"{
"name":"foo.txt"
}"#,
)
}
#[test]
fn can_serialize_file_with_data_to_json() {
let mut file = FlareTreeNode::file("foo.txt");
file.add_data("wibble", json!("fnord"));
assert_eq_json_str(
&file,
r#"{
"name":"foo.txt",
"data": {"wibble":"fnord"}
}"#,
)
}
#[test]
fn can_serialize_file_with_data_value_to_json() {
let mut file = FlareTreeNode::file("foo.txt");
let value = json!({"foo": ["bar", "baz", 123]});
file.add_data("bat", value);
assert_eq_json_str(
&file,
r#"{
"name":"foo.txt",
"data": {"bat": {"foo": ["bar", "baz", 123]}}
}"#,
)
}
#[test]
fn can_serialize_simple_tree_to_json() {
let mut root = FlareTreeNode::dir("root");
root.append_child(FlareTreeNode::file("child.txt"));
root.append_child(FlareTreeNode::dir("child2"));
assert_eq_json_value(
&root,
&json!({
"name":"root",
"children":[
{
"name": "child.txt"
},
{
"name":"child2",
"children":[]
}
]
}),
)
}
}