1use serde::{Deserialize, Serialize};
2use ts_rs::TS;
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
6#[ts(export, export_to = "../../../bindings/napi/generated/")]
7pub struct Position {
8 pub lat: f64,
9 pub lng: f64,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 pub altitude: Option<f64>,
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
19#[ts(export, export_to = "../../../bindings/napi/generated/")]
20pub enum StreamMetric {
21 HeartRate,
22 Speed,
23 Cadence,
24 Power,
25 Altitude,
26 Grade,
27 Temperature,
28 Position,
29 Custom(String),
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
38#[ts(export, export_to = "../../../bindings/napi/generated/")]
39#[serde(tag = "type", content = "values")]
40pub enum StreamData {
41 Scalar(Vec<f64>),
44 Position(Vec<Option<Position>>),
46}
47
48impl StreamData {
49 pub fn len(&self) -> usize {
51 match self {
52 StreamData::Scalar(v) => v.len(),
53 StreamData::Position(v) => v.len(),
54 }
55 }
56
57 pub fn is_empty(&self) -> bool {
58 self.len() == 0
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
67#[ts(export, export_to = "../../../bindings/napi/generated/")]
68pub struct Stream {
69 pub metric: StreamMetric,
70 pub data: StreamData,
71}
72
73#[derive(Debug, Clone, PartialEq)]
75pub struct StreamValidationError {
76 pub message: String,
77}
78
79impl std::fmt::Display for StreamValidationError {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(f, "{}", self.message)
82 }
83}
84
85impl std::error::Error for StreamValidationError {}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
108#[ts(export, export_to = "../../../bindings/napi/generated/")]
109pub struct Streams {
110 pub timestamps: Vec<f64>,
113
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
116 pub channels: Vec<Stream>,
117}
118
119impl Streams {
120 pub fn validate(&self) -> Result<(), StreamValidationError> {
126 let expected = self.timestamps.len();
127 for (i, channel) in self.channels.iter().enumerate() {
128 let actual = channel.data.len();
129 if actual != expected {
130 return Err(StreamValidationError {
131 message: format!(
132 "channel {} ({:?}) has {} samples, expected {} (timestamps.len())",
133 i, channel.metric, actual, expected
134 ),
135 });
136 }
137 }
138 Ok(())
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn scalar_stream_round_trip() {
148 let stream = Stream {
149 metric: StreamMetric::HeartRate,
150 data: StreamData::Scalar(vec![85.0, 125.0, 130.0, 129.0]),
151 };
152 let json = serde_json::to_string_pretty(&stream).unwrap();
153 let back: Stream = serde_json::from_str(&json).unwrap();
154 assert_eq!(back, stream);
155 assert!(json.contains(r#""type": "Scalar""#));
156 }
157
158 #[test]
159 fn position_stream_round_trip() {
160 let stream = Stream {
161 metric: StreamMetric::Position,
162 data: StreamData::Position(vec![
163 Some(Position { lat: 51.5074, lng: -0.1278, altitude: Some(45.0) }),
164 None, Some(Position { lat: 51.5075, lng: -0.1277, altitude: None }),
166 ]),
167 };
168 let json = serde_json::to_string_pretty(&stream).unwrap();
169 let back: Stream = serde_json::from_str(&json).unwrap();
170 assert_eq!(back, stream);
171 }
172
173 #[test]
174 fn streams_full_round_trip() {
175 let streams = Streams {
176 timestamps: vec![0.0, 5.0, 10.0, 15.0],
177 channels: vec![
178 Stream {
179 metric: StreamMetric::HeartRate,
180 data: StreamData::Scalar(vec![85.0, 125.0, 130.0, 129.0]),
181 },
182 Stream {
183 metric: StreamMetric::Speed,
184 data: StreamData::Scalar(vec![0.0, 2.5, 2.7, 2.8]),
185 },
186 Stream {
187 metric: StreamMetric::Position,
188 data: StreamData::Position(vec![
189 Some(Position { lat: 51.5, lng: -0.1, altitude: Some(45.0) }),
190 Some(Position { lat: 51.5, lng: -0.1, altitude: Some(46.0) }),
191 Some(Position { lat: 51.5, lng: -0.1, altitude: Some(48.0) }),
192 Some(Position { lat: 51.5, lng: -0.1, altitude: Some(52.0) }),
193 ]),
194 },
195 ],
196 };
197 let json = serde_json::to_string_pretty(&streams).unwrap();
198 let back: Streams = serde_json::from_str(&json).unwrap();
199 assert_eq!(back, streams);
200 }
201
202 #[test]
203 fn streams_validate_valid() {
204 let streams = Streams {
205 timestamps: vec![0.0, 5.0, 10.0],
206 channels: vec![
207 Stream {
208 metric: StreamMetric::HeartRate,
209 data: StreamData::Scalar(vec![85.0, 125.0, 130.0]),
210 },
211 Stream {
212 metric: StreamMetric::Position,
213 data: StreamData::Position(vec![
214 Some(Position { lat: 51.5, lng: -0.1, altitude: None }),
215 None,
216 Some(Position { lat: 51.5, lng: -0.1, altitude: None }),
217 ]),
218 },
219 ],
220 };
221 assert!(streams.validate().is_ok());
222 }
223
224 #[test]
225 fn streams_validate_invalid_scalar() {
226 let streams = Streams {
227 timestamps: vec![0.0, 5.0, 10.0],
228 channels: vec![
229 Stream {
230 metric: StreamMetric::HeartRate,
231 data: StreamData::Scalar(vec![85.0, 125.0]), },
233 ],
234 };
235 let err = streams.validate().unwrap_err();
236 assert!(err.message.contains("2 samples"));
237 assert!(err.message.contains("expected 3"));
238 }
239
240 #[test]
241 fn streams_validate_invalid_position() {
242 let streams = Streams {
243 timestamps: vec![0.0, 5.0, 10.0],
244 channels: vec![
245 Stream {
246 metric: StreamMetric::Position,
247 data: StreamData::Position(vec![
248 Some(Position { lat: 51.5, lng: -0.1, altitude: None }),
249 None,
250 None,
251 None, ]),
253 },
254 ],
255 };
256 let err = streams.validate().unwrap_err();
257 assert!(err.message.contains("4 samples"));
258 assert!(err.message.contains("expected 3"));
259 }
260
261 #[test]
262 fn stream_metric_variants_round_trip() {
263 let variants = vec![
264 StreamMetric::HeartRate,
265 StreamMetric::Speed,
266 StreamMetric::Cadence,
267 StreamMetric::Power,
268 StreamMetric::Altitude,
269 StreamMetric::Grade,
270 StreamMetric::Temperature,
271 StreamMetric::Position,
272 StreamMetric::Custom("VO2".into()),
273 ];
274 for metric in variants {
275 let json = serde_json::to_string(&metric).unwrap();
276 let back: StreamMetric = serde_json::from_str(&json).unwrap();
277 assert_eq!(back, metric);
278 }
279 }
280
281 #[test]
282 fn stream_data_len() {
283 let scalar = StreamData::Scalar(vec![1.0, 2.0, 3.0]);
284 assert_eq!(scalar.len(), 3);
285 assert!(!scalar.is_empty());
286
287 let pos = StreamData::Position(vec![None, None]);
288 assert_eq!(pos.len(), 2);
289
290 let empty = StreamData::Scalar(vec![]);
291 assert!(empty.is_empty());
292 }
293
294 #[test]
295 fn empty_streams_round_trip() {
296 let streams = Streams {
297 timestamps: vec![],
298 channels: vec![],
299 };
300 let json = serde_json::to_string(&streams).unwrap();
301 let back: Streams = serde_json::from_str(&json).unwrap();
302 assert_eq!(back, streams);
303 assert!(streams.validate().is_ok());
304 }
305}