use std::{
fmt::Write as _,
fs,
path::{Path, PathBuf},
};
use thiserror::Error;
use crate::{Config, DiscoveredFixtureFile, DiscoveryError, discover_fixture_files};
pub const GENERATED_FIXTURES_PATH: &str = ".hblank/generated/fixtures.rs";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GenerationResult {
pub fixture_files: Vec<DiscoveredFixtureFile>,
pub changed: bool,
}
pub fn refresh_generated_fixtures(
project_root: &Path,
config: &Config,
) -> Result<GenerationResult, GenerationError> {
let fixture_files = discover_fixture_files(project_root, config)?;
let source = generated_source(&fixture_files);
let path = project_root.join(GENERATED_FIXTURES_PATH);
let changed = fs::read(&path).map_or(true, |current| current != source.as_bytes());
if changed {
write_atomically(&path, source.as_bytes())?;
}
Ok(GenerationResult {
fixture_files,
changed,
})
}
#[must_use]
pub fn generated_source(fixture_files: &[DiscoveredFixtureFile]) -> String {
let mut source = String::from(
"// Generated by hblank dev. Do not edit.
",
);
for fixture_file in fixture_files {
let path = rust_string(&fixture_file.absolute_path);
source.push_str(
"#[allow(dead_code, unused_imports)]
",
);
write!(
&mut source,
"#[path = {path}]\nmod {};\n",
fixture_file.module_name
)
.expect("writing generated imports to a String cannot fail");
}
source
}
fn rust_string(path: &Path) -> String {
format!("{:?}", path.to_string_lossy())
}
fn write_atomically(path: &Path, contents: &[u8]) -> Result<(), GenerationError> {
let parent = path
.parent()
.ok_or_else(|| GenerationError::MissingParent(path.to_path_buf()))?;
fs::create_dir_all(parent).map_err(|source| GenerationError::CreateDirectory {
path: parent.to_path_buf(),
source,
})?;
let temporary = parent.join(format!(".fixtures.rs.{}.tmp", std::process::id()));
fs::write(&temporary, contents).map_err(|source| GenerationError::Write {
path: temporary.clone(),
source,
})?;
if let Err(source) = fs::rename(&temporary, path) {
let _ = fs::remove_file(&temporary);
return Err(GenerationError::Write {
path: path.to_path_buf(),
source,
});
}
Ok(())
}
#[derive(Debug, Error)]
pub enum GenerationError {
#[error(transparent)]
Discovery(#[from] DiscoveryError),
#[error("generated fixture path has no parent: {0}")]
MissingParent(PathBuf),
#[error("could not create generated source directory {path}: {source}")]
CreateDirectory {
path: PathBuf,
source: std::io::Error,
},
#[error("could not write generated fixture source {path}: {source}")]
Write {
path: PathBuf,
source: std::io::Error,
},
}