Skip to main content

syntax_workout_core/
workout.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use ts_rs::TS;
4
5use crate::node::Node;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
8#[ts(export, export_to = "../../../bindings/napi/generated/")]
9pub struct Workout {
10    pub id: String,
11    pub version: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub sport: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub date: Option<String>,
16    pub root: Node,
17    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
18    pub metadata: BTreeMap<String, serde_json::Value>,
19}
20
21impl Workout {
22    pub const SCHEMA_VERSION: &str = "1.0.0";
23
24    pub fn new(id: impl Into<String>, root: Node) -> Self {
25        Self {
26            id: id.into(),
27            version: Self::SCHEMA_VERSION.into(),
28            sport: None,
29            date: None,
30            root,
31            metadata: BTreeMap::new(),
32        }
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use crate::execution_mode::ExecutionMode;
40    use crate::measure::{Measure, WeightUnit};
41    use crate::node::*;
42
43    fn make_simple_workout() -> Workout {
44        let set = Node {
45            id: NodeId::from_string("s1"),
46            kind: NodeKind::Set,
47            name: None,
48            children: vec![],
49            payload: NodePayload::Leaf {
50                measures: vec![
51                    Measure::Weight { amount: 60.0, unit: WeightUnit::Kg },
52                    Measure::Reps(10),
53                ],
54                intensity: None,
55            },
56            metadata: BTreeMap::new(),
57        };
58
59        let exercise = Node {
60            id: NodeId::from_string("e1"),
61            kind: NodeKind::Exercise,
62            name: Some("Squat".into()),
63            children: vec![set],
64            payload: NodePayload::Exercise {
65                measures: vec![],
66                intensity: None,
67                rest_seconds: Some(120.0),
68            },
69            metadata: BTreeMap::new(),
70        };
71
72        let block = Node {
73            id: NodeId::from_string("b1"),
74            kind: NodeKind::Block,
75            name: None,
76            children: vec![exercise],
77            payload: NodePayload::Block {
78                execution_mode: ExecutionMode::Sequential,
79                rest_seconds: None,
80            },
81            metadata: BTreeMap::new(),
82        };
83
84        let session = Node {
85            id: NodeId::from_string("sess1"),
86            kind: NodeKind::Session,
87            name: Some("Leg Day".into()),
88            children: vec![block],
89            payload: NodePayload::Temporal { rest_seconds: None },
90            metadata: BTreeMap::new(),
91        };
92
93        let mut w = Workout::new("w1", session);
94        w.sport = Some("strength".into());
95        w.date = Some("2026-03-29".into());
96        w
97    }
98
99    #[test]
100    fn workout_round_trip() {
101        let w = make_simple_workout();
102        let json = serde_json::to_string_pretty(&w).unwrap();
103        let back: Workout = serde_json::from_str(&json).unwrap();
104        assert_eq!(back.id, "w1");
105        assert_eq!(back.version, "1.0.0");
106        assert_eq!(back.sport, Some("strength".into()));
107        assert_eq!(back.root.name, Some("Leg Day".into()));
108    }
109
110    #[test]
111    fn workout_default_version() {
112        let node = Node {
113            id: NodeId::from_string("r"),
114            kind: NodeKind::Session,
115            name: None,
116            children: vec![],
117            payload: NodePayload::Temporal { rest_seconds: None },
118            metadata: BTreeMap::new(),
119        };
120        let w = Workout::new("test", node);
121        assert_eq!(w.version, "1.0.0");
122    }
123}