immigrant_file_diffs/
lib.rs1use std::{
2 collections::HashMap,
3 ffi::OsString,
4 fs::{self, read_dir},
5 io,
6 path::{Path, PathBuf},
7 result,
8 str::FromStr,
9};
10
11mod migration;
12mod patch_util;
13
14use Error::*;
15
16pub use self::migration::Migration;
17
18#[derive(Debug)]
19pub struct MigrationId {
20 pub id: u32,
21 pub slug: String,
22 pub dirname: String,
23}
24impl MigrationId {
25 pub fn new(id: u32, slug: String) -> Self {
26 Self {
27 id,
28 dirname: format!("{id:0>14}_{slug}"),
29 slug,
30 }
31 }
32}
33
34#[derive(thiserror::Error, Debug)]
35pub enum Error {
36 #[error("io: {0}")]
37 Io(#[from] io::Error),
38 #[error("file name is not utf-8: {0:?}")]
39 NonUtf8(OsString),
40
41 #[error("missing migration id: {0:?}, dir name should start with number, i.e 00003-name")]
42 MissingMigrationId(String),
43 #[error("missing migration name: {0:?}, dir name should end with short name, i.e 00003-name")]
44 MissingSlug(String),
45
46 #[error("two migrations have the same number: {0:?} and {1:?}")]
47 SequenceIdConflict(String, String),
48 #[error("missing migration with id {0}")]
49 IdHole(u32),
50
51 #[error("failed to find migrations directory at {0} or any parent directory. Have you forgot to init your project?")]
52 FailedToFindRoot(PathBuf),
53
54 #[error("failed to parse migration {id}: {error}")]
55 MigrationParse {
56 id: String,
57 #[source]
58 error: migration::Error,
59 },
60
61 #[error("failed to read db.update")]
62 MigrationReadError(io::Error),
63}
64pub type Result<T, E = Error> = result::Result<T, E>;
65
66pub fn list_ids(path: &Path) -> Result<Vec<MigrationId>> {
67 let dir = read_dir(path)?;
68 let mut ids = Vec::new();
69 for ele in dir {
70 let ele = ele?;
71 let meta = ele.metadata()?;
72 if !meta.is_dir() {
73 continue;
74 }
75 let dirname = ele.file_name();
76 let dirname = dirname.to_str().ok_or_else(|| NonUtf8(dirname.clone()))?;
77
78 let mut split = dirname.splitn(2, '_');
79 let Some(id) = split.next() else {
80 return Err(MissingMigrationId(dirname.to_owned()));
81 };
82 let Ok(id) = id.parse::<u32>() else {
83 return Err(MissingMigrationId(dirname.to_owned()));
84 };
85 let Some(slug) = split.next() else {
86 return Err(MissingSlug(dirname.to_owned()));
87 };
88
89 ids.push(MigrationId {
90 id,
91 slug: slug.to_string(),
92 dirname: dirname.to_owned(),
93 });
94 }
95 if ids.is_empty() {
96 return Ok(Vec::new());
97 }
98 ids.sort_by_key(|id| id.id);
99 {
101 let mut has = HashMap::new();
102 for id in ids.iter() {
103 if let Some(old) = has.insert(id.id, id.slug.clone()) {
104 return Err(SequenceIdConflict(old, id.slug.to_owned()));
105 }
106 }
107 }
108 {
110 for (i, id) in ids.iter().enumerate() {
111 if id.id as usize != i {
112 return Err(IdHole(i as u32));
113 }
114 }
115 }
116 Ok(ids)
117}
118pub fn list(root: &Path) -> Result<Vec<(MigrationId, Migration, PathBuf)>> {
119 let mut out = Vec::new();
120 let ids = list_ids(root)?;
121 let mut path = root.to_path_buf();
122 for id in ids {
123 let slug = id.slug.clone();
124 path.push(&id.dirname);
125 let migration_dir = path.to_owned();
126 path.push("db.update");
127
128 let update = fs::read_to_string(&path).map_err(Error::MigrationReadError)?;
129 let migration =
130 Migration::from_str(&update).map_err(|error| MigrationParse { id: slug, error })?;
131 out.push((id, migration, migration_dir));
132
133 path.pop();
134 path.pop();
135 }
136 Ok(out)
137}
138
139pub fn find_root(from: &Path) -> Result<PathBuf> {
140 let mut out = from.to_path_buf();
141 loop {
142 out.push("migrations");
143 if out.is_dir() {
144 return Ok(out);
145 }
146 out.pop();
147 if !out.pop() {
148 return Err(FailedToFindRoot(from.to_owned()));
149 }
150 }
151}