linreg_core/serialization/
json.rs1use crate::error::Error;
7use crate::serialization::types::SerializedModel;
8use std::fs;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11pub fn iso_timestamp() -> String {
27 let now = SystemTime::now()
28 .duration_since(UNIX_EPOCH)
29 .unwrap_or_default();
30
31 let secs = now.as_secs();
32
33 let days_since_epoch = secs / 86400;
36 let secs_in_day = secs % 86400;
37
38 let days = days_since_epoch + 719468;
40
41 let era = days / 146097;
43 let day_of_era = days % 146097;
44 let year_of_era = (day_of_era - day_of_era / 1460 + day_of_era / 36524 - day_of_era / 146096) / 365;
45 let year = era * 400 + year_of_era;
46 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
47 let mp = (5 * day_of_year + 2) / 153;
48 let month = if mp < 10 { mp + 3 } else { mp - 9 };
49 let day = day_of_year - (153 * mp + 2) / 5 + 1;
50
51 let adjusted_year = if month <= 2 { year - 1 } else { year };
53
54 let hours = secs_in_day / 3600;
56 let mins = (secs_in_day % 3600) / 60;
57 let secs = secs_in_day % 60;
58
59 format!(
60 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
61 adjusted_year, month, day, hours, mins, secs
62 )
63}
64
65pub fn validate_format_version(file_version: &str) -> Result<(), Error> {
80 let current_major = super::FORMAT_VERSION
81 .split('.')
82 .next()
83 .and_then(|s| s.parse::<u32>().ok())
84 .unwrap_or(1);
85
86 let file_major = file_version
87 .split('.')
88 .next()
89 .and_then(|s| s.parse::<u32>().ok())
90 .unwrap_or(0);
91
92 if current_major != file_major {
93 return Err(Error::IncompatibleFormatVersion {
94 file_version: file_version.to_string(),
95 supported: super::FORMAT_VERSION.to_string(),
96 });
97 }
98
99 Ok(())
100}
101
102pub fn save_to_file(model: &SerializedModel, path: &str) -> Result<(), Error> {
115 validate_format_version(&model.metadata.format_version)?;
117
118 let json = serde_json::to_string_pretty(model).map_err(|e| {
120 Error::SerializationError(format!("Failed to serialize model: {}", e))
121 })?;
122
123 fs::write(path, json).map_err(|e| {
125 Error::IoError(format!("Failed to write to file '{}': {}", path, e))
126 })?;
127
128 Ok(())
129}
130
131pub fn load_from_file(path: &str) -> Result<SerializedModel, Error> {
144 let content = fs::read_to_string(path).map_err(|e| {
146 Error::IoError(format!("Failed to read file '{}': {}", path, e))
147 })?;
148
149 let model: SerializedModel = serde_json::from_str(&content).map_err(|e| {
151 Error::DeserializationError(format!("Failed to parse JSON from '{}': {}", path, e))
152 })?;
153
154 validate_format_version(&model.metadata.format_version)?;
156
157 Ok(model)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::serialization::{ModelMetadata, ModelType};
164 use serde_json::json;
165
166 #[test]
167 fn test_iso_timestamp_format() {
168 let ts = iso_timestamp();
169 assert!(ts.len() == 20);
171 assert!(ts.contains('T'));
172 assert!(ts.ends_with('Z'));
173 let parts: Vec<&str> = ts.split(&['T', '-', ':'][..]).collect();
175 assert_eq!(parts.len(), 6);
176 }
177
178 #[test]
179 fn test_validate_format_version_compatible() {
180 assert!(validate_format_version("1.0").is_ok());
182
183 assert!(validate_format_version("1.5").is_ok());
185 assert!(validate_format_version("1.99").is_ok());
186 }
187
188 #[test]
189 fn test_validate_format_version_incompatible() {
190 let result = validate_format_version("2.0");
192 assert!(matches!(result, Err(Error::IncompatibleFormatVersion { .. })));
193
194 let result = validate_format_version("0.9");
195 assert!(matches!(result, Err(Error::IncompatibleFormatVersion { .. })));
196 }
197
198 #[test]
199 fn test_validate_format_version_invalid() {
200 assert!(validate_format_version("invalid").is_err());
203 assert!(validate_format_version("").is_err());
204 }
205
206 #[test]
207 fn test_serialize_deserialize_roundtrip() {
208 let metadata = ModelMetadata {
210 format_version: "1.0".to_string(),
211 library_version: "0.6.0".to_string(),
212 model_type: ModelType::OLS,
213 created_at: "2026-02-10T15:30:00Z".to_string(),
214 name: Some("Test Model".to_string()),
215 };
216
217 let data = json!({
219 "coefficients": [1.0, 2.0, 3.0],
220 "r_squared": 0.95,
221 "n_observations": 100
222 });
223
224 let model = SerializedModel::new(metadata.clone(), data);
225
226 let json_str = serde_json::to_string(&model).unwrap();
228 let parsed: SerializedModel = serde_json::from_str(&json_str).unwrap();
229
230 assert_eq!(parsed.metadata.format_version, metadata.format_version);
232 assert_eq!(parsed.metadata.library_version, metadata.library_version);
233 assert_eq!(parsed.metadata.model_type, ModelType::OLS);
234 assert_eq!(parsed.metadata.created_at, metadata.created_at);
235 assert_eq!(parsed.metadata.name, metadata.name);
236
237 assert_eq!(parsed.data["coefficients"][0], 1.0);
239 assert_eq!(parsed.data["r_squared"], 0.95);
240 assert_eq!(parsed.data["n_observations"], 100);
241 }
242
243 #[test]
244 fn test_serialized_model_json_structure() {
245 let metadata = ModelMetadata::new(ModelType::Ridge, "0.6.0".to_string());
246 let data = json!({"test": "value"});
247
248 let model = SerializedModel::new(metadata, data);
249 let json = serde_json::to_string_pretty(&model).unwrap();
250
251 assert!(json.contains("\"metadata\""));
253 assert!(json.contains("\"data\""));
254 assert!(json.contains("\"format_version\""));
255 assert!(json.contains("\"model_type\""));
256 assert!(json.contains("\"Ridge\""));
257 }
258
259 #[test]
260 fn test_model_type() {
261 let model = SerializedModel {
262 metadata: ModelMetadata {
263 format_version: "1.0".to_string(),
264 library_version: "0.6.0".to_string(),
265 model_type: ModelType::Lasso,
266 created_at: "2026-02-10T00:00:00Z".to_string(),
267 name: None,
268 },
269 data: json!({}),
270 };
271
272 assert_eq!(model.model_type(), &ModelType::Lasso);
273 }
274}