use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export, export_to = "../../../bindings/napi/generated/")]
pub struct Position {
pub lat: f64,
pub lng: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub altitude: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export, export_to = "../../../bindings/napi/generated/")]
pub enum StreamMetric {
HeartRate,
Speed,
Cadence,
Power,
Altitude,
Grade,
Temperature,
Position,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export, export_to = "../../../bindings/napi/generated/")]
#[serde(tag = "type", content = "values")]
pub enum StreamData {
Scalar(Vec<f64>),
Position(Vec<Option<Position>>),
}
impl StreamData {
pub fn len(&self) -> usize {
match self {
StreamData::Scalar(v) => v.len(),
StreamData::Position(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export, export_to = "../../../bindings/napi/generated/")]
pub struct Stream {
pub metric: StreamMetric,
pub data: StreamData,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StreamValidationError {
pub message: String,
}
impl std::fmt::Display for StreamValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for StreamValidationError {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export, export_to = "../../../bindings/napi/generated/")]
pub struct Streams {
pub timestamps: Vec<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub channels: Vec<Stream>,
}
impl Streams {
pub fn validate(&self) -> Result<(), StreamValidationError> {
let expected = self.timestamps.len();
for (i, channel) in self.channels.iter().enumerate() {
let actual = channel.data.len();
if actual != expected {
return Err(StreamValidationError {
message: format!(
"channel {} ({:?}) has {} samples, expected {} (timestamps.len())",
i, channel.metric, actual, expected
),
});
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalar_stream_round_trip() {
let stream = Stream {
metric: StreamMetric::HeartRate,
data: StreamData::Scalar(vec![85.0, 125.0, 130.0, 129.0]),
};
let json = serde_json::to_string_pretty(&stream).unwrap();
let back: Stream = serde_json::from_str(&json).unwrap();
assert_eq!(back, stream);
assert!(json.contains(r#""type": "Scalar""#));
}
#[test]
fn position_stream_round_trip() {
let stream = Stream {
metric: StreamMetric::Position,
data: StreamData::Position(vec![
Some(Position { lat: 51.5074, lng: -0.1278, altitude: Some(45.0) }),
None, Some(Position { lat: 51.5075, lng: -0.1277, altitude: None }),
]),
};
let json = serde_json::to_string_pretty(&stream).unwrap();
let back: Stream = serde_json::from_str(&json).unwrap();
assert_eq!(back, stream);
}
#[test]
fn streams_full_round_trip() {
let streams = Streams {
timestamps: vec![0.0, 5.0, 10.0, 15.0],
channels: vec![
Stream {
metric: StreamMetric::HeartRate,
data: StreamData::Scalar(vec![85.0, 125.0, 130.0, 129.0]),
},
Stream {
metric: StreamMetric::Speed,
data: StreamData::Scalar(vec![0.0, 2.5, 2.7, 2.8]),
},
Stream {
metric: StreamMetric::Position,
data: StreamData::Position(vec![
Some(Position { lat: 51.5, lng: -0.1, altitude: Some(45.0) }),
Some(Position { lat: 51.5, lng: -0.1, altitude: Some(46.0) }),
Some(Position { lat: 51.5, lng: -0.1, altitude: Some(48.0) }),
Some(Position { lat: 51.5, lng: -0.1, altitude: Some(52.0) }),
]),
},
],
};
let json = serde_json::to_string_pretty(&streams).unwrap();
let back: Streams = serde_json::from_str(&json).unwrap();
assert_eq!(back, streams);
}
#[test]
fn streams_validate_valid() {
let streams = Streams {
timestamps: vec![0.0, 5.0, 10.0],
channels: vec![
Stream {
metric: StreamMetric::HeartRate,
data: StreamData::Scalar(vec![85.0, 125.0, 130.0]),
},
Stream {
metric: StreamMetric::Position,
data: StreamData::Position(vec![
Some(Position { lat: 51.5, lng: -0.1, altitude: None }),
None,
Some(Position { lat: 51.5, lng: -0.1, altitude: None }),
]),
},
],
};
assert!(streams.validate().is_ok());
}
#[test]
fn streams_validate_invalid_scalar() {
let streams = Streams {
timestamps: vec![0.0, 5.0, 10.0],
channels: vec![
Stream {
metric: StreamMetric::HeartRate,
data: StreamData::Scalar(vec![85.0, 125.0]), },
],
};
let err = streams.validate().unwrap_err();
assert!(err.message.contains("2 samples"));
assert!(err.message.contains("expected 3"));
}
#[test]
fn streams_validate_invalid_position() {
let streams = Streams {
timestamps: vec![0.0, 5.0, 10.0],
channels: vec![
Stream {
metric: StreamMetric::Position,
data: StreamData::Position(vec![
Some(Position { lat: 51.5, lng: -0.1, altitude: None }),
None,
None,
None, ]),
},
],
};
let err = streams.validate().unwrap_err();
assert!(err.message.contains("4 samples"));
assert!(err.message.contains("expected 3"));
}
#[test]
fn stream_metric_variants_round_trip() {
let variants = vec![
StreamMetric::HeartRate,
StreamMetric::Speed,
StreamMetric::Cadence,
StreamMetric::Power,
StreamMetric::Altitude,
StreamMetric::Grade,
StreamMetric::Temperature,
StreamMetric::Position,
StreamMetric::Custom("VO2".into()),
];
for metric in variants {
let json = serde_json::to_string(&metric).unwrap();
let back: StreamMetric = serde_json::from_str(&json).unwrap();
assert_eq!(back, metric);
}
}
#[test]
fn stream_data_len() {
let scalar = StreamData::Scalar(vec![1.0, 2.0, 3.0]);
assert_eq!(scalar.len(), 3);
assert!(!scalar.is_empty());
let pos = StreamData::Position(vec![None, None]);
assert_eq!(pos.len(), 2);
let empty = StreamData::Scalar(vec![]);
assert!(empty.is_empty());
}
#[test]
fn empty_streams_round_trip() {
let streams = Streams {
timestamps: vec![],
channels: vec![],
};
let json = serde_json::to_string(&streams).unwrap();
let back: Streams = serde_json::from_str(&json).unwrap();
assert_eq!(back, streams);
assert!(streams.validate().is_ok());
}
}