1use crate::{ConfigMigrator, MigrationError, Migrator, Queryable};
8use local_store::{FileStorageStrategy, FormatStrategy, LoadBehavior};
9use serde_json::Value as JsonValue;
10use std::path::{Path, PathBuf};
11
12pub struct VersionedFileStorage {
24 inner: local_store::FileStorage,
26 config: ConfigMigrator,
28 strategy: FileStorageStrategy,
30}
31
32impl VersionedFileStorage {
33 pub fn new(
53 path: PathBuf,
54 migrator: Migrator,
55 strategy: FileStorageStrategy,
56 ) -> Result<Self, MigrationError> {
57 let file_was_missing = !path.exists();
59
60 let inner_strategy = FileStorageStrategy {
63 load_behavior: LoadBehavior::CreateIfMissing,
64 ..strategy.clone()
65 };
66 let inner = local_store::FileStorage::new(path.clone(), inner_strategy)
67 .map_err(MigrationError::Store)?;
68
69 let json_string = if !file_was_missing {
71 let raw = inner.read_string().map_err(MigrationError::Store)?;
73 if raw.trim().is_empty() {
74 "{}".to_string()
75 } else {
76 match strategy.format {
77 FormatStrategy::Toml => {
78 let tv: toml::Value = toml::from_str(&raw)
79 .map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
80 let jv = toml_to_json(tv)?;
81 serde_json::to_string(&jv)
82 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
83 }
84 FormatStrategy::Json => raw,
85 }
86 }
87 } else {
88 match strategy.load_behavior {
90 LoadBehavior::ErrorIfMissing => {
91 return Err(MigrationError::Store(local_store::StoreError::IoError {
92 operation: local_store::IoOperationKind::Read,
93 path: path.display().to_string(),
94 context: None,
95 error: "File not found".to_string(),
96 }));
97 }
98 LoadBehavior::CreateIfMissing | LoadBehavior::SaveIfMissing => {
99 if let Some(ref default_value) = strategy.default_value {
100 serde_json::to_string(default_value)
101 .map_err(|e| MigrationError::SerializationError(e.to_string()))?
102 } else {
103 "{}".to_string()
104 }
105 }
106 }
107 };
108
109 let config = ConfigMigrator::from(&json_string, migrator)?;
110 let storage = Self {
111 inner,
112 config,
113 strategy,
114 };
115
116 if file_was_missing && storage.strategy.load_behavior == LoadBehavior::SaveIfMissing {
118 storage.save()?;
119 }
120
121 Ok(storage)
122 }
123
124 pub fn save(&self) -> Result<(), MigrationError> {
136 let json_value = self.config.as_value();
137
138 let content = match self.strategy.format {
139 FormatStrategy::Toml => {
140 let tv = local_store::format_convert::json_to_toml(json_value).map_err(|e| {
141 MigrationError::Store(local_store::StoreError::FormatConvert(e))
142 })?;
143 toml::to_string_pretty(&tv)
144 .map_err(|e| MigrationError::TomlSerializeError(e.to_string()))?
145 }
146 FormatStrategy::Json => serde_json::to_string_pretty(json_value)
147 .map_err(|e| MigrationError::SerializationError(e.to_string()))?,
148 };
149
150 self.inner
151 .write_string(&content)
152 .map_err(MigrationError::Store)
153 }
154
155 pub fn config(&self) -> &ConfigMigrator {
157 &self.config
158 }
159
160 pub fn config_mut(&mut self) -> &mut ConfigMigrator {
162 &mut self.config
163 }
164
165 pub fn query<T>(&self, key: &str) -> Result<Vec<T>, MigrationError>
173 where
174 T: Queryable + for<'de> serde::Deserialize<'de>,
175 {
176 self.config.query(key)
177 }
178
179 pub fn update<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
187 where
188 T: Queryable + serde::Serialize,
189 {
190 self.config.update(key, value)
191 }
192
193 pub fn update_and_save<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
201 where
202 T: Queryable + serde::Serialize,
203 {
204 self.update(key, value)?;
205 self.save()
206 }
207
208 pub fn path(&self) -> &Path {
210 self.inner.path()
211 }
212}
213
214fn toml_to_json(toml_value: toml::Value) -> Result<JsonValue, MigrationError> {
220 let json_str = serde_json::to_string(&toml_value)
221 .map_err(|e| MigrationError::SerializationError(e.to_string()))?;
222 let json_value: JsonValue = serde_json::from_str(&json_str)
223 .map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
224 Ok(json_value)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::{IntoDomain, MigratesTo, Versioned};
231 use serde::{Deserialize, Serialize};
232 use tempfile::TempDir;
233
234 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
235 struct TestEntity {
236 name: String,
237 count: u32,
238 }
239
240 impl Queryable for TestEntity {
241 const ENTITY_NAME: &'static str = "test";
242 }
243
244 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245 struct TestV1 {
246 name: String,
247 }
248
249 impl Versioned for TestV1 {
250 const VERSION: &'static str = "1.0.0";
251 }
252
253 impl MigratesTo<TestV2> for TestV1 {
254 fn migrate(self) -> TestV2 {
255 TestV2 {
256 name: self.name,
257 count: 0,
258 }
259 }
260 }
261
262 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263 struct TestV2 {
264 name: String,
265 count: u32,
266 }
267
268 impl Versioned for TestV2 {
269 const VERSION: &'static str = "2.0.0";
270 }
271
272 impl IntoDomain<TestEntity> for TestV2 {
273 fn into_domain(self) -> TestEntity {
274 TestEntity {
275 name: self.name,
276 count: self.count,
277 }
278 }
279 }
280
281 fn setup_migrator() -> Migrator {
282 let path = Migrator::define("test")
283 .from::<TestV1>()
284 .step::<TestV2>()
285 .into::<TestEntity>();
286
287 let mut migrator = Migrator::new();
288 migrator.register(path).unwrap();
291 migrator
292 }
293
294 #[test]
295 fn test_versioned_file_storage_new_create_if_missing() {
296 let temp_dir = TempDir::new().unwrap();
297 let file_path = temp_dir.path().join("nonexistent.toml");
298 let migrator = setup_migrator();
299 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
300
301 let result = VersionedFileStorage::new(file_path, migrator, strategy);
302 assert!(result.is_ok());
303 }
304
305 #[test]
306 fn test_versioned_file_storage_new_error_if_missing() {
307 let temp_dir = TempDir::new().unwrap();
308 let file_path = temp_dir.path().join("nonexistent.toml");
309 let migrator = setup_migrator();
310 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::ErrorIfMissing);
311
312 let result = VersionedFileStorage::new(file_path, migrator, strategy);
313 assert!(result.is_err());
314 assert!(matches!(
315 result,
316 Err(MigrationError::Store(
317 local_store::StoreError::IoError { .. }
318 ))
319 ));
320 }
321
322 #[test]
323 fn test_versioned_file_storage_save_and_reload() {
324 let temp_dir = TempDir::new().unwrap();
325 let file_path = temp_dir.path().join("data.toml");
326 let migrator = setup_migrator();
327 let strategy = FileStorageStrategy::default();
328
329 let mut storage = VersionedFileStorage::new(file_path.clone(), migrator, strategy).unwrap();
330
331 let entities = vec![TestEntity {
332 name: "hello".to_string(),
333 count: 7,
334 }];
335 storage.update_and_save("test", entities).unwrap();
336
337 let migrator2 = setup_migrator();
338 let storage2 =
339 VersionedFileStorage::new(file_path, migrator2, FileStorageStrategy::default())
340 .unwrap();
341
342 let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
343 assert_eq!(loaded.len(), 1);
344 assert_eq!(loaded[0].name, "hello");
345 assert_eq!(loaded[0].count, 7);
346 }
347
348 #[test]
349 fn test_versioned_file_storage_save_if_missing() {
350 let temp_dir = TempDir::new().unwrap();
351 let file_path = temp_dir.path().join("save_if_missing.toml");
352 let migrator = setup_migrator();
353 let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::SaveIfMissing);
354
355 assert!(!file_path.exists());
356
357 let result = VersionedFileStorage::new(file_path.clone(), migrator, strategy);
358 assert!(result.is_ok());
359 assert!(file_path.exists());
360 }
361
362 #[test]
363 fn test_versioned_file_storage_path() {
364 let temp_dir = TempDir::new().unwrap();
365 let file_path = temp_dir.path().join("config.toml");
366 let migrator = setup_migrator();
367
368 let storage =
369 VersionedFileStorage::new(file_path.clone(), migrator, FileStorageStrategy::default())
370 .unwrap();
371
372 assert_eq!(storage.path(), file_path.as_path());
373 }
374
375 #[test]
376 fn test_versioned_file_storage_config_accessors() {
377 let temp_dir = TempDir::new().unwrap();
378 let file_path = temp_dir.path().join("config.toml");
379 let migrator = setup_migrator();
380
381 let mut storage =
382 VersionedFileStorage::new(file_path, migrator, FileStorageStrategy::default()).unwrap();
383
384 let _config = storage.config();
385 let _config_mut = storage.config_mut();
386 }
387}