flowfine/migration/
parser.rs

1use crate::config::VersionFormatting;
2use crate::migration::lexer::delimit_queries;
3use crate::migration::version::MigrationVersionKey;
4use crate::migration::FileError::*;
5use crate::migration::*;
6use std::fs::{read_dir, read_to_string, DirEntry};
7
8pub fn get_migrations(
9    directory_path: &str,
10    version_formatting: &VersionFormatting,
11) -> Result<MigrationResult, FileError> {
12    let entries = read_dir(directory_path).map_err(|_err| DirectoryNotLoadedError)?;
13    let mut migration_stack = MigrationStack::new();
14
15    for entry in entries {
16        let entry = entry.map_err(|_err| FileNotLoadedError)?;
17
18        match parse_migration(&entry, version_formatting) {
19            Ok(migration) => migration_stack.push_migration(migration),
20            Err(err) => migration_stack.push_error(err),
21        }
22    }
23
24    Ok(migration_stack.into_result())
25}
26
27fn parse_migration(
28    path: &DirEntry,
29    version_formatting: &VersionFormatting,
30) -> Result<Migration, MigrationParsingError> {
31    let filename = parse_migration_filename(path);
32    let (version, version_key) = parse_migration_version(&filename, version_formatting)?;
33    let name = parse_migration_name(&filename)?;
34    let content = parse_migration_content(path)?;
35    let queries = delimit_queries(&filename, &content)?;
36
37    let migration = Migration {
38        filename,
39        version,
40        version_key,
41        name,
42        content,
43        queries,
44    };
45
46    Ok(migration)
47}
48
49fn parse_migration_filename(path: &DirEntry) -> String {
50    match path.file_name().into_string() {
51        Ok(filename) => filename,
52        Err(filename) => filename.to_string_lossy().to_string(),
53    }
54}
55
56fn parse_migration_version(
57    filename: &str,
58    version_formatting: &VersionFormatting,
59) -> Result<(String, MigrationVersionKey), MigrationParsingError> {
60    let start = 1;
61    let end = filename
62        .find("__")
63        .ok_or(InvalidMigrationFormatError(filename.to_string()))?;
64    let version = &filename[start..end];
65
66    MigrationVersionKey::new(version_formatting, version)
67        .map(|version_key| (version.to_string(), version_key))
68        .ok_or(InvalidVersionFormatError(filename.to_string()))
69}
70
71fn parse_migration_name(filename: &str) -> Result<String, MigrationParsingError> {
72    let extension = ".cql";
73
74    if !filename.ends_with(extension) {
75        return Err(InvalidMigrationFormatError(filename.to_string()));
76    }
77
78    let migration_name = filename
79        .find("__")
80        .and_then(|start| filename.rfind(extension).map(|end| (start, end)))
81        .map(|(start, end)| &filename[start + 2..end])
82        .ok_or(InvalidMigrationFormatError(filename.to_string()))?;
83
84    Ok(migration_name.replace("_", " "))
85}
86
87fn parse_migration_content(path: &DirEntry) -> Result<String, MigrationParsingError> {
88    match read_to_string(path.path()) {
89        Ok(content) if !content.trim().is_empty() => Ok(content),
90        Ok(_) => Err(MissingMigrationContentError(parse_migration_filename(path))),
91        Err(_) => Err(MissingMigrationContentError(parse_migration_filename(path))),
92    }
93}
94
95// todo: write tests for checking migration's queries
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use itertools::Itertools;
100    use rstest::rstest;
101    use std::collections::HashSet;
102
103    #[rstest(version_formatting, path, expected_result,
104    case(VersionFormatting::Numeric, "./tests/data/unit/numeric_migrations", vec![
105    "V1__migration.cql",
106    "V1.1__migration.cql",
107    "V2.0__migration.cql",
108    ]),
109    case(VersionFormatting::Datetime, "./tests/data/unit/datetime_migrations", vec![
110    "V20230903141500__migration.cql",
111    "V20230903141501__migration.cql",
112    "V20230903141502__migration.cql",
113    ])
114    )]
115    fn test_valid_migrations(
116        version_formatting: VersionFormatting,
117        path: &str,
118        expected_result: Vec<&str>,
119    ) {
120        // when
121        let result = get_migrations(path, &version_formatting);
122
123        // then
124        assert!(result.is_ok());
125        assert_migrations(expected_result, result.unwrap().migrations);
126    }
127
128    #[test]
129    fn test_invalid_migrations() {
130        // given
131        let version_formatting = VersionFormatting::Numeric;
132        let path = "./tests/data/unit/invalid_migrations";
133
134        // when
135        let result = get_migrations(path, &version_formatting);
136
137        // then
138        assert!(result.is_ok());
139        let expected = vec![
140            InvalidVersionFormatError("V__invalid_migration_version.cql".to_string()),
141            InvalidMigrationFormatError("V1_invalid_migration_underscore.cql".to_string()),
142            MissingMigrationContentError("V1__invalid_migration_missing_content.cql".to_string()),
143            InvalidMigrationFormatError("V1__invalid_migration_missing_extension.".to_string()),
144            NoSemicolonsFoundError("V1__invalid_migration_missing_semicolon.cql".to_string()),
145        ];
146
147        assert_errors_any_order(expected, result.unwrap().errors);
148    }
149
150    fn assert_migrations(expected: Vec<&str>, actual: Vec<Migration>) {
151        let actual_filenames = actual
152            .into_iter()
153            .into_iter()
154            .map(|migration| migration.filename)
155            .collect_vec();
156
157        assert_eq!(expected, actual_filenames);
158    }
159
160    fn assert_errors_any_order(
161        expected: Vec<MigrationParsingError>,
162        actual: Vec<MigrationParsingError>,
163    ) {
164        assert_eq!(
165            expected.into_iter().collect::<HashSet<_>>(),
166            actual.into_iter().collect::<HashSet<_>>(),
167        );
168    }
169}