1use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct OxiHumanMeta {
10 pub generator: String,
12 pub version: String,
14 pub exported_at: String,
16 pub measurements: Option<MeasurementsMeta>,
18 pub preset: Option<String>,
20 pub params: Option<ParamsMeta>,
22 pub expression: Option<String>,
24 pub target_count: Option<usize>,
26 pub policy: Option<String>,
28 pub extra: std::collections::HashMap<String, Value>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MeasurementsMeta {
35 pub height_cm: Option<f32>,
37 pub weight_kg: Option<f32>,
39 pub chest_cm: Option<f32>,
41 pub waist_cm: Option<f32>,
43 pub hips_cm: Option<f32>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ParamsMeta {
50 pub height: f32,
52 pub weight: f32,
54 pub muscle: f32,
56 pub age: f32,
58}
59
60impl OxiHumanMeta {
61 pub fn minimal() -> Self {
63 Self {
64 generator: "oxihuman-export".into(),
65 version: env!("CARGO_PKG_VERSION").into(),
66 exported_at: current_timestamp(),
67 measurements: None,
68 preset: None,
69 params: None,
70 expression: None,
71 target_count: None,
72 policy: None,
73 extra: Default::default(),
74 }
75 }
76
77 pub fn to_json(&self) -> Value {
79 serde_json::to_value(self).unwrap_or(Value::Null)
80 }
81
82 pub fn from_json(v: &Value) -> Option<Self> {
84 serde_json::from_value(v.clone()).ok()
85 }
86
87 pub fn with_params(mut self, height: f32, weight: f32, muscle: f32, age: f32) -> Self {
89 self.params = Some(ParamsMeta {
90 height,
91 weight,
92 muscle,
93 age,
94 });
95 self
96 }
97
98 pub fn with_measurements(mut self, m: MeasurementsMeta) -> Self {
100 self.measurements = Some(m);
101 self
102 }
103}
104
105fn current_timestamp() -> String {
108 use std::time::{SystemTime, UNIX_EPOCH};
109 let secs = SystemTime::now()
110 .duration_since(UNIX_EPOCH)
111 .map(|d| d.as_secs())
112 .unwrap_or(0);
113 let (y, mo, d, h, mi, sec) = unix_secs_to_datetime(secs);
114 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, mo, d, h, mi, sec)
115}
116
117fn unix_secs_to_datetime(secs: u64) -> (u32, u32, u32, u32, u32, u32) {
118 let sec = (secs % 60) as u32;
119 let min = ((secs / 60) % 60) as u32;
120 let hour = ((secs / 3600) % 24) as u32;
121 let days = secs / 86400;
122
123 let mut y = 1970u32;
125 let mut d = days;
126 loop {
127 let dy = if is_leap(y) { 366u64 } else { 365u64 };
128 if d < dy {
129 break;
130 }
131 d -= dy;
132 y += 1;
133 }
134
135 let months = [
136 31u32,
137 if is_leap(y) { 29 } else { 28 },
138 31,
139 30,
140 31,
141 30,
142 31,
143 31,
144 30,
145 31,
146 30,
147 31,
148 ];
149 let mut mo = 1u32;
150 for &ml in &months {
151 if d < ml as u64 {
152 break;
153 }
154 d -= ml as u64;
155 mo += 1;
156 }
157
158 (y, mo, d as u32 + 1, hour, min, sec)
159}
160
161fn is_leap(y: u32) -> bool {
162 (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn minimal_has_generator() {
171 assert_eq!(OxiHumanMeta::minimal().generator, "oxihuman-export");
172 }
173
174 #[test]
175 fn to_json_has_generator_key() {
176 let meta = OxiHumanMeta::minimal();
177 assert!(meta.to_json()["generator"].as_str().is_some());
178 }
179
180 #[test]
181 fn from_json_roundtrip() {
182 let meta = OxiHumanMeta::minimal();
183 let json = meta.to_json();
184 let back = OxiHumanMeta::from_json(&json).expect("deserialization failed");
185 assert_eq!(back.generator, "oxihuman-export");
186 }
187
188 #[test]
189 fn with_params_sets_params() {
190 let meta = OxiHumanMeta::minimal().with_params(0.5, 0.4, 0.3, 0.2);
191 assert!(meta.params.is_some());
192 }
193
194 #[test]
195 fn timestamp_is_nonempty() {
196 assert!(!OxiHumanMeta::minimal().exported_at.is_empty());
197 }
198
199 #[test]
200 fn timestamp_contains_t() {
201 assert!(OxiHumanMeta::minimal().exported_at.contains('T'));
202 }
203}