use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GoalStatus {
Pending,
Active,
Complete,
Failed,
}
impl Default for GoalStatus {
fn default() -> Self {
Self::Pending
}
}
impl std::fmt::Display for GoalStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GoalStatus::Pending => write!(f, "pending"),
GoalStatus::Active => write!(f, "active"),
GoalStatus::Complete => write!(f, "complete"),
GoalStatus::Failed => write!(f, "failed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Goal {
pub id: String,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_goal: Option<String>,
#[serde(default)]
pub status: GoalStatus,
#[serde(default)]
pub metrics: HashMap<String, String>,
}
impl Goal {
pub fn new(id: impl Into<String>, description: impl Into<String>) -> Self {
Self {
id: id.into(),
description: description.into(),
parent_goal: None,
status: GoalStatus::Pending,
metrics: HashMap::new(),
}
}
pub fn child(
id: impl Into<String>,
description: impl Into<String>,
parent_id: impl Into<String>,
) -> Self {
Self {
id: id.into(),
description: description.into(),
parent_goal: Some(parent_id.into()),
status: GoalStatus::Pending,
metrics: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GoalTree {
pub goals: Vec<Goal>,
}
impl GoalTree {
pub fn new() -> Self {
Self { goals: Vec::new() }
}
pub fn add(&mut self, goal: Goal) {
self.goals.push(goal);
}
pub fn find(&self, id: &str) -> Option<&Goal> {
self.goals.iter().find(|g| g.id == id)
}
pub fn find_mut(&mut self, id: &str) -> Option<&mut Goal> {
self.goals.iter_mut().find(|g| g.id == id)
}
pub fn roots(&self) -> Vec<&Goal> {
self.goals.iter().filter(|g| g.parent_goal.is_none()).collect()
}
pub fn children(&self, parent_id: &str) -> Vec<&Goal> {
self.goals
.iter()
.filter(|g| g.parent_goal.as_deref() == Some(parent_id))
.collect()
}
pub fn descendants(&self, parent_id: &str) -> Vec<&Goal> {
let mut result = Vec::new();
let mut stack: Vec<&str> = vec![parent_id];
while let Some(current) = stack.pop() {
for child in self.children(current) {
result.push(child);
stack.push(&child.id);
}
}
result
}
pub fn by_status(&self, status: GoalStatus) -> Vec<&Goal> {
self.goals.iter().filter(|g| g.status == status).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn goal_status_default_is_pending() {
assert_eq!(GoalStatus::default(), GoalStatus::Pending);
}
#[test]
fn goal_status_serde() {
let statuses = [
(GoalStatus::Pending, "\"pending\""),
(GoalStatus::Active, "\"active\""),
(GoalStatus::Complete, "\"complete\""),
(GoalStatus::Failed, "\"failed\""),
];
for (status, expected) in &statuses {
let json = serde_json::to_string(status).unwrap();
assert_eq!(&json, expected);
let restored: GoalStatus = serde_json::from_str(&json).unwrap();
assert_eq!(&restored, status);
}
}
#[test]
fn goal_status_display() {
assert_eq!(GoalStatus::Active.to_string(), "active");
assert_eq!(GoalStatus::Failed.to_string(), "failed");
}
#[test]
fn goal_new_top_level() {
let g = Goal::new("g1", "Increase revenue");
assert_eq!(g.id, "g1");
assert!(g.parent_goal.is_none());
assert_eq!(g.status, GoalStatus::Pending);
assert!(g.metrics.is_empty());
}
#[test]
fn goal_child() {
let g = Goal::child("g2", "Hire sales team", "g1");
assert_eq!(g.parent_goal.as_deref(), Some("g1"));
}
#[test]
fn goal_serde_roundtrip() {
let mut g = Goal::new("g1", "Ship v2");
g.status = GoalStatus::Active;
g.metrics.insert("pct".into(), "50".into());
let json = serde_json::to_string(&g).unwrap();
let restored: Goal = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, "g1");
assert_eq!(restored.status, GoalStatus::Active);
assert_eq!(restored.metrics.get("pct").unwrap(), "50");
}
#[test]
fn goal_omits_none_parent() {
let g = Goal::new("g1", "top");
let json = serde_json::to_string(&g).unwrap();
assert!(!json.contains("parent_goal"));
}
#[test]
fn goal_tree_empty() {
let tree = GoalTree::new();
assert!(tree.goals.is_empty());
assert!(tree.roots().is_empty());
}
#[test]
fn goal_tree_add_and_find() {
let mut tree = GoalTree::new();
tree.add(Goal::new("g1", "Revenue"));
assert!(tree.find("g1").is_some());
assert!(tree.find("nonexistent").is_none());
}
#[test]
fn goal_tree_find_mut() {
let mut tree = GoalTree::new();
tree.add(Goal::new("g1", "Revenue"));
tree.find_mut("g1").unwrap().status = GoalStatus::Complete;
assert_eq!(tree.find("g1").unwrap().status, GoalStatus::Complete);
}
#[test]
fn goal_tree_roots_and_children() {
let mut tree = GoalTree::new();
tree.add(Goal::new("root1", "Strategic goal A"));
tree.add(Goal::new("root2", "Strategic goal B"));
tree.add(Goal::child("sub1", "Sub-goal 1", "root1"));
tree.add(Goal::child("sub2", "Sub-goal 2", "root1"));
tree.add(Goal::child("sub3", "Sub-goal 3", "root2"));
let roots = tree.roots();
assert_eq!(roots.len(), 2);
let children = tree.children("root1");
assert_eq!(children.len(), 2);
assert!(children.iter().any(|g| g.id == "sub1"));
assert!(children.iter().any(|g| g.id == "sub2"));
let r2_children = tree.children("root2");
assert_eq!(r2_children.len(), 1);
}
#[test]
fn goal_tree_descendants() {
let mut tree = GoalTree::new();
tree.add(Goal::new("r", "Root"));
tree.add(Goal::child("a", "Child A", "r"));
tree.add(Goal::child("b", "Child B", "r"));
tree.add(Goal::child("a1", "Grandchild A1", "a"));
tree.add(Goal::child("a2", "Grandchild A2", "a"));
let desc = tree.descendants("r");
assert_eq!(desc.len(), 4);
let a_desc = tree.descendants("a");
assert_eq!(a_desc.len(), 2);
let leaf_desc = tree.descendants("a1");
assert!(leaf_desc.is_empty());
}
#[test]
fn goal_tree_by_status() {
let mut tree = GoalTree::new();
let mut g1 = Goal::new("g1", "Active goal");
g1.status = GoalStatus::Active;
let mut g2 = Goal::new("g2", "Complete goal");
g2.status = GoalStatus::Complete;
tree.add(g1);
tree.add(g2);
tree.add(Goal::new("g3", "Pending goal"));
assert_eq!(tree.by_status(GoalStatus::Active).len(), 1);
assert_eq!(tree.by_status(GoalStatus::Pending).len(), 1);
assert_eq!(tree.by_status(GoalStatus::Complete).len(), 1);
assert_eq!(tree.by_status(GoalStatus::Failed).len(), 0);
}
#[test]
fn goal_tree_serde_roundtrip() {
let mut tree = GoalTree::new();
tree.add(Goal::new("g1", "Top"));
tree.add(Goal::child("g2", "Sub", "g1"));
let json = serde_json::to_string(&tree).unwrap();
let restored: GoalTree = serde_json::from_str(&json).unwrap();
assert_eq!(restored.goals.len(), 2);
assert_eq!(restored.find("g2").unwrap().parent_goal.as_deref(), Some("g1"));
}
}