syntax_workout_core/
lap.rs1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use ts_rs::TS;
4
5pub mod summary_keys {
10 pub const DISTANCE: &str = "distance";
11 pub const AVG_HR: &str = "avg_hr";
12 pub const MAX_HR: &str = "max_hr";
13 pub const AVG_SPEED: &str = "avg_speed";
14 pub const MAX_SPEED: &str = "max_speed";
15 pub const AVG_CADENCE: &str = "avg_cadence";
16 pub const AVG_POWER: &str = "avg_power";
17 pub const MAX_POWER: &str = "max_power";
18 pub const TOTAL_CALORIES: &str = "total_calories";
19 pub const ELEVATION_GAIN: &str = "elevation_gain";
20 pub const ELEVATION_LOSS: &str = "elevation_loss";
21 pub const DURATION: &str = "duration";
22 pub const STROKES: &str = "strokes";
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
27#[ts(export, export_to = "../../../bindings/napi/generated/")]
28pub enum LapTrigger {
29 Manual,
31 Distance,
33 Time,
35 HeartRateZone,
37 Auto,
39 Custom(String),
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
59#[ts(export, export_to = "../../../bindings/napi/generated/")]
60pub struct Lap {
61 #[serde(skip_serializing_if = "Option::is_none")]
64 pub start_index: Option<usize>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
69 pub end_index: Option<usize>,
70
71 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
73 pub summary: BTreeMap<String, f64>,
74
75 pub trigger: LapTrigger,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub name: Option<String>,
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn lap_round_trip() {
89 let mut summary = BTreeMap::new();
90 summary.insert(summary_keys::DISTANCE.into(), 1000.0);
91 summary.insert(summary_keys::AVG_HR.into(), 135.0);
92 summary.insert(summary_keys::DURATION.into(), 300.0);
93
94 let lap = Lap {
95 start_index: Some(0),
96 end_index: Some(119),
97 summary,
98 trigger: LapTrigger::Distance,
99 name: Some("km 1".into()),
100 };
101
102 let json = serde_json::to_string_pretty(&lap).unwrap();
103 let back: Lap = serde_json::from_str(&json).unwrap();
104 assert_eq!(back, lap);
105 assert!(json.contains(r#""distance""#));
106 assert!(json.contains("1000.0"));
107 }
108
109 #[test]
110 fn lap_without_indices() {
111 let mut summary = BTreeMap::new();
112 summary.insert(summary_keys::DISTANCE.into(), 5000.0);
113 summary.insert(summary_keys::DURATION.into(), 1650.0);
114
115 let lap = Lap {
116 start_index: None,
117 end_index: None,
118 summary,
119 trigger: LapTrigger::Auto,
120 name: Some("Full run".into()),
121 };
122
123 let json = serde_json::to_string_pretty(&lap).unwrap();
124 assert!(!json.contains("start_index"));
125 assert!(!json.contains("end_index"));
126 let back: Lap = serde_json::from_str(&json).unwrap();
127 assert_eq!(back, lap);
128 }
129
130 #[test]
131 fn lap_trigger_variants_round_trip() {
132 let variants = vec![
133 LapTrigger::Manual,
134 LapTrigger::Distance,
135 LapTrigger::Time,
136 LapTrigger::HeartRateZone,
137 LapTrigger::Auto,
138 LapTrigger::Custom("swim_length".into()),
139 ];
140 for trigger in variants {
141 let json = serde_json::to_string(&trigger).unwrap();
142 let back: LapTrigger = serde_json::from_str(&json).unwrap();
143 assert_eq!(back, trigger);
144 }
145 }
146
147 #[test]
148 fn lap_empty_summary_omitted() {
149 let lap = Lap {
150 start_index: Some(0),
151 end_index: Some(10),
152 summary: BTreeMap::new(),
153 trigger: LapTrigger::Manual,
154 name: None,
155 };
156 let json = serde_json::to_string(&lap).unwrap();
157 assert!(!json.contains("summary"));
158 assert!(!json.contains("name"));
159 }
160}