flowfine/migration/
parser.rs1use 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#[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 let result = get_migrations(path, &version_formatting);
122
123 assert!(result.is_ok());
125 assert_migrations(expected_result, result.unwrap().migrations);
126 }
127
128 #[test]
129 fn test_invalid_migrations() {
130 let version_formatting = VersionFormatting::Numeric;
132 let path = "./tests/data/unit/invalid_migrations";
133
134 let result = get_migrations(path, &version_formatting);
136
137 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}