database_migration/definition/
mod.rs

1use crate::config::MIGRATION_KEY_FORMAT_STR;
2use crate::error::DefinitionError;
3use crate::migration::{Migration, MigrationKind, NewMigration};
4use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
5use std::ffi::OsStr;
6use std::path::{Path, PathBuf};
7
8pub trait ParseMigration {
9    type Err;
10
11    fn parse_migration(&self) -> Result<Migration, Self::Err>;
12}
13
14pub const SCRIPT_FILE_EXTENSION: &str = ".surql";
15pub const UP_SCRIPT_FILE_EXTENSION: &str = ".up.surql";
16pub const DOWN_SCRIPT_FILE_EXTENSION: &str = ".down.surql";
17
18fn parse_migration(path: &Path, filename: &str) -> Result<Migration, DefinitionError> {
19    if !filename.ends_with(SCRIPT_FILE_EXTENSION) {
20        return Err(DefinitionError::NoFilename);
21    }
22    let up = filename.ends_with(UP_SCRIPT_FILE_EXTENSION);
23    let down = filename.ends_with(DOWN_SCRIPT_FILE_EXTENSION);
24    let (kind, ext_len) = match (up, down) {
25        (false, false) => (MigrationKind::Up, SCRIPT_FILE_EXTENSION.len()),
26        (true, false) => (MigrationKind::Up, UP_SCRIPT_FILE_EXTENSION.len()),
27        (false, true) => (MigrationKind::Down, DOWN_SCRIPT_FILE_EXTENSION.len()),
28        (true, true) => return Err(DefinitionError::AmbiguousDirection),
29    };
30    if filename.contains(".up.") && filename.contains(".down.") {
31        return Err(DefinitionError::AmbiguousDirection);
32    }
33    let len = filename.len();
34    if len < 8 + ext_len {
35        return Err(DefinitionError::MissingDate);
36    }
37    let date_substr = &filename[0..8];
38    let date = NaiveDate::parse_from_str(date_substr, "%Y%m%d")
39        .map_err(|err| DefinitionError::InvalidDate(err.to_string()))?;
40    if len < 15 + ext_len || &filename[8..9] != "_" {
41        return Err(DefinitionError::MissingTime);
42    }
43    let time_substr = &filename[9..15];
44    let time = NaiveTime::parse_from_str(time_substr, "%H%M%S")
45        .map_err(|err| DefinitionError::InvalidTime(err.to_string()))?;
46    let key = NaiveDateTime::new(date, time);
47    let title = if len < 17 + ext_len || &filename[15..16] != "_" {
48        ""
49    } else {
50        &filename[16..len - ext_len].replace('_', " ")
51    };
52    let mut script_path = PathBuf::from(path);
53    script_path.push(filename);
54
55    Ok(Migration {
56        key,
57        title: title.to_string(),
58        kind,
59        script_path,
60    })
61}
62
63impl ParseMigration for str {
64    type Err = DefinitionError;
65
66    fn parse_migration(&self) -> Result<Migration, Self::Err> {
67        let (path, filename) = self
68            .rfind('/')
69            .map_or(("", self), |index| (&self[..index], &self[index + 1..]));
70
71        parse_migration(Path::new(path), filename)
72    }
73}
74
75impl ParseMigration for OsStr {
76    type Err = DefinitionError;
77
78    fn parse_migration(&self) -> Result<Migration, Self::Err> {
79        let path_str = self.to_str().ok_or(DefinitionError::InvalidUtf8Character)?;
80        ParseMigration::parse_migration(path_str)
81    }
82}
83
84impl ParseMigration for Path {
85    type Err = DefinitionError;
86
87    fn parse_migration(&self) -> Result<Migration, Self::Err> {
88        let path = self.parent().unwrap_or_else(|| Self::new(""));
89        let filename = self.file_name().ok_or(DefinitionError::NoFilename)?;
90        let filename = filename
91            .to_str()
92            .ok_or(DefinitionError::InvalidUtf8Character)?;
93
94        parse_migration(path, filename)
95    }
96}
97
98pub trait GetFilename {
99    fn get_filename(&self, migration: &NewMigration) -> String;
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103#[must_use]
104pub struct MigrationFilenameStrategy {
105    pub up_postfix: bool,
106}
107
108impl Default for MigrationFilenameStrategy {
109    fn default() -> Self {
110        Self { up_postfix: true }
111    }
112}
113
114impl MigrationFilenameStrategy {
115    pub const fn with_up_postfix(mut self, up_postfix: bool) -> Self {
116        self.up_postfix = up_postfix;
117        self
118    }
119}
120
121impl GetFilename for MigrationFilenameStrategy {
122    fn get_filename(&self, migration: &NewMigration) -> String {
123        let key = migration.key.format(MIGRATION_KEY_FORMAT_STR).to_string();
124        let title = migration.title.replace(' ', "_");
125        let extension = match (migration.kind, self.up_postfix) {
126            (MigrationKind::Up, true) => UP_SCRIPT_FILE_EXTENSION,
127            (MigrationKind::Up, false) => SCRIPT_FILE_EXTENSION,
128            (MigrationKind::Down, _) => DOWN_SCRIPT_FILE_EXTENSION,
129            (MigrationKind::Baseline, _) => panic!("baselines do not have migration scripts"),
130        };
131        if title.is_empty() {
132            format!("{key}{extension}")
133        } else {
134            format!("{key}_{title}{extension}")
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests;