1use std::collections::BTreeMap;
2use std::fs;
3use std::fs::File;
4use std::io::Read;
5use std::iter::repeat;
6use std::path::Path;
7
8use errors::{Result, ResultExt};
9use regex::Regex;
10
11#[derive(Debug, PartialEq)]
13pub enum Direction {
14 Up,
16 Down,
18}
19
20impl ToString for Direction {
21 fn to_string(&self) -> String {
22 match *self {
23 Direction::Up => "up".to_owned(),
24 Direction::Down => "down".to_owned(),
25 }
26 }
27}
28
29#[derive(Debug)]
31pub struct MigrationFile {
32 pub content: Option<String>,
34 pub direction: Direction,
36 pub number: i32,
38 pub filename: String,
40 pub name: String,
42}
43
44#[derive(Debug)]
46pub struct Migration {
47 pub up: Option<MigrationFile>,
49 pub down: Option<MigrationFile>,
51}
52
53pub type Migrations = BTreeMap<i32, Migration>;
55
56impl MigrationFile {
57 fn new(filename: &str, name: &str, number: i32, direction: Direction) -> MigrationFile {
59 MigrationFile {
60 content: None,
61 filename: filename.to_owned(),
62 number,
63 name: name.to_owned(),
64 direction,
65 }
66 }
67}
68
69pub fn create_migration(path: &Path, slug: &str, number: i32) -> Result<()> {
71 let fixed_slug = slug.replace(" ", "_");
72 let filename_up = get_filename(&fixed_slug, number, Direction::Up);
73 parse_filename(&filename_up)?;
74 let filename_down = get_filename(&fixed_slug, number, Direction::Down);
75 parse_filename(&filename_down)?;
76
77 println!("Creating {}", filename_up);
78 File::create(path.join(filename_up.clone()))
79 .chain_err(|| format!("Failed to create {}", filename_up))?;
80 println!("Creating {}", filename_down);
81 File::create(path.join(filename_down.clone()))
82 .chain_err(|| format!("Failed to create {}", filename_down))?;
83
84 Ok(())
85}
86
87fn get_filename(slug: &str, number: i32, direction: Direction) -> String {
89 let num = number.to_string();
90 let filler = repeat("0").take(4 - num.len()).collect::<String>();
91 filler + &num + "." + slug + "." + &direction.to_string() + ".sql"
92}
93
94pub fn read_migration_files(path: &Path) -> Result<Migrations> {
97 let mut btreemap: Migrations = BTreeMap::new();
98
99 for entry in fs::read_dir(path).chain_err(|| format!("Failed to open {:?}", path))? {
100 let entry = entry.unwrap();
101 let info = match parse_filename(entry.file_name().to_str().unwrap()) {
103 Ok(info) => info,
104 Err(_) => continue,
105 };
106 let mut file =
107 File::open(entry.path()).chain_err(|| format!("Failed to open {:?}", entry.path()))?;
108 let mut content = String::new();
109 file.read_to_string(&mut content)?;
110
111 let migration_file = MigrationFile {
112 content: Some(content),
113 ..info
114 };
115 let migration_number = migration_file.number;
116 let mut migration = match btreemap.remove(&migration_number) {
117 None => Migration {
118 up: None,
119 down: None,
120 },
121 Some(m) => m,
122 };
123 match migration_file.direction {
124 Direction::Up if migration.up.is_none() => {
125 migration.up = Some(migration_file);
126 }
127 Direction::Down if migration.down.is_none() => {
128 migration.down = Some(migration_file);
129 }
130 _ => {
131 bail!(
132 "There are multiple migrations with number {}",
133 migration_number
134 )
135 }
136 };
137
138 btreemap.insert(migration_number, migration);
139 }
140
141 let mut index = 1;
143 for (number, migration) in &btreemap {
144 if index != *number {
145 bail!("Files for migration {} are missing", index);
146 }
147 if migration.up.is_none() || migration.down.is_none() {
148 bail!("Migration {} is missing its up or down file", index);
149 }
150 index += 1;
151 }
152 Ok(btreemap)
153}
154
155fn parse_filename(filename: &str) -> Result<MigrationFile> {
158 let re =
159 Regex::new(r"^(?P<number>[0-9]{4})\.(?P<name>[_0-9a-zA-Z]*)\.(?P<direction>up|down)\.sql$")
160 .unwrap();
161
162 let caps = match re.captures(filename) {
163 None => bail!("File {} has an invalid filename", filename),
164 Some(c) => c,
165 };
166
167 let number = caps
169 .name("number")
170 .unwrap()
171 .as_str()
172 .parse::<i32>()
173 .unwrap();
174 let name = caps.name("name").unwrap().as_str();
175 let direction = if caps.name("direction").unwrap().as_str() == "up" {
176 Direction::Up
177 } else {
178 Direction::Down
179 };
180
181 Ok(MigrationFile::new(filename, name, number, direction))
182}
183
184#[cfg(test)]
185mod tests {
186 use super::{get_filename, parse_filename, read_migration_files, Direction};
187 use std::fs::File;
188 use std::io::prelude::*;
189 use std::path::PathBuf;
190 use tempdir::TempDir;
191
192 fn create_file(path: &PathBuf, filename: &str) {
193 let mut new_path = path.clone();
194 new_path.push(filename);
195 let mut f = File::create(new_path.to_str().unwrap()).unwrap();
196 f.write_all(b"Hello, world!").unwrap();
197 }
198
199 #[test]
200 fn test_parse_good_filename() {
201 let result = parse_filename("0001.tests.up.sql").unwrap();
202 assert_eq!(result.number, 1);
203 assert_eq!(result.name, "tests");
204 assert_eq!(result.direction, Direction::Up);
205 }
206
207 #[test]
208 fn test_parse_bad_filename_format() {
209 let result = parse_filename("0001_tests.up.sql");
211 assert_eq!(result.is_ok(), false);
212 }
213
214 #[test]
215 fn test_get_filename_ok() {
216 let result = get_filename("initial", 1, Direction::Up);
217 assert_eq!(result, "0001.initial.up.sql");
218 }
219
220 #[test]
221 fn test_parse_good_migrations_directory() {
222 let pathbuf = TempDir::new("migrations").unwrap().into_path();
223 create_file(&pathbuf, "0001.tests.up.sql");
224 create_file(&pathbuf, "0001.tests.down.sql");
225 create_file(&pathbuf, "0002.tests_second.up.sql");
226 create_file(&pathbuf, "0002.tests_second.down.sql");
227 let migrations = read_migration_files(pathbuf.as_path());
228
229 assert_eq!(migrations.is_ok(), true);
230 }
231
232 #[test]
233 fn test_parse_missing_migrations_directory() {
234 let pathbuf = TempDir::new("migrations").unwrap().into_path();
235 create_file(&pathbuf, "0001.tests.up.sql");
236 create_file(&pathbuf, "0001.tests.down.sql");
237 create_file(&pathbuf, "0002.tests_second.up.sql");
238 let migrations = read_migration_files(pathbuf.as_path());
239
240 assert_eq!(migrations.is_err(), true);
241 }
242
243 #[test]
244 fn test_parse_skipping_migrations_directory() {
245 let pathbuf = TempDir::new("migrations").unwrap().into_path();
246 create_file(&pathbuf, "0001.tests.up.sql");
247 create_file(&pathbuf, "0001.tests.down.sql");
248 create_file(&pathbuf, "0003.tests_second.up.sql");
249 create_file(&pathbuf, "0003.tests_second.down.sql");
250 let migrations = read_migration_files(pathbuf.as_path());
251
252 assert_eq!(migrations.is_err(), true);
253 }
254
255 #[test]
256 fn test_two_migrations_same_number() {
257 let tests: &[&[&str]] = &[
258 &["0001.a.up.sql", "0001.a.down.sql", "0001.b.up.sql"],
260 &["0001.a.up.sql", "0001.a.down.sql", "0001.b.down.sql"],
262 &[
264 "0001.a.up.sql",
265 "0001.a.down.sql",
266 "0001.b.up.sql",
267 "0001.b.down.sql",
268 ],
269 ];
270
271 for files in tests {
272 let pathbuf = TempDir::new("migrations").unwrap().into_path();
273
274 for file in files.iter() {
275 create_file(&pathbuf, file);
276 }
277
278 let migrations = read_migration_files(pathbuf.as_path());
279
280 assert_eq!(migrations.is_err(), true);
281 }
282 }
283}