1#![doc = include_str!("../README.md")]
2
3use std::any::{Any, TypeId};
4
5use serde::de::DeserializeOwned;
6use thiserror::Error;
7use toml_edit::DocumentMut;
8
9pub trait Migrate: From<Self::From> + DeserializeOwned + Any {
13 type From: Migrate;
14 const VERSION: i64;
15
16 fn migrate_from_doc(version: i64, doc: DocumentMut) -> Result<Self, Error> {
17 if version == Self::VERSION {
18 Ok(toml_edit::de::from_document(doc)?)
19 } else if TypeId::of::<Self>() == TypeId::of::<Self::From>() {
20 Err(Error::NoValidVersion)
21 } else {
22 Self::From::migrate_from_doc(version, doc).map(Into::into)
23 }
24 }
25}
26
27pub struct ConfigMigrator<'a> {
35 version_key: &'a str,
36 default_version: Option<i64>,
37}
38
39impl<'a> ConfigMigrator<'a> {
40 #[must_use]
42 pub const fn new(version_key: &'a str) -> Self {
43 Self {
44 version_key,
45 default_version: None,
46 }
47 }
48
49 #[must_use]
51 pub const fn with_default_version(mut self, default_version: i64) -> Self {
52 self.default_version = Some(default_version);
53 self
54 }
55
56 pub fn migrate_config<T: Migrate>(&self, config_str: &str) -> Result<(T, bool), Error> {
62 let mut doc = config_str.parse::<DocumentMut>()?;
63 let version = doc
64 .remove(self.version_key)
65 .and_then(|x| x.as_integer())
66 .or(self.default_version)
67 .ok_or(Error::NoValidVersion)?;
68
69 let config = T::migrate_from_doc(version, doc)?;
70 let migration_occured = version != T::VERSION;
71
72 Ok((config, migration_occured))
73 }
74}
75
76#[macro_export]
82macro_rules! build_migration_chain {
83 ($type:ident = $ver:literal) => {
84 impl $crate::Migrate for $type {
85 type From = Self;
86 const VERSION: i64 = $ver;
87 }
88 };
89 ($first_type:ident = $first_ver:literal, $($rest:tt)*) => {
90 build_migration_chain!($first_type = $first_ver);
91
92 build_migration_chain!(@internal $first_type, $($rest)*);
93 };
94 (@internal $prev_type:ident, $type:ident = $ver:literal $(, $($rest:tt)*)?) => {
95 impl $crate::Migrate for $type {
96 type From = $prev_type;
97 const VERSION: i64 = $ver;
98 }
99
100 $(build_migration_chain!(@internal $type, $($rest)*);)?
101 };
102}
103
104#[derive(Debug, Error)]
105pub enum Error {
106 #[error("parsing error")]
108 Parse(#[from] toml_edit::TomlError),
109 #[error("deserialization error")]
111 Deser(#[from] toml_edit::de::Error),
112 #[error("no valid config version")]
114 NoValidVersion,
115}