1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11#[non_exhaustive]
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum GoalStatus {
16 Pending,
18 Active,
20 Complete,
22 Failed,
24}
25
26impl Default for GoalStatus {
27 fn default() -> Self {
28 Self::Pending
29 }
30}
31
32impl std::fmt::Display for GoalStatus {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 GoalStatus::Pending => write!(f, "pending"),
36 GoalStatus::Active => write!(f, "active"),
37 GoalStatus::Complete => write!(f, "complete"),
38 GoalStatus::Failed => write!(f, "failed"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Goal {
46 pub id: String,
48 pub description: String,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub parent_goal: Option<String>,
53 #[serde(default)]
55 pub status: GoalStatus,
56 #[serde(default)]
58 pub metrics: HashMap<String, String>,
59}
60
61impl Goal {
62 pub fn new(id: impl Into<String>, description: impl Into<String>) -> Self {
64 Self {
65 id: id.into(),
66 description: description.into(),
67 parent_goal: None,
68 status: GoalStatus::Pending,
69 metrics: HashMap::new(),
70 }
71 }
72
73 pub fn child(
75 id: impl Into<String>,
76 description: impl Into<String>,
77 parent_id: impl Into<String>,
78 ) -> Self {
79 Self {
80 id: id.into(),
81 description: description.into(),
82 parent_goal: Some(parent_id.into()),
83 status: GoalStatus::Pending,
84 metrics: HashMap::new(),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Default, Serialize, Deserialize)]
91pub struct GoalTree {
92 pub goals: Vec<Goal>,
94}
95
96impl GoalTree {
97 pub fn new() -> Self {
99 Self { goals: Vec::new() }
100 }
101
102 pub fn add(&mut self, goal: Goal) {
104 self.goals.push(goal);
105 }
106
107 pub fn find(&self, id: &str) -> Option<&Goal> {
109 self.goals.iter().find(|g| g.id == id)
110 }
111
112 pub fn find_mut(&mut self, id: &str) -> Option<&mut Goal> {
114 self.goals.iter_mut().find(|g| g.id == id)
115 }
116
117 pub fn roots(&self) -> Vec<&Goal> {
119 self.goals.iter().filter(|g| g.parent_goal.is_none()).collect()
120 }
121
122 pub fn children(&self, parent_id: &str) -> Vec<&Goal> {
124 self.goals
125 .iter()
126 .filter(|g| g.parent_goal.as_deref() == Some(parent_id))
127 .collect()
128 }
129
130 pub fn descendants(&self, parent_id: &str) -> Vec<&Goal> {
132 let mut result = Vec::new();
133 let mut stack: Vec<&str> = vec![parent_id];
134 while let Some(current) = stack.pop() {
135 for child in self.children(current) {
136 result.push(child);
137 stack.push(&child.id);
138 }
139 }
140 result
141 }
142
143 pub fn by_status(&self, status: GoalStatus) -> Vec<&Goal> {
145 self.goals.iter().filter(|g| g.status == status).collect()
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn goal_status_default_is_pending() {
155 assert_eq!(GoalStatus::default(), GoalStatus::Pending);
156 }
157
158 #[test]
159 fn goal_status_serde() {
160 let statuses = [
161 (GoalStatus::Pending, "\"pending\""),
162 (GoalStatus::Active, "\"active\""),
163 (GoalStatus::Complete, "\"complete\""),
164 (GoalStatus::Failed, "\"failed\""),
165 ];
166 for (status, expected) in &statuses {
167 let json = serde_json::to_string(status).unwrap();
168 assert_eq!(&json, expected);
169 let restored: GoalStatus = serde_json::from_str(&json).unwrap();
170 assert_eq!(&restored, status);
171 }
172 }
173
174 #[test]
175 fn goal_status_display() {
176 assert_eq!(GoalStatus::Active.to_string(), "active");
177 assert_eq!(GoalStatus::Failed.to_string(), "failed");
178 }
179
180 #[test]
181 fn goal_new_top_level() {
182 let g = Goal::new("g1", "Increase revenue");
183 assert_eq!(g.id, "g1");
184 assert!(g.parent_goal.is_none());
185 assert_eq!(g.status, GoalStatus::Pending);
186 assert!(g.metrics.is_empty());
187 }
188
189 #[test]
190 fn goal_child() {
191 let g = Goal::child("g2", "Hire sales team", "g1");
192 assert_eq!(g.parent_goal.as_deref(), Some("g1"));
193 }
194
195 #[test]
196 fn goal_serde_roundtrip() {
197 let mut g = Goal::new("g1", "Ship v2");
198 g.status = GoalStatus::Active;
199 g.metrics.insert("pct".into(), "50".into());
200 let json = serde_json::to_string(&g).unwrap();
201 let restored: Goal = serde_json::from_str(&json).unwrap();
202 assert_eq!(restored.id, "g1");
203 assert_eq!(restored.status, GoalStatus::Active);
204 assert_eq!(restored.metrics.get("pct").unwrap(), "50");
205 }
206
207 #[test]
208 fn goal_omits_none_parent() {
209 let g = Goal::new("g1", "top");
210 let json = serde_json::to_string(&g).unwrap();
211 assert!(!json.contains("parent_goal"));
212 }
213
214 #[test]
215 fn goal_tree_empty() {
216 let tree = GoalTree::new();
217 assert!(tree.goals.is_empty());
218 assert!(tree.roots().is_empty());
219 }
220
221 #[test]
222 fn goal_tree_add_and_find() {
223 let mut tree = GoalTree::new();
224 tree.add(Goal::new("g1", "Revenue"));
225 assert!(tree.find("g1").is_some());
226 assert!(tree.find("nonexistent").is_none());
227 }
228
229 #[test]
230 fn goal_tree_find_mut() {
231 let mut tree = GoalTree::new();
232 tree.add(Goal::new("g1", "Revenue"));
233 tree.find_mut("g1").unwrap().status = GoalStatus::Complete;
234 assert_eq!(tree.find("g1").unwrap().status, GoalStatus::Complete);
235 }
236
237 #[test]
238 fn goal_tree_roots_and_children() {
239 let mut tree = GoalTree::new();
240 tree.add(Goal::new("root1", "Strategic goal A"));
241 tree.add(Goal::new("root2", "Strategic goal B"));
242 tree.add(Goal::child("sub1", "Sub-goal 1", "root1"));
243 tree.add(Goal::child("sub2", "Sub-goal 2", "root1"));
244 tree.add(Goal::child("sub3", "Sub-goal 3", "root2"));
245
246 let roots = tree.roots();
247 assert_eq!(roots.len(), 2);
248
249 let children = tree.children("root1");
250 assert_eq!(children.len(), 2);
251 assert!(children.iter().any(|g| g.id == "sub1"));
252 assert!(children.iter().any(|g| g.id == "sub2"));
253
254 let r2_children = tree.children("root2");
255 assert_eq!(r2_children.len(), 1);
256 }
257
258 #[test]
259 fn goal_tree_descendants() {
260 let mut tree = GoalTree::new();
261 tree.add(Goal::new("r", "Root"));
262 tree.add(Goal::child("a", "Child A", "r"));
263 tree.add(Goal::child("b", "Child B", "r"));
264 tree.add(Goal::child("a1", "Grandchild A1", "a"));
265 tree.add(Goal::child("a2", "Grandchild A2", "a"));
266
267 let desc = tree.descendants("r");
268 assert_eq!(desc.len(), 4);
269
270 let a_desc = tree.descendants("a");
271 assert_eq!(a_desc.len(), 2);
272
273 let leaf_desc = tree.descendants("a1");
274 assert!(leaf_desc.is_empty());
275 }
276
277 #[test]
278 fn goal_tree_by_status() {
279 let mut tree = GoalTree::new();
280 let mut g1 = Goal::new("g1", "Active goal");
281 g1.status = GoalStatus::Active;
282 let mut g2 = Goal::new("g2", "Complete goal");
283 g2.status = GoalStatus::Complete;
284 tree.add(g1);
285 tree.add(g2);
286 tree.add(Goal::new("g3", "Pending goal"));
287
288 assert_eq!(tree.by_status(GoalStatus::Active).len(), 1);
289 assert_eq!(tree.by_status(GoalStatus::Pending).len(), 1);
290 assert_eq!(tree.by_status(GoalStatus::Complete).len(), 1);
291 assert_eq!(tree.by_status(GoalStatus::Failed).len(), 0);
292 }
293
294 #[test]
295 fn goal_tree_serde_roundtrip() {
296 let mut tree = GoalTree::new();
297 tree.add(Goal::new("g1", "Top"));
298 tree.add(Goal::child("g2", "Sub", "g1"));
299 let json = serde_json::to_string(&tree).unwrap();
300 let restored: GoalTree = serde_json::from_str(&json).unwrap();
301 assert_eq!(restored.goals.len(), 2);
302 assert_eq!(restored.find("g2").unwrap().parent_goal.as_deref(), Some("g1"));
303 }
304}