use crate::{ConfigMigrator, MigrationError, Migrator, Queryable};
use local_store::{FileStorageStrategy, FormatStrategy, LoadBehavior};
use serde_json::Value as JsonValue;
use std::path::{Path, PathBuf};
pub struct VersionedFileStorage {
inner: local_store::FileStorage,
config: ConfigMigrator,
strategy: FileStorageStrategy,
}
impl VersionedFileStorage {
pub fn new(
path: PathBuf,
migrator: Migrator,
strategy: FileStorageStrategy,
) -> Result<Self, MigrationError> {
let file_was_missing = !path.exists();
let inner_strategy = FileStorageStrategy {
load_behavior: LoadBehavior::CreateIfMissing,
..strategy.clone()
};
let inner = local_store::FileStorage::new(path.clone(), inner_strategy)
.map_err(MigrationError::Store)?;
let json_string = if !file_was_missing {
let raw = inner.read_string().map_err(MigrationError::Store)?;
if raw.trim().is_empty() {
"{}".to_string()
} else {
match strategy.format {
FormatStrategy::Toml => {
let tv: toml::Value = toml::from_str(&raw)
.map_err(|e| MigrationError::TomlParseError(e.to_string()))?;
let jv = toml_to_json(tv)?;
serde_json::to_string(&jv)
.map_err(|e| MigrationError::SerializationError(e.to_string()))?
}
FormatStrategy::Json => raw,
}
}
} else {
match strategy.load_behavior {
LoadBehavior::ErrorIfMissing => {
return Err(MigrationError::Store(local_store::StoreError::IoError {
operation: local_store::IoOperationKind::Read,
path: path.display().to_string(),
context: None,
error: "File not found".to_string(),
}));
}
LoadBehavior::CreateIfMissing | LoadBehavior::SaveIfMissing => {
if let Some(ref default_value) = strategy.default_value {
serde_json::to_string(default_value)
.map_err(|e| MigrationError::SerializationError(e.to_string()))?
} else {
"{}".to_string()
}
}
}
};
let config = ConfigMigrator::from(&json_string, migrator)?;
let storage = Self {
inner,
config,
strategy,
};
if file_was_missing && storage.strategy.load_behavior == LoadBehavior::SaveIfMissing {
storage.save()?;
}
Ok(storage)
}
pub fn save(&self) -> Result<(), MigrationError> {
let json_value = self.config.as_value();
let content = match self.strategy.format {
FormatStrategy::Toml => {
let tv = local_store::format_convert::json_to_toml(json_value).map_err(|e| {
MigrationError::Store(local_store::StoreError::FormatConvert(e))
})?;
toml::to_string_pretty(&tv)
.map_err(|e| MigrationError::TomlSerializeError(e.to_string()))?
}
FormatStrategy::Json => serde_json::to_string_pretty(json_value)
.map_err(|e| MigrationError::SerializationError(e.to_string()))?,
};
self.inner
.write_string(&content)
.map_err(MigrationError::Store)
}
pub fn config(&self) -> &ConfigMigrator {
&self.config
}
pub fn config_mut(&mut self) -> &mut ConfigMigrator {
&mut self.config
}
pub fn query<T>(&self, key: &str) -> Result<Vec<T>, MigrationError>
where
T: Queryable + for<'de> serde::Deserialize<'de>,
{
self.config.query(key)
}
pub fn update<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
where
T: Queryable + serde::Serialize,
{
self.config.update(key, value)
}
pub fn update_and_save<T>(&mut self, key: &str, value: Vec<T>) -> Result<(), MigrationError>
where
T: Queryable + serde::Serialize,
{
self.update(key, value)?;
self.save()
}
pub fn path(&self) -> &Path {
self.inner.path()
}
}
fn toml_to_json(toml_value: toml::Value) -> Result<JsonValue, MigrationError> {
let json_str = serde_json::to_string(&toml_value)
.map_err(|e| MigrationError::SerializationError(e.to_string()))?;
let json_value: JsonValue = serde_json::from_str(&json_str)
.map_err(|e| MigrationError::DeserializationError(e.to_string()))?;
Ok(json_value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{IntoDomain, MigratesTo, Versioned};
use serde::{Deserialize, Serialize};
use tempfile::TempDir;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct TestEntity {
name: String,
count: u32,
}
impl Queryable for TestEntity {
const ENTITY_NAME: &'static str = "test";
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct TestV1 {
name: String,
}
impl Versioned for TestV1 {
const VERSION: &'static str = "1.0.0";
}
impl MigratesTo<TestV2> for TestV1 {
fn migrate(self) -> TestV2 {
TestV2 {
name: self.name,
count: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct TestV2 {
name: String,
count: u32,
}
impl Versioned for TestV2 {
const VERSION: &'static str = "2.0.0";
}
impl IntoDomain<TestEntity> for TestV2 {
fn into_domain(self) -> TestEntity {
TestEntity {
name: self.name,
count: self.count,
}
}
}
fn setup_migrator() -> Migrator {
let path = Migrator::define("test")
.from::<TestV1>()
.step::<TestV2>()
.into::<TestEntity>();
let mut migrator = Migrator::new();
migrator.register(path).unwrap();
migrator
}
#[test]
fn test_versioned_file_storage_new_create_if_missing() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("nonexistent.toml");
let migrator = setup_migrator();
let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::CreateIfMissing);
let result = VersionedFileStorage::new(file_path, migrator, strategy);
assert!(result.is_ok());
}
#[test]
fn test_versioned_file_storage_new_error_if_missing() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("nonexistent.toml");
let migrator = setup_migrator();
let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::ErrorIfMissing);
let result = VersionedFileStorage::new(file_path, migrator, strategy);
assert!(result.is_err());
assert!(matches!(
result,
Err(MigrationError::Store(
local_store::StoreError::IoError { .. }
))
));
}
#[test]
fn test_versioned_file_storage_save_and_reload() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("data.toml");
let migrator = setup_migrator();
let strategy = FileStorageStrategy::default();
let mut storage = VersionedFileStorage::new(file_path.clone(), migrator, strategy).unwrap();
let entities = vec![TestEntity {
name: "hello".to_string(),
count: 7,
}];
storage.update_and_save("test", entities).unwrap();
let migrator2 = setup_migrator();
let storage2 =
VersionedFileStorage::new(file_path, migrator2, FileStorageStrategy::default())
.unwrap();
let loaded: Vec<TestEntity> = storage2.query("test").unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].name, "hello");
assert_eq!(loaded[0].count, 7);
}
#[test]
fn test_versioned_file_storage_save_if_missing() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("save_if_missing.toml");
let migrator = setup_migrator();
let strategy = FileStorageStrategy::new().with_load_behavior(LoadBehavior::SaveIfMissing);
assert!(!file_path.exists());
let result = VersionedFileStorage::new(file_path.clone(), migrator, strategy);
assert!(result.is_ok());
assert!(file_path.exists());
}
#[test]
fn test_versioned_file_storage_path() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("config.toml");
let migrator = setup_migrator();
let storage =
VersionedFileStorage::new(file_path.clone(), migrator, FileStorageStrategy::default())
.unwrap();
assert_eq!(storage.path(), file_path.as_path());
}
#[test]
fn test_versioned_file_storage_config_accessors() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("config.toml");
let migrator = setup_migrator();
let mut storage =
VersionedFileStorage::new(file_path, migrator, FileStorageStrategy::default()).unwrap();
let _config = storage.config();
let _config_mut = storage.config_mut();
}
}