database_migration_files/
lib.rs

1#![doc(html_root_url = "https://docs.rs/database-migration-files/0.1.0")]
2
3use database_migration::checksum::hash_migration_script;
4use database_migration::definition::{GetFilename, ParseMigration};
5use database_migration::error::Error;
6use database_migration::migration::{Migration, NewMigration, ScriptContent};
7use database_migration::repository::{CreateNewMigration, ListMigrations, ReadScriptContent};
8use std::fs;
9use std::fs::File;
10#[cfg(target_family = "windows")]
11use std::os::windows::fs::FileTypeExt;
12use std::path::Path;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct MigrationDirectory<'a> {
16    path: &'a Path,
17}
18
19impl<'a> MigrationDirectory<'a> {
20    pub const fn new(path: &'a Path) -> Self {
21        Self { path }
22    }
23
24    pub fn create_directory_if_not_existing(&self) -> Result<(), Error> {
25        if self.path.exists() {
26            return Ok(());
27        }
28        fs::create_dir_all(self.path)
29            .map_err(|err| Error::CreatingMigrationsFolder(err.to_string()))
30    }
31
32    pub const fn files<S>(&self, filename_strategy: S) -> MigrationFiles<'a, S> {
33        MigrationFiles::new(self.path, filename_strategy)
34    }
35}
36
37impl<'a> ListMigrations for MigrationDirectory<'a> {
38    type Iter = MigDirIter<'a>;
39
40    fn list_all_migrations(&self) -> Result<Self::Iter, Error> {
41        fs::read_dir(self.path)
42            .map_err(|err| Error::ScanningMigrationDirectory(err.to_string()))
43            .map(|dir_iter| MigDirIter {
44                base_dir: self.path,
45                dir_iter,
46            })
47    }
48}
49
50impl ReadScriptContent for MigrationDirectory<'_> {
51    fn read_script_content(&self, migration: &Migration) -> Result<ScriptContent, Error> {
52        let content = fs::read_to_string(&migration.script_path)
53            .map_err(|err| Error::ReadingMigrationFile(err.to_string()))?;
54        let checksum = hash_migration_script(migration, &content);
55        Ok(ScriptContent {
56            key: migration.key,
57            kind: migration.kind,
58            path: migration.script_path.clone(),
59            content,
60            checksum,
61        })
62    }
63}
64
65#[derive(Debug)]
66pub struct MigDirIter<'a> {
67    base_dir: &'a Path,
68    dir_iter: fs::ReadDir,
69}
70
71impl Iterator for MigDirIter<'_> {
72    type Item = Result<Migration, Error>;
73
74    fn next(&mut self) -> Option<Self::Item> {
75        for dir_entry in &mut self.dir_iter {
76            return match dir_entry {
77                Ok(entry) => {
78                    match entry.file_type() {
79                        Ok(file_type) => {
80                            #[cfg(target_family = "windows")]
81                            if file_type.is_dir() || file_type.is_symlink_dir() {
82                                continue;
83                            }
84                            #[cfg(not(target_family = "windows"))]
85                            if file_type.is_dir() {
86                                continue;
87                            }
88                        },
89                        Err(err) => {
90                            return Some(Err(Error::ScanningMigrationDirectory(err.to_string())))
91                        },
92                    }
93                    let file_path = self.base_dir.join(entry.file_name());
94                    Some(file_path.parse_migration().map_err(Error::from))
95                },
96                Err(err) => Some(Err(Error::ScanningMigrationDirectory(err.to_string()))),
97            };
98        }
99        None
100    }
101
102    fn size_hint(&self) -> (usize, Option<usize>) {
103        self.dir_iter.size_hint()
104    }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct MigrationFiles<'a, S> {
109    path: &'a Path,
110    filename_strategy: S,
111}
112
113impl<'a, S> MigrationFiles<'a, S> {
114    pub const fn new(path: &'a Path, filename_strategy: S) -> Self {
115        Self {
116            path,
117            filename_strategy,
118        }
119    }
120}
121
122impl<S> CreateNewMigration for MigrationFiles<'_, S>
123where
124    S: GetFilename,
125{
126    fn create_new_migration(&self, new_migration: NewMigration) -> Result<Migration, Error> {
127        let filename = self.filename_strategy.get_filename(&new_migration);
128        let script_path = self.path.join(&filename);
129        File::create_new(&script_path).map_err(|err| Error::CreatingScriptFile(err.to_string()))?;
130        Ok(Migration {
131            key: new_migration.key,
132            title: new_migration.title,
133            kind: new_migration.kind,
134            script_path,
135        })
136    }
137}
138
139#[cfg(test)]
140mod tests;
141
142// workaround for false positive 'unused extern crate' warnings until
143// Rust issue [#95513](https://github.com/rust-lang/rust/issues/95513) is fixed
144#[cfg(test)]
145mod dummy_extern_uses {
146    use version_sync as _;
147}