syntax_workout_core/
workout.rs1use 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}