use anyhow::Result;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SchemaVersion(pub u32);
impl SchemaVersion {
pub const V0: SchemaVersion = SchemaVersion(0);
pub const V1: SchemaVersion = SchemaVersion(1);
pub const CURRENT: SchemaVersion = SchemaVersion::V1;
pub const fn as_u32(self) -> u32 {
self.0
}
}
impl Default for SchemaVersion {
fn default() -> Self {
Self::CURRENT
}
}
pub trait VersionedState: Sized + Serialize + DeserializeOwned {
fn schema_version(&self) -> SchemaVersion;
fn next_version(current: SchemaVersion) -> Option<SchemaVersion> {
match current {
SchemaVersion::V0 => Some(SchemaVersion::V1),
SchemaVersion::V1 => None,
_ => None,
}
}
fn migrate(self, target: SchemaVersion) -> Result<Self> {
let mut current = self.schema_version();
if current >= target {
return Ok(self);
}
let mut value = self;
loop {
let next = Self::next_version(current);
match next {
Some(version) if version <= target => {
value = value.migrate_one_step(current, version)?;
current = version;
}
_ => break,
}
}
Ok(value)
}
fn migrate_one_step(self, from: SchemaVersion, to: SchemaVersion) -> Result<Self>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_version_constants() {
assert_eq!(SchemaVersion::V0.as_u32(), 0);
assert_eq!(SchemaVersion::V1.as_u32(), 1);
assert_eq!(SchemaVersion::CURRENT.as_u32(), 1);
}
#[test]
fn next_version_chain() {
assert_eq!(TestState::next_version(SchemaVersion::V0), Some(SchemaVersion::V1));
assert_eq!(TestState::next_version(SchemaVersion::V1), None);
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct TestState {
version: SchemaVersion,
value: u32,
}
impl VersionedState for TestState {
fn schema_version(&self) -> SchemaVersion {
self.version
}
fn migrate_one_step(self, from: SchemaVersion, to: SchemaVersion) -> Result<Self> {
match (from, to) {
(SchemaVersion::V0, SchemaVersion::V1) => {
Ok(Self { version: SchemaVersion::V1, ..self })
}
_ => anyhow::bail!("unsupported migration: {from:?} -> {to:?}"),
}
}
}
#[test]
fn test_migrate_v0_to_v1() {
let state = TestState { version: SchemaVersion::V0, value: 42 };
let migrated = state.migrate(SchemaVersion::V1).unwrap();
assert_eq!(migrated.version, SchemaVersion::V1);
assert_eq!(migrated.value, 42);
}
#[test]
fn test_migrate_at_target_is_noop() {
let state = TestState { version: SchemaVersion::V1, value: 99 };
let migrated = state.migrate(SchemaVersion::V1).unwrap();
assert_eq!(migrated.version, SchemaVersion::V1);
assert_eq!(migrated.value, 99);
}
#[test]
fn test_migrate_above_current_is_noop() {
let state = TestState { version: SchemaVersion::V0, value: 7 };
let migrated = state.migrate(SchemaVersion::V0).unwrap();
assert_eq!(migrated.version, SchemaVersion::V0);
assert_eq!(migrated.value, 7);
}
}