Skip to main content

syntax_workout_core/
visit.rs

1use crate::execution_mode::ExecutionMode;
2use crate::intensity::Intensity;
3use crate::measure::Measure;
4use crate::node::{Node, NodePayload};
5
6/// Immutable tree visitor with ancestor path context.
7///
8/// `ancestors` is a slice of parent nodes from root to immediate parent.
9/// Empty at the root node, grows as the walk descends.
10///
11/// Usage:
12///   struct MyVisitor;
13///   impl Visit for MyVisitor {
14///       fn visit_leaf(&mut self, measures: &[Measure], _: Option<&Intensity>, ancestors: &[&Node]) {
15///           // ancestors[0] is root, ancestors.last() is the parent Exercise/Block
16///       }
17///   }
18///   let mut v = MyVisitor;
19///   v.visit_tree(&workout.root);
20pub trait Visit {
21    /// Convenience entry point. Calls visit_node with empty ancestors.
22    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    /// Demonstrates path-aware search: find which exercise each set belongs to.
138    struct SetWithExercise {
139        results: Vec<(String, String)>, // (exercise_name, set_id)
140    }
141
142    impl Visit for SetWithExercise {
143        fn visit_leaf(&mut self, _measures: &[Measure], _: Option<&Intensity>, ancestors: &[&Node]) {
144            // Walk ancestors backwards to find the nearest Exercise
145            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            // We don't have the current node in ancestors, but we know it's a Set
153            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        // 3 sets, each should know its parent exercise
164        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}