From 87ad809e078c7cda2942e99f2263bfa301def211 Mon Sep 17 00:00:00 2001 From: James Pace Date: Sun, 28 Jun 2026 09:12:55 -0400 Subject: [PATCH] Add logic to reconcile the levels in a graph. --- src/diagnostic_node.rs | 39 +++++++++ src/diagnostic_status.rs | 46 ++++------ src/lib.rs | 183 ++++++++++++++++++++++++++++++--------- src/name_parsing.rs | 82 ++++++++++++++++++ 4 files changed, 281 insertions(+), 69 deletions(-) create mode 100644 src/diagnostic_node.rs create mode 100644 src/name_parsing.rs diff --git a/src/diagnostic_node.rs b/src/diagnostic_node.rs new file mode 100644 index 0000000..abf398d --- /dev/null +++ b/src/diagnostic_node.rs @@ -0,0 +1,39 @@ +// +// 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::DiagnosticStatus; +use anyhow::anyhow; + +#[derive(Clone)] +pub enum DiagnosticNode { + Root, + DiagnosticStatus(DiagnosticStatus), +} +impl limbo_graph::NodeValue for DiagnosticNode {} + +impl DiagnosticNode { + pub fn from_status(status: DiagnosticStatus) -> Self { + Self::DiagnosticStatus(status) + } + + pub fn is_root(&self) -> bool { + match self { + Self::Root => true, + _ => false, + } + } + + pub fn value(&self) -> anyhow::Result<&DiagnosticStatus> { + match self { + Self::DiagnosticStatus(value) => Ok(value), + _ => Err(anyhow!("Can't get value of root key.")), + } + } +} diff --git a/src/diagnostic_status.rs b/src/diagnostic_status.rs index 63e5112..66ee763 100644 --- a/src/diagnostic_status.rs +++ b/src/diagnostic_status.rs @@ -12,9 +12,14 @@ use alloc::borrow::ToOwned; use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; +use core::default::Default; -#[derive(Clone, PartialEq)] +use crate::name_parsing::{get_child_name, get_parent_names, name_is_basic}; + +#[derive(Clone, PartialOrd, PartialEq, Default)] pub enum DiagnosticLevel { + #[default] + UNSET, OK, WARN, ERROR, @@ -50,7 +55,7 @@ impl DiagnosticStatus { } pub fn from_name(name: String) -> Self { - let level = DiagnosticLevel::STALE; + let level = DiagnosticLevel::UNSET; let message = ""; let hardware_id = ""; let values = BTreeMap::::new(); @@ -93,40 +98,15 @@ impl DiagnosticStatus { } pub fn name_is_basic(&self) -> bool { - // A basic name is one that doesn't have slashes in it other than the - // first character. - let is_not_basic = self - .name - .strip_prefix("/") - .unwrap_or(&self.name) - .contains("/"); - return !is_not_basic; + name_is_basic(&self.name) } pub fn get_parent_names(&self) -> Vec { - if self.name_is_basic() { - return Vec::::new(); - } - - let split: Vec = self - .name() - .split("/") - .map(|x| x.to_owned()) - .filter(|x| !x.is_empty()) - .collect(); - return split - .iter() - .take(split.len() - 1) - .map(|x| x.to_owned()) - .collect(); + get_parent_names(&self.name) } pub fn get_child_name(&self) -> String { - if self.name_is_basic() { - return self.name(); - } - let split: Vec = self.name().split("/").map(|x| x.to_owned()).collect(); - return split.last().unwrap().clone(); + get_child_name(&self.name) } pub fn copy_with_child_name(&self) -> Self { @@ -135,6 +115,12 @@ impl DiagnosticStatus { return to_return; } + pub fn copy_with_new_level(&self, level: DiagnosticLevel) -> Self { + let mut to_return: Self = self.clone(); + to_return.level = level; + return to_return; + } + fn clean_name(name: &str) -> String { // Remove prefix "/" let without_prefix = name.strip_prefix("/").unwrap_or(name); diff --git a/src/lib.rs b/src/lib.rs index 898ab94..5fb6cd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,12 +9,6 @@ // defined by the Mozilla Public License, v. 2.0. // -// Take in list of diagnostic statuses -// For each status: -// 1. Parse name into level, splitting on / -// 2. Cycle through current tree for each level from above. -// 1. If level from above is missing add it. -// 2. If present, keep looking down. // Once all elements added, start at the edge nodes and travel up // to mark levels of parent nodes. // Depth first search from top to build new vector to return. @@ -24,7 +18,9 @@ #![allow(unused_imports)] extern crate alloc; +mod diagnostic_node; mod diagnostic_status; +mod name_parsing; use anyhow::anyhow; @@ -36,29 +32,8 @@ use alloc::vec::Vec; pub use crate::diagnostic_status::*; -// TODO: Make this not public. -#[derive(Clone)] -pub enum DiagnosticNode { - Root, - DiagnosticStatus(DiagnosticStatus), -} -impl limbo_graph::NodeValue for DiagnosticNode {} - -impl DiagnosticNode { - pub fn is_root(&self) -> bool { - match self { - Self::Root => true, - _ => false, - } - } - - pub fn value(&self) -> Option<&DiagnosticStatus> { - match self { - Self::DiagnosticStatus(value) => Some(value), - _ => None, - } - } -} +use crate::diagnostic_node::*; +use crate::name_parsing::*; pub struct DiagnosticGraph { graph: limbo_graph::Graph, @@ -77,11 +52,8 @@ impl DiagnosticGraph { pub fn value_of(&self, key: &limbo_graph::Key) -> anyhow::Result { let graph_value = self.graph.value_of(key)?; - let result = graph_value.value(); - if result.is_none() { - return Err(anyhow!("Can't get value of root key.")); - } - Ok(result.unwrap().clone()) + let result = graph_value.value()?; + Ok(result.clone()) } pub fn children_of(&self, key: &limbo_graph::Key) -> anyhow::Result> { @@ -89,11 +61,14 @@ impl DiagnosticGraph { Ok(result) } - pub fn child_of_with_name( + pub fn child_of_which_has_name( &self, parent: &limbo_graph::Key, desired_child_name: &str, ) -> anyhow::Result> { + if !name_is_basic(&desired_child_name.to_owned()) { + return Err(anyhow!("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(); @@ -101,10 +76,41 @@ impl DiagnosticGraph { return Ok(Some(child.clone())); } } - return Ok(None); } + pub fn key_from_full_name( + &self, + full_name: &String, + ) -> anyhow::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)); + } + + pub fn value_from_name( + &self, + full_name: &String, + ) -> anyhow::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))); + } + pub fn add_status(&mut self, status: DiagnosticStatus) -> anyhow::Result<()> { // If I'm basic I just need to be added as a child of the root. if status.name_is_basic() { @@ -138,7 +144,7 @@ impl DiagnosticGraph { // A child node can't be root. return Err(anyhow!("A child node can't be root!")); } - if child_node.value().unwrap().name() == *parent_name { + 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. @@ -183,6 +189,42 @@ impl DiagnosticGraph { } Ok(()) } + + pub fn reconcile_levels(&mut self) -> anyhow::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(()) + } } #[cfg(test)] @@ -205,6 +247,21 @@ mod tests { ) } + 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() -> anyhow::Result<()> { let mut graph = DiagnosticGraph::new(); @@ -258,16 +315,64 @@ mod tests { assert!(children_of_a_names.any(|x| x == "b")); assert!(children_of_a_names.any(|x| x == "d")); - let b_key = graph.child_of_with_name(&root_children[0], "b")?; + 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_with_name(&root_children[0], "d")?; + 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_from_name() -> anyhow::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); + + Ok(()) + } + + #[test] + fn reconcile_graph() -> anyhow::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(()) + } } diff --git a/src/name_parsing.rs b/src/name_parsing.rs new file mode 100644 index 0000000..ba7762f --- /dev/null +++ b/src/name_parsing.rs @@ -0,0 +1,82 @@ +// +// 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::string::String; +use alloc::vec::Vec; + +pub fn name_is_basic(name: &String) -> bool { + // A basic name is one that doesn't have slashes in it other than the + // first character. + let is_not_basic = name.strip_prefix("/").unwrap_or(name).contains("/"); + return !is_not_basic; +} + +pub fn get_parent_names(name: &String) -> Vec { + if name_is_basic(&name) { + return Vec::::new(); + } + + let split: Vec = name + .split("/") + .map(|x| x.to_owned()) + .filter(|x| !x.is_empty()) + .collect(); + + return split + .iter() + .take(split.len() - 1) + .map(|x| x.to_owned()) + .collect(); +} + +pub fn get_child_name(name: &String) -> String { + if name_is_basic(&name) { + return name.clone(); + } + let split: Vec = name.split("/").map(|x| x.to_owned()).collect(); + return split.last().unwrap().clone(); +} + +pub fn split_name(name: &String) -> Vec { + name.split("/") + .map(|x| x.to_owned()) + .filter(|x| !x.is_empty()) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_basic() { + assert!(name_is_basic(&"a".to_owned())); + assert!(name_is_basic(&"/a".to_owned())); + assert!(!name_is_basic(&"/a/b".to_owned())); + } + + #[test] + fn test_get_names() { + let name = "/a/b/c"; + + let parent_names = get_parent_names(&name.to_owned()); + assert!(parent_names[0] == "a"); + assert!(parent_names[1] == "b"); + + let child_name = get_child_name(&name.to_owned()); + assert!(child_name == "c"); + + let split = split_name(&name.to_owned()); + assert!(split[0] == "a"); + assert!(split[1] == "b"); + assert!(split[2] == "c"); + } +}