use std::fs::{self, OpenOptions};
use std::io::{ErrorKind, Write};
use std::path::Path;
use anyhow::{Context, Result, bail};
pub fn write_file(path: &Path, contents: &str, force: bool) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
let mut options = OpenOptions::new();
options.write(true);
if force {
options.create(true).truncate(true);
} else {
options.create_new(true);
}
let mut file = match options.open(path) {
Ok(file) => file,
Err(err) if err.kind() == ErrorKind::AlreadyExists => {
bail!(
"{} already exists (use --force to overwrite)",
path.display()
)
}
Err(err) => {
return Err(err).with_context(|| format!("failed to create {}", path.display()));
}
};
file.write_all(contents.as_bytes())
.with_context(|| format!("failed to write {}", path.display()))
}
pub fn ensure_mod_decl(file: &Path, decl: &str) -> Result<()> {
let name = mod_name(decl).unwrap_or(decl).to_owned();
edit_lines(file, |lines| {
if contains_line(lines, decl) {
return;
}
let index = match lines.iter().rposition(|l| mod_name(l).is_some()) {
Some(last) => {
let first = lines[..=last]
.iter()
.rposition(|l| mod_name(l).is_none())
.map_or(0, |i| i + 1);
(first..=last)
.find(|&i| mod_name(&lines[i]).is_some_and(|other| other > name.as_str()))
.unwrap_or(last + 1)
}
None => {
let docs = lines.iter().take_while(|l| l.starts_with("//!")).count();
if docs > 0 {
lines.insert(docs, String::new());
}
docs + usize::from(docs > 0)
}
};
lines.insert(index, decl.to_owned());
})
}
fn mod_name(line: &str) -> Option<&str> {
let line = line.trim();
let rest = line.strip_prefix("pub ").unwrap_or(line);
rest.strip_prefix("mod ")?.strip_suffix(';').map(str::trim)
}
pub fn ensure_trailing_line(file: &Path, line: &str) -> Result<()> {
edit_lines(file, |lines| {
if !contains_line(lines, line) {
lines.push(String::new());
lines.push(line.to_owned());
}
})
}
pub fn insert_before_marker(file: &Path, marker: &str, line: &str) -> Result<bool> {
let mut found = true;
edit_lines(file, |lines| {
if contains_line(lines, line) {
return;
}
match lines.iter().position(|l| l.contains(marker)) {
Some(index) => lines.insert(index, line.to_owned()),
None => found = false,
}
})?;
Ok(found)
}
fn contains_line(lines: &[String], wanted: &str) -> bool {
lines.iter().any(|line| line.trim() == wanted.trim())
}
fn edit_lines(file: &Path, edit: impl FnOnce(&mut Vec<String>)) -> Result<()> {
let original = match fs::read_to_string(file) {
Ok(text) => text,
Err(err) if err.kind() == ErrorKind::NotFound => String::new(),
Err(err) => return Err(err).with_context(|| format!("failed to read {}", file.display())),
};
let mut lines: Vec<String> = original.lines().map(str::to_owned).collect();
edit(&mut lines);
let mut updated = lines.join("\n");
updated.push('\n');
if updated != original {
if let Some(parent) = file.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
}
fs::write(file, updated).with_context(|| format!("failed to update {}", file.display()))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_file(name: &str, contents: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("slave-gen-{}-{name}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
let file = dir.join("mod.rs");
fs::write(&file, contents).unwrap();
file
}
#[test]
fn mod_decls_go_after_existing_decls() {
let file = temp_file("after", "//! Docs.\n\nmod b;\nmod d;\n\npub use b::x;\n");
ensure_mod_decl(&file, "mod c;").unwrap();
ensure_mod_decl(&file, "mod c;").unwrap();
ensure_mod_decl(&file, "mod a;").unwrap();
ensure_mod_decl(&file, "mod e;").unwrap();
let text = fs::read_to_string(&file).unwrap();
assert_eq!(
text,
"//! Docs.\n\nmod a;\nmod b;\nmod c;\nmod d;\nmod e;\n\npub use b::x;\n"
);
}
#[test]
fn mod_decls_go_after_doc_comment() {
let file = temp_file("docs", "//! Docs.\n");
ensure_mod_decl(&file, "mod a;").unwrap();
assert_eq!(fs::read_to_string(&file).unwrap(), "//! Docs.\n\nmod a;\n");
}
#[test]
fn inserts_before_marker_once() {
let file = temp_file("marker", "a\n // marker\n}\n");
assert!(insert_before_marker(&file, "// marker", " x").unwrap());
assert!(insert_before_marker(&file, "// marker", " x").unwrap());
assert_eq!(
fs::read_to_string(&file).unwrap(),
"a\n x\n // marker\n}\n"
);
let file = temp_file("nomarker", "a\n");
assert!(!insert_before_marker(&file, "// marker", "x").unwrap());
}
}