use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use ts_rs::TS;
use crate::node::Node;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export, export_to = "../../../bindings/napi/generated/")]
pub struct Workout {
pub id: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sport: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
pub root: Node,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, serde_json::Value>,
}
impl Workout {
pub const SCHEMA_VERSION: &str = "1.0.0";
pub fn new(id: impl Into<String>, root: Node) -> Self {
Self {
id: id.into(),
version: Self::SCHEMA_VERSION.into(),
sport: None,
date: None,
root,
metadata: BTreeMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::execution_mode::ExecutionMode;
use crate::measure::{Measure, WeightUnit};
use crate::node::*;
fn make_simple_workout() -> Workout {
let set = Node {
id: NodeId::from_string("s1"),
kind: NodeKind::Set,
name: None,
children: vec![],
payload: NodePayload::Leaf {
measures: vec![
Measure::Weight { amount: 60.0, unit: WeightUnit::Kg },
Measure::Reps(10),
],
intensity: None,
},
metadata: BTreeMap::new(),
};
let exercise = Node {
id: NodeId::from_string("e1"),
kind: NodeKind::Exercise,
name: Some("Squat".into()),
children: vec![set],
payload: NodePayload::Exercise {
measures: vec![],
intensity: None,
rest_seconds: Some(120.0),
},
metadata: BTreeMap::new(),
};
let block = Node {
id: NodeId::from_string("b1"),
kind: NodeKind::Block,
name: None,
children: vec![exercise],
payload: NodePayload::Block {
execution_mode: ExecutionMode::Sequential,
rest_seconds: None,
},
metadata: BTreeMap::new(),
};
let session = Node {
id: NodeId::from_string("sess1"),
kind: NodeKind::Session,
name: Some("Leg Day".into()),
children: vec![block],
payload: NodePayload::Temporal { rest_seconds: None },
metadata: BTreeMap::new(),
};
let mut w = Workout::new("w1", session);
w.sport = Some("strength".into());
w.date = Some("2026-03-29".into());
w
}
#[test]
fn workout_round_trip() {
let w = make_simple_workout();
let json = serde_json::to_string_pretty(&w).unwrap();
let back: Workout = serde_json::from_str(&json).unwrap();
assert_eq!(back.id, "w1");
assert_eq!(back.version, "1.0.0");
assert_eq!(back.sport, Some("strength".into()));
assert_eq!(back.root.name, Some("Leg Day".into()));
}
#[test]
fn workout_default_version() {
let node = Node {
id: NodeId::from_string("r"),
kind: NodeKind::Session,
name: None,
children: vec![],
payload: NodePayload::Temporal { rest_seconds: None },
metadata: BTreeMap::new(),
};
let w = Workout::new("test", node);
assert_eq!(w.version, "1.0.0");
}
}