database_migration/definition/
mod.rs1use 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;