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