use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OxiHumanMeta {
pub generator: String,
pub version: String,
pub exported_at: String,
pub measurements: Option<MeasurementsMeta>,
pub preset: Option<String>,
pub params: Option<ParamsMeta>,
pub expression: Option<String>,
pub target_count: Option<usize>,
pub policy: Option<String>,
pub extra: std::collections::HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MeasurementsMeta {
pub height_cm: Option<f32>,
pub weight_kg: Option<f32>,
pub chest_cm: Option<f32>,
pub waist_cm: Option<f32>,
pub hips_cm: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParamsMeta {
pub height: f32,
pub weight: f32,
pub muscle: f32,
pub age: f32,
}
impl OxiHumanMeta {
pub fn minimal() -> Self {
Self {
generator: "oxihuman-export".into(),
version: env!("CARGO_PKG_VERSION").into(),
exported_at: current_timestamp(),
measurements: None,
preset: None,
params: None,
expression: None,
target_count: None,
policy: None,
extra: Default::default(),
}
}
pub fn to_json(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
pub fn from_json(v: &Value) -> Option<Self> {
serde_json::from_value(v.clone()).ok()
}
pub fn with_params(mut self, height: f32, weight: f32, muscle: f32, age: f32) -> Self {
self.params = Some(ParamsMeta {
height,
weight,
muscle,
age,
});
self
}
pub fn with_measurements(mut self, m: MeasurementsMeta) -> Self {
self.measurements = Some(m);
self
}
}
fn current_timestamp() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let (y, mo, d, h, mi, sec) = unix_secs_to_datetime(secs);
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, mo, d, h, mi, sec)
}
fn unix_secs_to_datetime(secs: u64) -> (u32, u32, u32, u32, u32, u32) {
let sec = (secs % 60) as u32;
let min = ((secs / 60) % 60) as u32;
let hour = ((secs / 3600) % 24) as u32;
let days = secs / 86400;
let mut y = 1970u32;
let mut d = days;
loop {
let dy = if is_leap(y) { 366u64 } else { 365u64 };
if d < dy {
break;
}
d -= dy;
y += 1;
}
let months = [
31u32,
if is_leap(y) { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut mo = 1u32;
for &ml in &months {
if d < ml as u64 {
break;
}
d -= ml as u64;
mo += 1;
}
(y, mo, d as u32 + 1, hour, min, sec)
}
fn is_leap(y: u32) -> bool {
(y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimal_has_generator() {
assert_eq!(OxiHumanMeta::minimal().generator, "oxihuman-export");
}
#[test]
fn to_json_has_generator_key() {
let meta = OxiHumanMeta::minimal();
assert!(meta.to_json()["generator"].as_str().is_some());
}
#[test]
fn from_json_roundtrip() {
let meta = OxiHumanMeta::minimal();
let json = meta.to_json();
let back = OxiHumanMeta::from_json(&json).expect("deserialization failed");
assert_eq!(back.generator, "oxihuman-export");
}
#[test]
fn with_params_sets_params() {
let meta = OxiHumanMeta::minimal().with_params(0.5, 0.4, 0.3, 0.2);
assert!(meta.params.is_some());
}
#[test]
fn timestamp_is_nonempty() {
assert!(!OxiHumanMeta::minimal().exported_at.is_empty());
}
#[test]
fn timestamp_contains_t() {
assert!(OxiHumanMeta::minimal().exported_at.contains('T'));
}
}