use std::io;
use crate::entry::SessionEntry;
use crate::meta::SessionMeta;
pub trait SessionMigrator: Send + Sync {
fn source_version(&self) -> u32;
fn target_version(&self) -> u32;
fn migrate(
&self,
meta: &SessionMeta,
entries: Vec<SessionEntry>,
) -> io::Result<Vec<SessionEntry>>;
}
pub const CURRENT_VERSION: u32 = 1;
pub fn run_migrations(
meta: &mut SessionMeta,
entries: &mut Vec<SessionEntry>,
migrators: &[Box<dyn SessionMigrator>],
) -> io::Result<()> {
if meta.version > CURRENT_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"unsupported session version {} (current: {CURRENT_VERSION})",
meta.version
),
));
}
while meta.version < CURRENT_VERSION {
let migrator = migrators
.iter()
.find(|m| m.source_version() == meta.version)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"no migrator found for version {} -> {}",
meta.version,
meta.version + 1
),
)
})?;
*entries = migrator.migrate(meta, std::mem::take(entries))?;
meta.version = migrator.target_version();
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
struct UpperCaseMigrator;
impl SessionMigrator for UpperCaseMigrator {
fn source_version(&self) -> u32 {
1
}
fn target_version(&self) -> u32 {
2
}
fn migrate(
&self,
_meta: &SessionMeta,
entries: Vec<SessionEntry>,
) -> io::Result<Vec<SessionEntry>> {
Ok(entries
.into_iter()
.map(|entry| match entry {
SessionEntry::Message(msg) => {
SessionEntry::Message(msg)
}
other => other,
})
.collect())
}
}
#[test]
fn migrator_upgrades_session() {
use chrono::Utc;
use swink_agent::{ContentBlock, LlmMessage, UserMessage};
let mut meta = SessionMeta {
id: "test".to_string(),
title: "Test".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
version: 1,
sequence: 0,
};
let entries = vec![SessionEntry::Message(LlmMessage::User(
UserMessage::new(vec![ContentBlock::Text {
text: "hello".to_string(),
}])
.with_timestamp(0),
))];
let migrator = UpperCaseMigrator;
assert_eq!(migrator.source_version(), 1);
assert_eq!(migrator.target_version(), 2);
let result = migrator.migrate(&meta, entries).unwrap();
assert_eq!(result.len(), 1);
assert!(matches!(result[0], SessionEntry::Message(_)));
meta.version = migrator.target_version();
assert_eq!(meta.version, 2);
}
#[test]
fn unsupported_future_version_returns_error() {
let mut meta = SessionMeta {
id: "future".to_string(),
title: "Future".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
version: 999,
sequence: 0,
};
let mut entries = vec![];
let err = run_migrations(&mut meta, &mut entries, &[]).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(err.to_string().contains("unsupported session version 999"));
}
use chrono::Utc;
}