diff --git a/Cargo.toml b/Cargo.toml index 7a389d4..bf8cf7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,4 +5,9 @@ edition = "2024" [dependencies] limbo_graph = { branch = "main", git="https://git.jpace121.net/public/limbo_graph.git" } -thiserror = { version = "2.0.18", default-features = false } \ No newline at end of file +thiserror = { version = "2.0.18", default-features = false} +serde = { version = "1.0", default-features = false, features=["derive", "alloc"]} +serde_json = { version = "1.0" , default-features = false, features=["alloc"]} + +[features] +std = ["serde_json/std"] \ No newline at end of file diff --git a/src/diagnostic_graph.rs b/src/diagnostic_graph.rs new file mode 100644 index 0000000..818d0de --- /dev/null +++ b/src/diagnostic_graph.rs @@ -0,0 +1,450 @@ +// +// Copyright 2026 James Pace +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +use alloc::borrow::ToOwned; +use alloc::collections::BTreeMap; +use alloc::collections::VecDeque; +use alloc::string::String; +use alloc::string::ToString; +use alloc::vec::Vec; + +use crate::diagnostic_node::*; +use crate::diagnostic_status::*; +use crate::error::*; +use crate::name_parsing::*; + +/// A graph of diagnostic statuses, following the general idea of +/// ros aggregated diagnostics. +/// +/// Diagnostics are stored in a tree by their namme. +/// Their levels can be reconciled, and the individual +/// statuses queried. +#[derive(Clone)] +pub struct DiagnosticGraph { + graph: limbo_graph::Graph, +} + +impl DiagnosticGraph { + /// Generate a empty graph. + pub fn new() -> Self { + Self { + graph: limbo_graph::Graph::::new(DiagnosticNode::Root), + } + } + + /// Get the root key. + /// Needed to do searches in the graph outside of this crate. + pub fn root(&self) -> limbo_graph::Key { + self.graph.root_key() + } + + /// Get the value of a specific element of the graph. + /// Will error if the key is not in the graph, or if the key + /// is the root, which doesn't have a value. + pub fn value_of(&self, key: &limbo_graph::Key) -> Result { + let graph_value = self.graph.value_of(key)?; + let result = graph_value.value()?; + Ok(result.clone()) + } + + /// Find all the children of key as keys. + /// Will error if key is not in the graph. + pub fn children_of(&self, key: &limbo_graph::Key) -> Result> { + let result = self.graph.children_of(key)?; + Ok(result) + } + + /// Given a key, find the child of that key that has the provided name. + /// The provided name must be basic (have no slashes). + /// Will return None if there are no children with that name. + pub fn child_of_which_has_name( + &self, + parent: &limbo_graph::Key, + desired_child_name: &str, + ) -> Result> { + if !name_is_basic(&desired_child_name.to_owned()) { + return Err(Error::from_msg("desired child name must be basic.")); + } + let all_children = self.children_of(parent)?; + for child in all_children.iter() { + let child_name = self.value_of(&child)?.name(); + if child_name == desired_child_name { + return Ok(Some(child.clone())); + } + } + return Ok(None); + } + + /// Find the key of the item in the graph with the provided full name + /// (i.e. the name has slashes.) + pub fn key_from_full_name(&self, full_name: &String) -> Result> { + let name_as_vec = split_name(&full_name); + + let mut cur_base_key = self.graph.root_key(); + + for cur_name in name_as_vec.iter() { + let key_of_cur_name = self.child_of_which_has_name(&cur_base_key, &cur_name)?; + if key_of_cur_name.is_none() { + return Ok(None); + } + cur_base_key = key_of_cur_name.unwrap(); + } + return Ok(Some(cur_base_key)); + } + + /// Provide the full name for a given key. (i.e. the name will have slashes). + pub fn full_name_from_key(&self, key: &limbo_graph::Key) -> Result { + let path_to_key = self.graph.backtrack_from_key(key)?; + let mut full_name = String::new(); + + for curr_key in path_to_key.iter() { + if *curr_key == self.graph.root_key() { + continue; + } + let status = self.value_of(&curr_key)?; + full_name = full_name + "/" + &status.name(); + } + + Ok(full_name) + } + + /// Provide the value of a status from the full name. + pub fn value_from_name( + &self, + full_name: &String, + ) -> Result> { + let key = self.key_from_full_name(full_name)?; + if key.is_none() { + return Ok(None); + } + let key = key.unwrap(); + let value = self.value_of(&key)?; + + return Ok(Some((value, key))); + } + + /// Add a status to the graph. + /// The name of the status will be used to determine where in the + /// graph the status belongs. + pub fn add_status(&mut self, status: DiagnosticStatus) -> Result<()> { + // If I'm basic I just need to be added as a child of the root. + if status.name_is_basic() { + self.graph.add( + DiagnosticNode::DiagnosticStatus(status), + self.graph.root_key(), + )?; + return Ok(()); + } + // Find this status' child name and parent's names + let parent_names = status.get_parent_names(); + + let mut cur_key = self.graph.root_key(); + + 'parent_loop: for parent_name in parent_names.iter() { + let children = self.graph.children_of(&cur_key)?; + // There are no children of this parent, so we can add + // the parent and go to the next parent. + if children.len() == 0 { + let holder_for_parent = DiagnosticStatus::from_name(parent_name.clone()); + cur_key = self + .graph + .add(DiagnosticNode::DiagnosticStatus(holder_for_parent), cur_key)?; + continue 'parent_loop; + } + // This parent does have children, look at them and + // update loop if appropriate. + for child in children { + let child_node = self.graph.value_of(&child)?; + if child_node.is_root() { + // A child node can't be root. + return Err(Error::from_msg("A child node can't be root!")); + } + if child_node.value()?.name() == *parent_name { + // The child node matched the parent we were looking for. + // Continue to find the next parent, with this key as the + // current key. + cur_key = child; + continue 'parent_loop; + } + } + // Parent wasn't any of the children. Add it and look for next parent. + let holder_for_parent = DiagnosticStatus::from_name(parent_name.clone()); + cur_key = self + .graph + .add(DiagnosticNode::DiagnosticStatus(holder_for_parent), cur_key)?; + } + // We've updated all the parents, so we can add ourselves now. + + // Do we already exist as a child of parent? + let children_of_this_parent = self.graph.children_of(&cur_key)?; + let mut names_of_children_of_this_parent = children_of_this_parent.iter().map(|x| { + self.graph + .value_of(&x) + .expect("Given child not in graph.") + .value() + .expect("Given root node as child?") + .name() + }); + let child_in_children_of_this_parent = + names_of_children_of_this_parent.any(|x| x == status.get_child_name()); + if !child_in_children_of_this_parent { + // If we don't we can be added. + self.graph.add( + DiagnosticNode::DiagnosticStatus(status.copy_with_child_name()), + cur_key, + )?; + } + + Ok(()) + } + + /// Add all the statuses in the provided vector. + /// The name of the statuses will be used to determine where in the + /// graph the statuses belong. + pub fn add_status_vec(&mut self, statuses: &Vec) -> Result<()> { + for status in statuses { + self.add_status(status.clone())?; + } + Ok(()) + } + + /// Update the levels for parent nodes based on the level of their children. + pub fn reconcile_levels(&mut self) -> Result<()> { + let leaf_keys = self.graph.find_leaf_keys()?; + + 'leaf_key_loop: for leaf_key in leaf_keys.iter() { + let mut child_key = leaf_key.clone(); + + while child_key != self.graph.root_key() { + let parent_key_opt = self.graph.parent_of(&child_key)?; + if parent_key_opt == None || parent_key_opt == Some(self.graph.root_key()) { + continue 'leaf_key_loop; + } + let parent_key = parent_key_opt.unwrap(); + // Reconcile the parent values. + let parent_node = self.graph.value_of(&parent_key)?; + let parent_value = parent_node.value()?; + let child_node = self.graph.value_of(&child_key)?; + let child_value = child_node.value()?; + + // if child level is worse than parent key. + // reset parent level. + if child_value.level() > parent_value.level() { + let replacement_parent = DiagnosticNode::from_status( + parent_value.copy_with_new_level(child_value.level()), + ); + self.graph + .replace_value_of(&parent_key, replacement_parent)?; + } + + // Push up the graph. + child_key = parent_key; + } + } + + Ok(()) + } + + pub fn export_graph(&self) -> Result> { + let keys_in_order = self.graph.get_keys_by_depth()?; + + let mut statuses = Vec::::new(); + + for key in keys_in_order.iter() { + if *key == self.root() { + // The root key will always be the first one through this + // loop. + continue; + } + let status_at_key = self.value_of(&key)?; + statuses.push(status_at_key.copy_with_new_name(self.full_name_from_key(&key)?)); + } + + Ok(statuses) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + fn make_a_status_with_name(name: &str) -> DiagnosticStatus { + let level = DiagnosticLevel::OK; + let message = "I'm ok"; + let hardware_id = ""; + let values = BTreeMap::::new(); + + DiagnosticStatus::new( + level, + name.to_owned(), + message.to_owned(), + hardware_id.to_owned(), + values, + ) + } + + fn make_a_status_with_name_and_level(name: &str, level: DiagnosticLevel) -> DiagnosticStatus { + let level = level; + let message = ""; + let hardware_id = ""; + let values = BTreeMap::::new(); + + DiagnosticStatus::new( + level, + name.to_owned(), + message.to_owned(), + hardware_id.to_owned(), + values, + ) + } + + #[test] + fn add_one_to_graph() -> Result<()> { + let mut graph = DiagnosticGraph::new(); + graph.add_status(make_a_status_with_name("/a/b/c"))?; + + let first_child_keys = graph.children_of(&graph.root())?; + assert!(first_child_keys.len() == 1); + let first_child_key = first_child_keys[0]; + let first_child_value = graph.value_of(&first_child_key)?; + assert!(first_child_value.name() == "a"); + + let second_child_keys = graph.children_of(&first_child_key)?; + assert!(second_child_keys.len() == 1); + let second_child_key = second_child_keys[0]; + let second_child_value = graph.value_of(&second_child_key)?; + assert!(second_child_value.name() == "b"); + + let third_child_keys = graph.children_of(&second_child_key)?; + assert!(third_child_keys.len() == 1); + let third_child_key = third_child_keys[0]; + let third_child_value = graph.value_of(&third_child_key)?; + assert!(third_child_value.name() == "c"); + assert!(third_child_value.level() == DiagnosticLevel::OK); + + Ok(()) + } + + #[test] + fn add_multiple_to_graph() -> Result<()> { + let statuses = vec![ + make_a_status_with_name("/a"), + make_a_status_with_name("/a/b"), + make_a_status_with_name("/a/b/c"), + make_a_status_with_name("/a/d/e"), + make_a_status_with_name("/a/d/f"), + make_a_status_with_name("/a/d"), + ]; + + let mut graph = DiagnosticGraph::new(); + graph.add_status_vec(&statuses)?; + + let root_children = graph.children_of(&graph.root())?; + assert!(root_children.len() == 1); + assert!(graph.value_of(&root_children[0])?.name() == "a"); + + let children_of_a = graph.children_of(&root_children[0])?; + let mut children_of_a_names = children_of_a + .iter() + .map(|x| graph.value_of(&x).expect("Value of failed.").name()); + assert!(children_of_a.len() == 2); + assert!(children_of_a_names.any(|x| x == "b")); + assert!(children_of_a_names.any(|x| x == "d")); + + let b_key = graph.child_of_which_has_name(&root_children[0], "b")?; + assert!(b_key.is_some()); + let b_children = graph.children_of(&b_key.unwrap())?; + assert!(b_children.len() == 1); + + let d_key = graph.child_of_which_has_name(&root_children[0], "d")?; + assert!(d_key.is_some()); + let d_children = graph.children_of(&d_key.unwrap())?; + assert!(d_children.len() == 2); + + Ok(()) + } + + #[test] + fn key_name_connections() -> Result<()> { + let statuses = vec![ + make_a_status_with_name("/a"), + make_a_status_with_name("/a/b"), + make_a_status_with_name("/a/d/e"), + make_a_status_with_name("/a/d"), + ]; + + let mut graph = DiagnosticGraph::new(); + graph.add_status_vec(&statuses)?; + + let key_for_a = graph.key_from_full_name(&"/a".to_owned())?; + assert!(key_for_a == Some(1)); + + let key_for_e = graph.key_from_full_name(&"/a/d/e".to_owned())?; + assert!(key_for_e == Some(4)); + + let not_in_graph = graph.key_from_full_name(&"/a/b/c".to_owned())?; + assert!(not_in_graph == None); + + let name_for_e = graph.full_name_from_key(&4)?; + assert!(name_for_e == "/a/d/e".to_owned()); + + Ok(()) + } + + #[test] + fn reconcile_graph() -> Result<()> { + let statuses = vec![ + make_a_status_with_name_and_level("/a", DiagnosticLevel::UNSET), + make_a_status_with_name_and_level("/a/b", DiagnosticLevel::UNSET), + make_a_status_with_name_and_level("/a/b/c", DiagnosticLevel::OK), + make_a_status_with_name_and_level("/a/d/e", DiagnosticLevel::WARN), + make_a_status_with_name_and_level("/a/d", DiagnosticLevel::WARN), + make_a_status_with_name_and_level("/a/d/f", DiagnosticLevel::OK), + ]; + let mut graph = DiagnosticGraph::new(); + graph.add_status_vec(&statuses)?; + + graph.reconcile_levels()?; + + let (a_status, _) = graph.value_from_name(&"/a".to_owned())?.unwrap(); + assert!(a_status.level() == DiagnosticLevel::WARN); + + let (d_status, _) = graph.value_from_name(&"/a/d".to_owned())?.unwrap(); + assert!(d_status.level() == DiagnosticLevel::WARN); + + Ok(()) + } + + #[test] + fn export_graph() -> Result<()> { + let statuses = vec![ + make_a_status_with_name_and_level("/a", DiagnosticLevel::UNSET), + make_a_status_with_name_and_level("/a/b/c", DiagnosticLevel::OK), + make_a_status_with_name_and_level("/a/b", DiagnosticLevel::UNSET), + make_a_status_with_name_and_level("/a/d", DiagnosticLevel::UNSET), + ]; + let mut graph = DiagnosticGraph::new(); + graph.add_status_vec(&statuses)?; + + let exported = graph.export_graph()?; + + assert!(exported.len() == 4); + // Searching doen the b tree first would also be + // technically correct, but this is the order the implementation + // is currently going with. + assert!(exported[0].name() == "/a"); + assert!(exported[1].name() == "/a/d"); + assert!(exported[2].name() == "/a/b"); + assert!(exported[3].name() == "/a/b/c"); + + Ok(()) + } +} diff --git a/src/diagnostic_status.rs b/src/diagnostic_status.rs index 65ff614..6e5c2e4 100644 --- a/src/diagnostic_status.rs +++ b/src/diagnostic_status.rs @@ -14,9 +14,11 @@ use alloc::string::String; use alloc::vec::Vec; use core::default::Default; +use serde::{Deserialize, Serialize}; + use crate::name_parsing::{get_child_name, get_parent_names, name_is_basic}; -#[derive(Clone, PartialOrd, PartialEq, Default)] +#[derive(Clone, PartialOrd, PartialEq, Default, Serialize, Deserialize)] pub enum DiagnosticLevel { #[default] UNSET, @@ -26,7 +28,7 @@ pub enum DiagnosticLevel { STALE, } -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct DiagnosticStatus { level: DiagnosticLevel, name: String, diff --git a/src/error.rs b/src/error.rs index c929b10..05089bf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,8 @@ use alloc::string::{String, ToString}; pub enum Error { #[error("GraphError: {0}")] GraphError(#[from] limbo_graph::Error), + #[error("serde_json Error: {0}")] + JsonError(#[from] serde_json::Error), #[error("Error from j7s_diagnostics: {0}")] Msg(String), } diff --git a/src/lib.rs b/src/lib.rs index ef06fab..acb9f76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,453 +9,22 @@ // defined by the Mozilla Public License, v. 2.0. // -#![no_std] #![allow(unused_imports)] +#![cfg_attr(not(feature = "std"), no_std)] + extern crate alloc; +mod diagnostic_graph; mod diagnostic_node; mod diagnostic_status; mod error; mod name_parsing; -use alloc::borrow::ToOwned; -use alloc::collections::BTreeMap; -use alloc::collections::VecDeque; -use alloc::string::String; -use alloc::string::ToString; -use alloc::vec::Vec; - +pub use crate::diagnostic_graph::*; pub use crate::diagnostic_status::*; pub use crate::error::*; -use crate::diagnostic_node::*; -use crate::name_parsing::*; - -/// A graph of diagnostic statuses, following the general idea of -/// ros aggregated diagnostics. -/// -/// Diagnostics are stored in a tree by their namme. -/// Their levels can be reconciled, and the individual -/// statuses queried. -#[derive(Clone)] -pub struct DiagnosticGraph { - graph: limbo_graph::Graph, -} - -impl DiagnosticGraph { - /// Generate a empty graph. - pub fn new() -> Self { - Self { - graph: limbo_graph::Graph::::new(DiagnosticNode::Root), - } - } - - /// Get the root key. - /// Needed to do searches in the graph outside of this crate. - pub fn root(&self) -> limbo_graph::Key { - self.graph.root_key() - } - - /// Get the value of a specific element of the graph. - /// Will error if the key is not in the graph, or if the key - /// is the root, which doesn't have a value. - pub fn value_of(&self, key: &limbo_graph::Key) -> Result { - let graph_value = self.graph.value_of(key)?; - let result = graph_value.value()?; - Ok(result.clone()) - } - - /// Find all the children of key as keys. - /// Will error if key is not in the graph. - pub fn children_of(&self, key: &limbo_graph::Key) -> Result> { - let result = self.graph.children_of(key)?; - Ok(result) - } - - /// Given a key, find the child of that key that has the provided name. - /// The provided name must be basic (have no slashes). - /// Will return None if there are no children with that name. - pub fn child_of_which_has_name( - &self, - parent: &limbo_graph::Key, - desired_child_name: &str, - ) -> Result> { - if !name_is_basic(&desired_child_name.to_owned()) { - return Err(Error::from_msg("desired child name must be basic.")); - } - let all_children = self.children_of(parent)?; - for child in all_children.iter() { - let child_name = self.value_of(&child)?.name(); - if child_name == desired_child_name { - return Ok(Some(child.clone())); - } - } - return Ok(None); - } - - /// Find the key of the item in the graph with the provided full name - /// (i.e. the name has slashes.) - pub fn key_from_full_name(&self, full_name: &String) -> Result> { - let name_as_vec = split_name(&full_name); - - let mut cur_base_key = self.graph.root_key(); - - for cur_name in name_as_vec.iter() { - let key_of_cur_name = self.child_of_which_has_name(&cur_base_key, &cur_name)?; - if key_of_cur_name.is_none() { - return Ok(None); - } - cur_base_key = key_of_cur_name.unwrap(); - } - return Ok(Some(cur_base_key)); - } - - /// Provide the full name for a given key. (i.e. the name will have slashes). - pub fn full_name_from_key(&self, key: &limbo_graph::Key) -> Result { - let path_to_key = self.graph.backtrack_from_key(key)?; - let mut full_name = String::new(); - - for curr_key in path_to_key.iter() { - if *curr_key == self.graph.root_key() { - continue; - } - let status = self.value_of(&curr_key)?; - full_name = full_name + "/" + &status.name(); - } - - Ok(full_name) - } - - /// Provide the value of a status from the full name. - pub fn value_from_name( - &self, - full_name: &String, - ) -> Result> { - let key = self.key_from_full_name(full_name)?; - if key.is_none() { - return Ok(None); - } - let key = key.unwrap(); - let value = self.value_of(&key)?; - - return Ok(Some((value, key))); - } - - /// Add a status to the graph. - /// The name of the status will be used to determine where in the - /// graph the status belongs. - pub fn add_status(&mut self, status: DiagnosticStatus) -> Result<()> { - // If I'm basic I just need to be added as a child of the root. - if status.name_is_basic() { - self.graph.add( - DiagnosticNode::DiagnosticStatus(status), - self.graph.root_key(), - )?; - return Ok(()); - } - // Find this status' child name and parent's names - let parent_names = status.get_parent_names(); - - let mut cur_key = self.graph.root_key(); - - 'parent_loop: for parent_name in parent_names.iter() { - let children = self.graph.children_of(&cur_key)?; - // There are no children of this parent, so we can add - // the parent and go to the next parent. - if children.len() == 0 { - let holder_for_parent = DiagnosticStatus::from_name(parent_name.clone()); - cur_key = self - .graph - .add(DiagnosticNode::DiagnosticStatus(holder_for_parent), cur_key)?; - continue 'parent_loop; - } - // This parent does have children, look at them and - // update loop if appropriate. - for child in children { - let child_node = self.graph.value_of(&child)?; - if child_node.is_root() { - // A child node can't be root. - return Err(Error::from_msg("A child node can't be root!")); - } - if child_node.value()?.name() == *parent_name { - // The child node matched the parent we were looking for. - // Continue to find the next parent, with this key as the - // current key. - cur_key = child; - continue 'parent_loop; - } - } - // Parent wasn't any of the children. Add it and look for next parent. - let holder_for_parent = DiagnosticStatus::from_name(parent_name.clone()); - cur_key = self - .graph - .add(DiagnosticNode::DiagnosticStatus(holder_for_parent), cur_key)?; - } - // We've updated all the parents, so we can add ourselves now. - - // Do we already exist as a child of parent? - let children_of_this_parent = self.graph.children_of(&cur_key)?; - let mut names_of_children_of_this_parent = children_of_this_parent.iter().map(|x| { - self.graph - .value_of(&x) - .expect("Given child not in graph.") - .value() - .expect("Given root node as child?") - .name() - }); - let child_in_children_of_this_parent = - names_of_children_of_this_parent.any(|x| x == status.get_child_name()); - if !child_in_children_of_this_parent { - // If we don't we can be added. - self.graph.add( - DiagnosticNode::DiagnosticStatus(status.copy_with_child_name()), - cur_key, - )?; - } - - Ok(()) - } - - /// Add all the statuses in the provided vector. - /// The name of the statuses will be used to determine where in the - /// graph the statuses belong. - pub fn add_status_vec(&mut self, statuses: &Vec) -> Result<()> { - for status in statuses { - self.add_status(status.clone())?; - } - Ok(()) - } - - /// Update the levels for parent nodes based on the level of their children. - pub fn reconcile_levels(&mut self) -> Result<()> { - let leaf_keys = self.graph.find_leaf_keys()?; - - 'leaf_key_loop: for leaf_key in leaf_keys.iter() { - let mut child_key = leaf_key.clone(); - - while child_key != self.graph.root_key() { - let parent_key_opt = self.graph.parent_of(&child_key)?; - if parent_key_opt == None || parent_key_opt == Some(self.graph.root_key()) { - continue 'leaf_key_loop; - } - let parent_key = parent_key_opt.unwrap(); - // Reconcile the parent values. - let parent_node = self.graph.value_of(&parent_key)?; - let parent_value = parent_node.value()?; - let child_node = self.graph.value_of(&child_key)?; - let child_value = child_node.value()?; - - // if child level is worse than parent key. - // reset parent level. - if child_value.level() > parent_value.level() { - let replacement_parent = DiagnosticNode::from_status( - parent_value.copy_with_new_level(child_value.level()), - ); - self.graph - .replace_value_of(&parent_key, replacement_parent)?; - } - - // Push up the graph. - child_key = parent_key; - } - } - - Ok(()) - } - - pub fn export_graph(&self) -> Result> { - let keys_in_order = self.graph.get_keys_by_depth()?; - - let mut statuses = Vec::::new(); - - for key in keys_in_order.iter() { - if *key == self.root() { - // The root key will always be the first one through this - // loop. - continue; - } - let status_at_key = self.value_of(&key)?; - statuses.push(status_at_key.copy_with_new_name(self.full_name_from_key(&key)?)); - } - - Ok(statuses) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloc::vec; - - fn make_a_status_with_name(name: &str) -> DiagnosticStatus { - let level = DiagnosticLevel::OK; - let message = "I'm ok"; - let hardware_id = ""; - let values = BTreeMap::::new(); - - DiagnosticStatus::new( - level, - name.to_owned(), - message.to_owned(), - hardware_id.to_owned(), - values, - ) - } - - fn make_a_status_with_name_and_level(name: &str, level: DiagnosticLevel) -> DiagnosticStatus { - let level = level; - let message = ""; - let hardware_id = ""; - let values = BTreeMap::::new(); - - DiagnosticStatus::new( - level, - name.to_owned(), - message.to_owned(), - hardware_id.to_owned(), - values, - ) - } - - #[test] - fn add_one_to_graph() -> Result<()> { - let mut graph = DiagnosticGraph::new(); - graph.add_status(make_a_status_with_name("/a/b/c"))?; - - let first_child_keys = graph.children_of(&graph.root())?; - assert!(first_child_keys.len() == 1); - let first_child_key = first_child_keys[0]; - let first_child_value = graph.value_of(&first_child_key)?; - assert!(first_child_value.name() == "a"); - - let second_child_keys = graph.children_of(&first_child_key)?; - assert!(second_child_keys.len() == 1); - let second_child_key = second_child_keys[0]; - let second_child_value = graph.value_of(&second_child_key)?; - assert!(second_child_value.name() == "b"); - - let third_child_keys = graph.children_of(&second_child_key)?; - assert!(third_child_keys.len() == 1); - let third_child_key = third_child_keys[0]; - let third_child_value = graph.value_of(&third_child_key)?; - assert!(third_child_value.name() == "c"); - assert!(third_child_value.level() == DiagnosticLevel::OK); - - Ok(()) - } - - #[test] - fn add_multiple_to_graph() -> Result<()> { - let statuses = vec![ - make_a_status_with_name("/a"), - make_a_status_with_name("/a/b"), - make_a_status_with_name("/a/b/c"), - make_a_status_with_name("/a/d/e"), - make_a_status_with_name("/a/d/f"), - make_a_status_with_name("/a/d"), - ]; - - let mut graph = DiagnosticGraph::new(); - graph.add_status_vec(&statuses)?; - - let root_children = graph.children_of(&graph.root())?; - assert!(root_children.len() == 1); - assert!(graph.value_of(&root_children[0])?.name() == "a"); - - let children_of_a = graph.children_of(&root_children[0])?; - let mut children_of_a_names = children_of_a - .iter() - .map(|x| graph.value_of(&x).expect("Value of failed.").name()); - assert!(children_of_a.len() == 2); - assert!(children_of_a_names.any(|x| x == "b")); - assert!(children_of_a_names.any(|x| x == "d")); - - let b_key = graph.child_of_which_has_name(&root_children[0], "b")?; - assert!(b_key.is_some()); - let b_children = graph.children_of(&b_key.unwrap())?; - assert!(b_children.len() == 1); - - let d_key = graph.child_of_which_has_name(&root_children[0], "d")?; - assert!(d_key.is_some()); - let d_children = graph.children_of(&d_key.unwrap())?; - assert!(d_children.len() == 2); - - Ok(()) - } - - #[test] - fn key_name_connections() -> Result<()> { - let statuses = vec![ - make_a_status_with_name("/a"), - make_a_status_with_name("/a/b"), - make_a_status_with_name("/a/d/e"), - make_a_status_with_name("/a/d"), - ]; - - let mut graph = DiagnosticGraph::new(); - graph.add_status_vec(&statuses)?; - - let key_for_a = graph.key_from_full_name(&"/a".to_owned())?; - assert!(key_for_a == Some(1)); - - let key_for_e = graph.key_from_full_name(&"/a/d/e".to_owned())?; - assert!(key_for_e == Some(4)); - - let not_in_graph = graph.key_from_full_name(&"/a/b/c".to_owned())?; - assert!(not_in_graph == None); - - let name_for_e = graph.full_name_from_key(&4)?; - assert!(name_for_e == "/a/d/e".to_owned()); - - Ok(()) - } - - #[test] - fn reconcile_graph() -> Result<()> { - let statuses = vec![ - make_a_status_with_name_and_level("/a", DiagnosticLevel::UNSET), - make_a_status_with_name_and_level("/a/b", DiagnosticLevel::UNSET), - make_a_status_with_name_and_level("/a/b/c", DiagnosticLevel::OK), - make_a_status_with_name_and_level("/a/d/e", DiagnosticLevel::WARN), - make_a_status_with_name_and_level("/a/d", DiagnosticLevel::WARN), - make_a_status_with_name_and_level("/a/d/f", DiagnosticLevel::OK), - ]; - let mut graph = DiagnosticGraph::new(); - graph.add_status_vec(&statuses)?; - - graph.reconcile_levels()?; - - let (a_status, _) = graph.value_from_name(&"/a".to_owned())?.unwrap(); - assert!(a_status.level() == DiagnosticLevel::WARN); - - let (d_status, _) = graph.value_from_name(&"/a/d".to_owned())?.unwrap(); - assert!(d_status.level() == DiagnosticLevel::WARN); - - Ok(()) - } - - #[test] - fn export_graph() -> Result<()> { - let statuses = vec![ - make_a_status_with_name_and_level("/a", DiagnosticLevel::UNSET), - make_a_status_with_name_and_level("/a/b/c", DiagnosticLevel::OK), - make_a_status_with_name_and_level("/a/b", DiagnosticLevel::UNSET), - make_a_status_with_name_and_level("/a/d", DiagnosticLevel::UNSET), - ]; - let mut graph = DiagnosticGraph::new(); - graph.add_status_vec(&statuses)?; - - let exported = graph.export_graph()?; - - assert!(exported.len() == 4); - // Searching doen the b tree first would also be - // technically correct, but this is the order the implementation - // is currently going with. - assert!(exported[0].name() == "/a"); - assert!(exported[1].name() == "/a/d"); - assert!(exported[2].name() == "/a/b"); - assert!(exported[3].name() == "/a/b/c"); - - Ok(()) - } -} +#[cfg(feature = "std")] +mod tree_view; +#[cfg(feature = "std")] +pub use crate::tree_view::*; diff --git a/src/tree_view.rs b/src/tree_view.rs new file mode 100644 index 0000000..7f8fed9 --- /dev/null +++ b/src/tree_view.rs @@ -0,0 +1,114 @@ +// +// Copyright 2026 James Pace +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// This Source Code Form is "Incompatible With Secondary Licenses", as +// defined by the Mozilla Public License, v. 2.0. +// +use crate::diagnostic_graph::DiagnosticGraph; +use crate::diagnostic_status::DiagnosticStatus; +use crate::error::Result; +use alloc::string::String; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct TreeView { + children: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +struct TreeViewNode { + id: usize, + status: DiagnosticStatus, + children: Vec, +} + +impl TreeView { + fn get_nodes(key: &limbo_graph::Key, graph: &DiagnosticGraph) -> Result> { + let children = graph.children_of(&key)?; + if children.len() == 0 { + return Ok(Vec::::new()); + } + + let mut to_ret = Vec::::new(); + + for child in children { + let status = graph.value_of(&child)?; + let node = TreeViewNode { + id: child.clone(), + status: status, + children: Self::get_nodes(&child, &graph)?, + }; + + to_ret.push(node); + } + return Ok(to_ret); + } + + pub fn from_graph(graph: &DiagnosticGraph) -> Result { + let view = TreeView { + children: Self::get_nodes(&graph.root(), &graph)?, + }; + + Ok(view) + } + + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string(self)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::*; + use alloc::collections::BTreeMap; + + fn make_a_status_with_name(name: &str) -> DiagnosticStatus { + let level = DiagnosticLevel::OK; + let message = "I'm ok"; + let hardware_id = ""; + let values = BTreeMap::::new(); + + DiagnosticStatus::new( + level, + name.to_owned(), + message.to_owned(), + hardware_id.to_owned(), + values, + ) + } + + #[test] + fn make_view() -> Result<()> { + let statuses = vec![ + make_a_status_with_name("/a"), + make_a_status_with_name("/a/b"), + make_a_status_with_name("/a/b/c"), + make_a_status_with_name("/a/d/e"), + make_a_status_with_name("/a/d"), + ]; + + let mut graph = DiagnosticGraph::new(); + graph.add_status_vec(&statuses)?; + + let view = TreeView::from_graph(&graph)?; + + // First level is just a + assert!(view.children.len() == 1); + // a has children b and d. + assert!(view.children[0].children.len() == 2); + // regardless is this is b or d, it has one child. + // TODO: This isn't the best test. + assert!(view.children[0].children[0].children.len() == 1); + + // Just to confirm it doesn't panic. + let _js = view.to_json()?; + + Ok(()) + } +}