immigrant_file_diffs/
lib.rs

1use 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	// Disallow duplicates
100	{
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	// Disallow holes
109	{
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}