commit 2e66ec1faddfdb57e69889461b85801be5774944 Author: James Pace Date: Sat Jun 13 20:53:18 2026 -0400 Init commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4cc1bea --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,21 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "j7s_diagnostics" +version = "0.1.0" +dependencies = [ + "anyhow", + "limbo_graph", +] + +[[package]] +name = "limbo_graph" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d20e9c5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "j7s_diagnostics" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { version = "1.0", default-features = false } +limbo_graph = { path = "../limbo_graph" } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..96399a2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,337 @@ +// 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. + +// TODO: Remove anyhow. +#![no_std] +#![allow(unused_imports)] +extern crate alloc; + +use anyhow::anyhow; + +use alloc::borrow::ToOwned; +use alloc::collections::BTreeMap; +use alloc::collections::VecDeque; +use alloc::string::String; +use alloc::vec::Vec; + +#[derive(Clone, PartialEq)] +pub enum DiagnosticLevel { + OK, + WARN, + ERROR, + STALE, +} + +#[derive(Clone)] +pub struct DiagnosticStatus { + level: DiagnosticLevel, + name: String, + message: String, + hardware_id: String, + values: BTreeMap, +} + +impl DiagnosticStatus { + pub fn new( + level: DiagnosticLevel, + name: String, + message: String, + hardware_id: String, + values: BTreeMap, + ) -> Self { + Self { + level: level, + name: name, + message: message, + hardware_id: hardware_id, + values: values, + } + } + + pub fn from_name(name: String) -> Self { + let level = DiagnosticLevel::STALE; + let message = ""; + let hardware_id = ""; + let values = BTreeMap::::new(); + + Self::new( + level, + name, + message.to_owned(), + hardware_id.to_owned(), + values, + ) + } + + pub fn level(&self) -> DiagnosticLevel { + self.level.clone() + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn message(&self) -> String { + self.message.clone() + } + + pub fn hardware_id(&self) -> String { + self.hardware_id.clone() + } + + pub fn keys(&self) -> Vec { + self.values.keys().cloned().collect() + } + + pub fn value(&self, key: &str) -> Option { + let value = self.values.get(key); + if value.is_none() { + return None; + } + return Some(value.unwrap().clone()); + } + + 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; + } + + 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(); + } + + 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(); + } + + pub fn copy_with_child_name(&self) -> Self { + let mut to_return: Self = self.clone(); + to_return.name = self.get_child_name(); + return to_return; + } +} +// 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, + } + } +} + +pub struct DiagnosticGraph { + graph: limbo_graph::Graph, +} + +impl DiagnosticGraph { + pub fn new() -> Self { + Self { + graph: limbo_graph::Graph::::new(DiagnosticNode::Root), + } + } + + pub fn root(&self) -> limbo_graph::Key { + return self.graph.root_key(); + } + + 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()) + } + + pub fn children_of(&self, key: &limbo_graph::Key) -> anyhow::Result> { + let result = self.graph.children_of(key)?; + Ok(result) + } + + 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() { + 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)?; + 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; + } + 'child_loop: for child in children { + let child_node = self.graph.value_of(&child)?; + if child_node.is_root() { + // child node can't be root, but we'll catch here anyway. + continue 'child_loop; + } + if child_node.value().unwrap().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. + self.graph.add( + DiagnosticNode::DiagnosticStatus(status.copy_with_child_name()), + cur_key, + )?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn basic_diagnostic_creation() { + let level = DiagnosticLevel::OK; + let name = "/a/b/c"; + let message = "I'm ok"; + let hardware_id = ""; + let values = BTreeMap::::new(); + + let diag_status = DiagnosticStatus::new( + level, + name.to_owned(), + message.to_owned(), + hardware_id.to_owned(), + values, + ); + assert!(diag_status.level() == DiagnosticLevel::OK); + } + + #[test] + fn diagnostic_name_parsing() { + let level = DiagnosticLevel::OK; + let name = "/a/b/c"; + let message = "I'm ok"; + let hardware_id = ""; + let values = BTreeMap::::new(); + + let diag_status = DiagnosticStatus::new( + level, + name.to_owned(), + message.to_owned(), + hardware_id.to_owned(), + values, + ); + + assert!(!diag_status.name_is_basic()); + + let parent_names = diag_status.get_parent_names(); + let child_name = diag_status.get_child_name(); + assert!(parent_names[0] == "a"); + assert!(parent_names[1] == "b"); + assert!(child_name == "c"); + } + + #[test] + fn add_one_to_graph() -> anyhow::Result<()> { + let level = DiagnosticLevel::OK; + let name = "/a/b/c"; + let message = "I'm ok"; + let hardware_id = ""; + let values = BTreeMap::::new(); + + let diag_status = DiagnosticStatus::new( + level, + name.to_owned(), + message.to_owned(), + hardware_id.to_owned(), + values, + ); + + let mut graph = DiagnosticGraph::new(); + graph.add_status(diag_status)?; + + 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(()) + } +}