toasty_cli/migration/
snapshot_file.rs

1use anyhow::{Result, bail};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::path::Path;
5use std::str::FromStr;
6use toasty_core::schema::db::Schema;
7use toml_edit::{DocumentMut, Item};
8
9const SNAPSHOT_FILE_VERSION: u32 = 1;
10
11/// A TOML-serializable snapshot of the database schema at a point in time.
12///
13/// Each time a migration is generated, a corresponding snapshot is written
14/// alongside it. The next `generate` run loads the most recent snapshot to
15/// compute a diff against the current schema.
16///
17/// The file carries a version number. [`SnapshotFile::load`] and the
18/// [`FromStr`] implementation reject files whose version does not match the
19/// current format.
20///
21/// # Examples
22///
23/// ```
24/// use toasty_cli::SnapshotFile;
25/// use toasty_core::schema::db::Schema;
26///
27/// let snapshot = SnapshotFile::new(Schema::default());
28///
29/// // Serialize to TOML via the serde impl
30/// let toml_str = toml::to_string_pretty(&snapshot).unwrap();
31/// let restored: SnapshotFile = toml::from_str(&toml_str).unwrap();
32/// ```
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SnapshotFile {
35    /// Snapshot file format version
36    version: u32,
37
38    /// The database schema
39    pub schema: Schema,
40}
41
42impl SnapshotFile {
43    /// Create a new snapshot file with the given schema
44    pub fn new(schema: Schema) -> Self {
45        Self {
46            version: SNAPSHOT_FILE_VERSION,
47            schema,
48        }
49    }
50
51    /// Load a snapshot file from a TOML file
52    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
53        let contents = std::fs::read_to_string(path.as_ref())?;
54        contents.parse()
55    }
56
57    /// Save the snapshot file to a TOML file
58    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
59        std::fs::write(path.as_ref(), self.to_string())?;
60        Ok(())
61    }
62}
63
64impl FromStr for SnapshotFile {
65    type Err = anyhow::Error;
66
67    fn from_str(s: &str) -> Result<Self> {
68        let file: SnapshotFile = toml::from_str(s)?;
69
70        // Validate version
71        if file.version != SNAPSHOT_FILE_VERSION {
72            bail!(
73                "Unsupported snapshot file version: {}. Expected version {}",
74                file.version,
75                SNAPSHOT_FILE_VERSION
76            );
77        }
78
79        Ok(file)
80    }
81}
82
83impl fmt::Display for SnapshotFile {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        let doc = self.to_toml_document().map_err(|_| fmt::Error)?;
86        write!(f, "{}", doc)
87    }
88}
89
90impl SnapshotFile {
91    fn to_toml_document(&self) -> Result<DocumentMut> {
92        let mut doc = toml_edit::ser::to_document(self)?;
93        for (_key, item) in doc.as_table_mut().iter_mut() {
94            if item.is_inline_table() {
95                let mut placeholder = Item::None;
96                std::mem::swap(item, &mut placeholder);
97                let mut table = placeholder.into_table().unwrap();
98
99                for (_key, item) in table.iter_mut() {
100                    if item.is_array() {
101                        let mut placeholder = Item::None;
102                        std::mem::swap(item, &mut placeholder);
103                        let mut array = placeholder.into_array_of_tables().unwrap();
104
105                        for table in array.iter_mut() {
106                            for (_key, item) in table.iter_mut() {
107                                if item.is_array() {
108                                    let mut placeholder = Item::None;
109                                    std::mem::swap(item, &mut placeholder);
110                                    let array = placeholder.into_array_of_tables().unwrap();
111                                    *item = array.into();
112                                }
113                            }
114                        }
115
116                        *item = array.into();
117                    }
118                }
119
120                *item = table.into();
121            }
122        }
123
124        Ok(doc)
125    }
126}