1use crate::execution_mode::ExecutionMode;
2use crate::intensity::Intensity;
3use crate::measure::Measure;
4use crate::node::{Node, NodePayload};
5
6pub trait Visit {
21 fn visit_tree(&mut self, root: &Node) {
23 self.visit_node(root, &[]);
24 }
25
26 fn visit_node(&mut self, node: &Node, ancestors: &[&Node]) {
27 walk_node(self, node, ancestors);
28 }
29
30 fn visit_leaf(&mut self, _measures: &[Measure], _intensity: Option<&Intensity>, _ancestors: &[&Node]) {}
31
32 fn visit_exercise(&mut self, _name: Option<&str>, _measures: &[Measure], _intensity: Option<&Intensity>, _ancestors: &[&Node]) {}
33
34 fn visit_block(&mut self, _mode: &ExecutionMode, _ancestors: &[&Node]) {}
35}
36
37pub fn walk_node<V: Visit + ?Sized>(v: &mut V, node: &Node, ancestors: &[&Node]) {
38 match &node.payload {
39 NodePayload::Leaf { measures, intensity } => {
40 v.visit_leaf(measures, intensity.as_ref(), ancestors);
41 }
42 NodePayload::Exercise { measures, intensity, .. } => {
43 v.visit_exercise(node.name.as_deref(), measures, intensity.as_ref(), ancestors);
44 }
45 NodePayload::Block { execution_mode, .. } => {
46 v.visit_block(execution_mode, ancestors);
47 }
48 NodePayload::Temporal { .. } | NodePayload::Custom { .. } => {}
49 }
50 let mut child_ancestors = ancestors.to_vec();
51 child_ancestors.push(node);
52 for child in &node.children {
53 v.visit_node(child, &child_ancestors);
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use crate::execution_mode::ExecutionMode;
61 use crate::measure::{Measure, WeightUnit};
62 use crate::node::*;
63 use std::collections::BTreeMap;
64
65 struct TreeStats {
66 sets: usize,
67 blocks: usize,
68 exercises: Vec<String>,
69 }
70
71 impl Visit for TreeStats {
72 fn visit_leaf(&mut self, _measures: &[Measure], _intensity: Option<&Intensity>, _ancestors: &[&Node]) {
73 self.sets += 1;
74 }
75 fn visit_block(&mut self, _mode: &ExecutionMode, _ancestors: &[&Node]) {
76 self.blocks += 1;
77 }
78 fn visit_exercise(&mut self, name: Option<&str>, _measures: &[Measure], _intensity: Option<&Intensity>, _ancestors: &[&Node]) {
79 if let Some(n) = name {
80 self.exercises.push(n.to_string());
81 }
82 }
83 }
84
85 fn make_superset_session() -> Node {
86 let set_a1 = Node {
87 id: NodeId::from_string("sa1"), kind: NodeKind::Set, name: None, children: vec![],
88 payload: NodePayload::Leaf { measures: vec![Measure::Weight { amount: 30.0, unit: WeightUnit::Kg }, Measure::Reps(10)], intensity: None },
89 metadata: BTreeMap::new(),
90 };
91 let set_a2 = Node {
92 id: NodeId::from_string("sa2"), kind: NodeKind::Set, name: None, children: vec![],
93 payload: NodePayload::Leaf { measures: vec![Measure::Weight { amount: 30.0, unit: WeightUnit::Kg }, Measure::Reps(10)], intensity: None },
94 metadata: BTreeMap::new(),
95 };
96 let ex_a = Node {
97 id: NodeId::from_string("ea"), kind: NodeKind::Exercise, name: Some("DB Row".into()),
98 children: vec![set_a1, set_a2],
99 payload: NodePayload::Exercise { measures: vec![], intensity: None, rest_seconds: None },
100 metadata: BTreeMap::new(),
101 };
102 let set_b1 = Node {
103 id: NodeId::from_string("sb1"), kind: NodeKind::Set, name: None, children: vec![],
104 payload: NodePayload::Leaf { measures: vec![Measure::Weight { amount: 25.0, unit: WeightUnit::Kg }, Measure::Reps(12)], intensity: None },
105 metadata: BTreeMap::new(),
106 };
107 let ex_b = Node {
108 id: NodeId::from_string("eb"), kind: NodeKind::Exercise, name: Some("Lateral Raise".into()),
109 children: vec![set_b1],
110 payload: NodePayload::Exercise { measures: vec![], intensity: None, rest_seconds: None },
111 metadata: BTreeMap::new(),
112 };
113 let block = Node {
114 id: NodeId::from_string("blk"), kind: NodeKind::Block, name: Some("Superset A".into()),
115 children: vec![ex_a, ex_b],
116 payload: NodePayload::Block { execution_mode: ExecutionMode::Parallel, rest_seconds: Some(90.0) },
117 metadata: BTreeMap::new(),
118 };
119 Node {
120 id: NodeId::from_string("sess"), kind: NodeKind::Session, name: Some("Upper".into()),
121 children: vec![block],
122 payload: NodePayload::Temporal { rest_seconds: None },
123 metadata: BTreeMap::new(),
124 }
125 }
126
127 #[test]
128 fn visit_counts_sets_and_blocks() {
129 let session = make_superset_session();
130 let mut stats = TreeStats { sets: 0, blocks: 0, exercises: vec![] };
131 stats.visit_tree(&session);
132 assert_eq!(stats.sets, 3);
133 assert_eq!(stats.blocks, 1);
134 assert_eq!(stats.exercises, vec!["DB Row", "Lateral Raise"]);
135 }
136
137 struct SetWithExercise {
139 results: Vec<(String, String)>, }
141
142 impl Visit for SetWithExercise {
143 fn visit_leaf(&mut self, _measures: &[Measure], _: Option<&Intensity>, ancestors: &[&Node]) {
144 let exercise_name = ancestors.iter().rev()
146 .find(|n| matches!(n.kind, NodeKind::Exercise))
147 .and_then(|n| n.name.as_deref())
148 .unwrap_or("unknown");
149 let set_id = ancestors.last()
150 .map(|_| "set")
151 .unwrap_or("orphan");
152 self.results.push((exercise_name.to_string(), set_id.to_string()));
154 }
155 }
156
157 #[test]
158 fn visit_with_path_context() {
159 let session = make_superset_session();
160 let mut finder = SetWithExercise { results: vec![] };
161 finder.visit_tree(&session);
162
163 assert_eq!(finder.results.len(), 3);
165 assert_eq!(finder.results[0].0, "DB Row");
166 assert_eq!(finder.results[1].0, "DB Row");
167 assert_eq!(finder.results[2].0, "Lateral Raise");
168 }
169}