pub(crate) mod convert;
mod patch;
pub(crate) mod schema;
use std::{
fs,
path::{Path, PathBuf},
process::Command,
time::{SystemTime, UNIX_EPOCH},
};
use aho_corasick::AhoCorasick;
use xshell::{Shell, cmd};
fn ensure_rustfmt(sh: &Shell) {
let toolchain = "nightly";
let version = cmd!(sh, "rustup run {toolchain} rustfmt --version").read().unwrap_or_default();
if !version.contains("nightly") {
panic!(
"Failed to run rustfmt from toolchain '{toolchain}'. Please run `rustup component add \
rustfmt --toolchain {toolchain}` to install it.",
);
}
}
pub fn reformat(text: String) -> String {
let toolchain = "nightly";
let sh = Shell::new().unwrap();
ensure_rustfmt(&sh);
let rustfmt_toml = project_root().join("../../rustfmt.toml");
let unique = SystemTime::now().duration_since(UNIX_EPOCH).expect("time drift").as_nanos();
let temp_path = project_root().join("src").join(format!("__codegen_tmp_{unique}.rs"));
fs::write(&temp_path, text).expect("failed to write temp rustfmt input");
let rustfmt_result = Command::new("rustup")
.arg("run")
.arg(toolchain)
.arg("rustfmt")
.arg("--edition")
.arg("2024")
.arg("--config-path")
.arg(&rustfmt_toml)
.arg("--config")
.arg("skip_children=true")
.arg(&temp_path)
.status();
let mut formatted = fs::read_to_string(&temp_path).expect("failed to read rustfmt output");
let _ = fs::remove_file(&temp_path);
let status = rustfmt_result.expect("failed to run rustfmt for generated content");
assert!(status.success(), "rustfmt failed for generated content");
if !formatted.ends_with('\n') {
formatted.push('\n');
}
formatted
}
pub fn add_hidden_preamble(generator: &'static str, mut text: String) -> String {
let preamble = format!("// Generated by `{generator}`, do not edit by hand.\n\n");
text.insert_str(0, &preamble);
text
}
pub fn add_preamble(generator: &'static str, mut text: String) -> String {
let preamble = format!("//! Generated by `{generator}`, do not edit by hand.\n\n");
text.insert_str(0, &preamble);
text
}
pub fn ensure_file_contents(file: &Path, contents: &str) {
ensure_files_contents([(file, contents)])
}
pub fn ensure_files_contents<'a>(
files_and_contents: impl IntoIterator<Item = (&'a Path, &'a str)>,
) {
let mut err_count = 0;
for (path, contents) in files_and_contents {
if path.exists() {
let old_contents = fs::read_to_string(path).unwrap();
if normalize_newlines(&old_contents) == normalize_newlines(contents) {
continue;
}
}
err_count += 1;
let display_path = path.strip_prefix(project_root()).unwrap_or(path);
eprintln!(
"\n\x1b[31;1merror\x1b[0m: {} was not up-to-date, updating\n",
display_path.display()
);
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
fs::write(path, contents.as_bytes()).unwrap();
}
let (s, were) = match err_count {
0 => return,
1 => ("", "was"),
_ => ("s", "were"),
};
if std::env::var("CI").is_ok() {
eprintln!(" NOTE: run `cargo test` locally and commit the updated files\n");
}
panic!("some file{s} {were} not up to date and has been updated, simply re-run the tests");
}
pub fn replace_block(path: &Path, title: &str, new: &str) -> String {
let file = fs::read_to_string(path).unwrap();
let start = format!("// START BLOCK {title}");
let end = format!("// END BLOCK {title}");
let mut starts = vec![];
let mut ends = vec![];
let searcher = AhoCorasick::new([start.as_str(), end.as_str()]).expect("Wrong block pattern");
for finding in searcher.find_iter(&file) {
match finding.pattern().as_usize() {
0 => starts.push(finding.start()..finding.end()),
1 => ends.push(finding.start()..finding.end()),
n => panic!("{n}"),
}
}
let start_offset = match &*starts {
[] => panic!("Coulnd't find start of block {title} in {p}", p = path.display()),
[offset] => {
let tail = &file[offset.end..];
match tail.find('\n') {
Some(newline_pos) => offset.end + newline_pos + 1,
None => file.len(),
}
}
[..] => panic!(),
};
let end_offset = match &*ends {
[] => panic!("Coulnd't find end of block {title} in {p}", p = path.display()),
[offset] => offset.start,
[..] => panic!(),
};
if end_offset < start_offset {
panic!("End of the {title} block is located before the start in {p}", p = path.display());
}
format!("{}{}{}", &file[..start_offset], new, &file[end_offset..])
}
fn normalize_newlines(s: &str) -> String {
s.replace("\r\n", "\n")
}
pub fn to_uppercase(s: &str) -> String {
let mut chars = s.chars();
format!("{}{}", chars.next().unwrap().to_uppercase(), chars.as_str())
}
pub fn project_root() -> PathBuf {
let dir = env!("CARGO_MANIFEST_DIR");
let res = PathBuf::from(dir);
assert!(res.join("CHANGELOG.md").exists());
res
}
#[track_caller]
pub fn min_prefix<'a>(l: &'a str, r: &str) -> &'a str {
l.char_indices()
.zip(r.chars())
.find(|((_, l), r)| l != r)
.map(|((i, _), _)| &l[..=i])
.unwrap_or_else(|| panic!("there is no different prefix for {l} and {r}"))
}