use serde::{Deserialize, Serialize};
use super::paths::EnvelopePaths;
use crate::crypto::key_derivation::KeyDerivationParams;
use crate::error::EnvelopeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum BudgetPeriodType {
#[default]
Monthly,
Weekly,
BiWeekly,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupRetention {
pub daily_count: u32,
pub monthly_count: u32,
}
impl Default for BackupRetention {
fn default() -> Self {
Self {
daily_count: 30,
monthly_count: 12,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EncryptionSettings {
#[serde(default)]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_params: Option<KeyDerivationParams>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification_hash: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub budget_period_type: BudgetPeriodType,
#[serde(default)]
pub encryption_enabled: bool,
#[serde(default)]
pub encryption: EncryptionSettings,
#[serde(default)]
pub backup_retention: BackupRetention,
#[serde(default = "default_currency")]
pub currency_symbol: String,
#[serde(default = "default_date_format")]
pub date_format: String,
#[serde(default = "default_first_day_of_week")]
pub first_day_of_week: u8,
#[serde(default)]
pub setup_completed: bool,
}
fn default_schema_version() -> u32 {
1
}
fn default_currency() -> String {
"$".to_string()
}
fn default_date_format() -> String {
"%Y-%m-%d".to_string()
}
fn default_first_day_of_week() -> u8 {
0 }
impl Default for Settings {
fn default() -> Self {
Self {
schema_version: default_schema_version(),
budget_period_type: BudgetPeriodType::default(),
encryption_enabled: false,
encryption: EncryptionSettings::default(),
backup_retention: BackupRetention::default(),
currency_symbol: default_currency(),
date_format: default_date_format(),
first_day_of_week: default_first_day_of_week(),
setup_completed: false,
}
}
}
impl Settings {
pub fn is_encryption_enabled(&self) -> bool {
self.encryption.enabled || self.encryption_enabled
}
pub fn load_or_create(paths: &EnvelopePaths) -> Result<Self, EnvelopeError> {
let settings_path = paths.settings_file();
if settings_path.exists() {
let contents = std::fs::read_to_string(&settings_path)
.map_err(|e| EnvelopeError::Io(format!("Failed to read settings file: {}", e)))?;
let settings: Settings = serde_json::from_str(&contents).map_err(|e| {
EnvelopeError::Config(format!("Failed to parse settings file: {}", e))
})?;
Ok(settings)
} else {
let settings = Settings::default();
Ok(settings)
}
}
pub fn save(&self, paths: &EnvelopePaths) -> Result<(), EnvelopeError> {
paths.ensure_directories()?;
let settings_path = paths.settings_file();
let contents = serde_json::to_string_pretty(self)
.map_err(|e| EnvelopeError::Config(format!("Failed to serialize settings: {}", e)))?;
std::fs::write(&settings_path, contents)
.map_err(|e| EnvelopeError::Io(format!("Failed to write settings file: {}", e)))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_default_settings() {
let settings = Settings::default();
assert_eq!(settings.budget_period_type, BudgetPeriodType::Monthly);
assert!(!settings.encryption_enabled);
assert_eq!(settings.backup_retention.daily_count, 30);
assert_eq!(settings.backup_retention.monthly_count, 12);
}
#[test]
fn test_save_and_load() {
let temp_dir = TempDir::new().unwrap();
let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
let settings = Settings {
budget_period_type: BudgetPeriodType::Weekly,
encryption_enabled: true,
..Default::default()
};
settings.save(&paths).unwrap();
let loaded = Settings::load_or_create(&paths).unwrap();
assert_eq!(loaded.budget_period_type, BudgetPeriodType::Weekly);
assert!(loaded.encryption_enabled);
}
#[test]
fn test_serde_round_trip() {
let settings = Settings::default();
let json = serde_json::to_string(&settings).unwrap();
let deserialized: Settings = serde_json::from_str(&json).unwrap();
assert_eq!(settings.budget_period_type, deserialized.budget_period_type);
}
}