#![allow(dead_code)]
use chrono::{DateTime, Utc};
use nono::{NonoError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
const VERSION_STATE_FILE: &str = "versions.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionState {
pub version: u64,
pub last_seen: DateTime<Utc>,
pub source: String,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct VersionTracker {
#[serde(flatten)]
pub configs: HashMap<String, VersionState>,
}
impl VersionTracker {
pub fn load() -> Result<Self> {
let path = Self::state_file_path()?;
if !path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&path).map_err(|e| NonoError::ConfigRead {
path: path.clone(),
source: e,
})?;
serde_json::from_str(&content)
.map_err(|e| NonoError::ConfigParse(format!("Failed to parse version state: {}", e)))
}
pub fn save(&self) -> Result<()> {
let path = Self::state_file_path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| NonoError::ConfigWrite {
path: parent.to_path_buf(),
source: e,
})?;
}
let content = serde_json::to_string_pretty(self).map_err(|e| {
NonoError::ConfigParse(format!("Failed to serialize version state: {}", e))
})?;
fs::write(&path, content).map_err(|e| NonoError::ConfigWrite { path, source: e })?;
Ok(())
}
pub fn check_version(&self, name: &str, version: u64) -> Result<()> {
if let Some(state) = self.configs.get(name) {
if version < state.version {
return Err(NonoError::VersionDowngrade {
config: name.to_string(),
current: state.version,
attempted: version,
});
}
}
Ok(())
}
pub fn update_version(&mut self, name: &str, version: u64, source: &str) {
let now = Utc::now();
self.configs.insert(
name.to_string(),
VersionState {
version,
last_seen: now,
source: source.to_string(),
},
);
}
fn state_file_path() -> Result<PathBuf> {
let state_dir = super::user_state_dir().ok_or_else(|| {
NonoError::ConfigParse("Could not determine user state directory".to_string())
})?;
Ok(state_dir.join(VERSION_STATE_FILE))
}
}
pub fn check_and_update_version(name: &str, version: u64, source: &str) -> Result<()> {
let mut tracker = VersionTracker::load()?;
tracker.check_version(name, version)?;
tracker.update_version(name, version, source);
tracker.save()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_check_new() {
let tracker = VersionTracker::default();
assert!(tracker.check_version("test", 1).is_ok());
assert!(tracker.check_version("test", 100).is_ok());
}
#[test]
fn test_version_check_upgrade() {
let mut tracker = VersionTracker::default();
tracker.update_version("test", 5, "embedded");
assert!(tracker.check_version("test", 5).is_ok());
assert!(tracker.check_version("test", 6).is_ok());
assert!(tracker.check_version("test", 100).is_ok());
}
#[test]
fn test_version_check_downgrade() {
let mut tracker = VersionTracker::default();
tracker.update_version("test", 5, "embedded");
let result = tracker.check_version("test", 4);
assert!(result.is_err());
if let Err(NonoError::VersionDowngrade {
config,
current,
attempted,
}) = result
{
assert_eq!(config, "test");
assert_eq!(current, 5);
assert_eq!(attempted, 4);
} else {
panic!("Expected VersionDowngrade error");
}
}
#[test]
fn test_update_version() {
let mut tracker = VersionTracker::default();
tracker.update_version("test", 1, "embedded");
assert_eq!(
tracker
.configs
.get("test")
.expect("config not found")
.version,
1
);
tracker.update_version("test", 5, "system");
assert_eq!(
tracker
.configs
.get("test")
.expect("config not found")
.version,
5
);
assert_eq!(
tracker
.configs
.get("test")
.expect("config not found")
.source,
"system"
);
}
}