toml_migrate/
lib.rs

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
9/// Trait used to determine config versions and migration order
10///
11/// You should probably not implement this yourself, but instead use the [`build_migration_chain!`] macro.
12pub 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
27/// Struct that contains some configuration on how to migrate a config
28///
29/// ```no_run
30/// let migrator = ConfigMigrator::new("version").with_default_version(0);
31///
32/// let (config, migration_occured) = migrator.migrate::<ConfigV2>(config_str).unwrap();
33/// ```
34pub struct ConfigMigrator<'a> {
35    version_key: &'a str,
36    default_version: Option<i64>,
37}
38
39impl<'a> ConfigMigrator<'a> {
40    /// Creates a new [`ConfigMigrator`] using the provided key to find the version of the config
41    #[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    /// Adds a default version to use if it cannot be read from the config file
50    #[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    /// Handles the migration between versions of a configuration
57    ///
58    /// On success, returns a tuple with the config and whether any migrations were performed.
59    /// Errors if it could not read the version (and no default was provided), if the version failed to match
60    /// any of the config structs, or if the config file failed to parse.
61    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/// Generates a chain connecting different config versions with the [`Migrate`] trait
77///
78/// ```no_run
79/// build_migration_chain!(ConfigV1 = 1, ConfigV2 = 2, ConfigV3 = 3);
80/// ```
81#[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    /// Syntax error when parsing the TOML
107    #[error("parsing error")]
108    Parse(#[from] toml_edit::TomlError),
109    /// Error when deserializing the TOML
110    #[error("deserialization error")]
111    Deser(#[from] toml_edit::de::Error),
112    /// Either version field could not be read or provided version field doesn't match a valid version
113    #[error("no valid config version")]
114    NoValidVersion,
115}