1mod error;
2pub use error::*;
3
4use log::debug;
5use regex::Regex;
6use std::{collections::HashMap, fmt::Display, fs, path::PathBuf, process::Command, str::from_utf8};
7use syn::{
8	Ident, Item,
9	Type::{self, Path, Tuple},
10};
11
12#[derive(Debug, Clone)]
14pub enum Migration {
15	Ok(String),
16	NotOk(),
17}
18
19impl Display for Migration {
20	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21		let s = match self {
22			Migration::Ok(s) => s.to_string(),
23			Migration::NotOk() => "err".to_string(),
24		};
25		f.write_str(&s)
26	}
27}
28
29impl From<&str> for Migration {
30	fn from(s: &str) -> Self {
31		Migration::Ok(s.to_string())
32	}
33}
34
35pub fn get_files(
39	repo: &PathBuf,
40	) -> Result<Vec<PathBuf>> {
42	const PATTERN: &str = "pub type Migrations";
43
44	let mut grep = Command::new("git");
45	grep.args(["grep", "--name-only", PATTERN]);
46	let output = grep.current_dir(repo).output();
48
49	match output {
50		Ok(o) => {
51			let arr: Vec<PathBuf> = o
52				.stdout
53				.split(|c| c == &10)
54				.map(|a| from_utf8(a).unwrap())
55				.filter(|s| !s.is_empty())
56				.map(|s| PathBuf::from(repo).join(PathBuf::from(s)))
58				.collect();
59			Ok(arr)
60		}
61		Err(e) => {
62			eprintln!("repo: {repo:?}");
63			eprintln!("{e:?}");
64			todo!()
65		}
66	}
67}
68
69fn get_migration(t: &Type) -> Result<Vec<Migration>> {
72	match t {
73		Path(p) => {
74			let segment = p.path.segments.iter().nth(1).ok_or(SubmigError::NonStandard)?;
82			let ident = &(segment.ident.clone() as Ident);
83			Ok(vec![(Migration::Ok(ident.to_string()))])
84		}
85		Tuple(t) => {
86			log::debug!("tuple: nb elems: {}", t.elems.len());
87
88			let content = t.elems.iter().flat_map(get_migration).flatten().collect();
89
90			Ok(content)
91		}
92		Type::Paren(p) => {
93			log::debug!("{p:?}");
94			let content = p.elem.clone();
95			get_migration(content.as_ref())
96		}
97		x => {
98			log::warn!("Non standard: {x:?})");
99			Err(SubmigError::NonStandard)
100		}
101	}
102}
103fn get_migrations(it: &Item) -> Result<Vec<Migration>> {
105	log::debug!("get_migrations");
106
107	let migrations: Vec<Migration> = match it {
108		Item::Type(t) => get_migration(&t.ty).unwrap(),
109		_ => unreachable!(),
110	};
111	debug!("Migrations: {migrations:?}");
112	Ok(migrations)
113}
114
115fn check_naming(migrations: Vec<Migration>) -> (Vec<Migration>, Vec<Migration>) {
119	let version_regexp = Regex::new(r"^V.*$").unwrap();
120
121	let valid = migrations
122		.iter()
123		.filter(|m| m.to_string() == "Unreleased" || version_regexp.is_match(&m.to_string()))
124		.cloned()
125		.collect();
126	let invalid = migrations
127		.iter()
128		.filter(|m| m.to_string() != "Unreleased" && !version_regexp.is_match(&m.to_string()))
129		.cloned()
130		.collect();
131	(valid, invalid)
132}
133
134type SearchResult = HashMap<PathBuf, (Vec<Migration>, Vec<Migration>)>;
135
136pub fn find(
140	repo: &PathBuf,
141	) -> Result<SearchResult> {
143	let files = get_files(
144		repo,
145		)?;
147	let mut res: SearchResult = HashMap::new();
148
149	for file in files {
150		let code = fs::read_to_string(&file).map_err(|_e| SubmigError::IO)?;
151		let syntax = syn::parse_file(&code).map_err(|_| SubmigError::Parsing)?;
152
153		let hits: Vec<&Item> =
154			syntax.items.iter().filter(|&item| matches!(item, syn::Item::Type(i) if i.ident == "Migrations")).collect();
155
156		debug!("Found {} Migration hits in {}", hits.len(), file.display());
157		if let Some(hit) = hits.first() {
158			let migrations: Vec<Migration> = get_migrations(hit)?;
159			let (valid, invalid) = check_naming(migrations);
160			res.insert(file, (valid, invalid));
161		}
162	}
163	Ok(res)
164}
165
166#[cfg(test)]
167mod tests {
168	use super::*;
169	use std::env;
170
171	fn setup() {
172		let repo_polkadot_sdk: &str = &env::var("REPO_POLKADOT_SDK").unwrap_or_default();
173		if repo_polkadot_sdk.is_empty() {
174			env::set_var("REPO_POLKADOT_SDK", "/projects/polkadot-sdk");
175		}
176
177		let repo_fellowship_runtimes: &str = &env::var("REPO_FELLOWSHIP_RUNTIMES").unwrap_or_default();
178		if repo_fellowship_runtimes.is_empty() {
179			env::set_var("REPO_FELLOWSHIP_RUNTIMES", "/projects/fellowship-runtimes");
180		}
181	}
182
183	#[test]
184	fn it_find_files() {
185		setup();
186		let polkadot_repo: &str = &env::var("REPO_POLKADOT_SDK").unwrap();
187		let result = get_files(&PathBuf::from(polkadot_repo)).unwrap();
188		assert_eq!(13, result.len());
189	}
190
191	#[test]
192	fn it_finds_migrations_polkadot_sdk() {
193		setup();
194		let polkadot_repo: &str = &env::var("REPO_POLKADOT_SDK").unwrap();
195		let result = find(&PathBuf::from(polkadot_repo)).unwrap();
196		assert_eq!(12, result.len());
197		println!("result = {:?}", result);
198	}
199
200	#[test]
201	fn it_finds_migrations_fellowship_runtimes() {
202		setup();
203		let polkadot_repo: &str = &env::var("REPO_FELLOWSHIP_RUNTIMES").unwrap();
204		let result = find(&PathBuf::from(polkadot_repo)).unwrap();
205		assert_eq!(6, result.len());
206		println!("result = {:?}", result);
207	}
208}