use atomicwrites::{AtomicFile, OverwriteBehavior};
use camino::{Utf8Path, Utf8PathBuf};
use diffy::Patch;
use std::{error, fmt, io};
#[derive(Clone, Debug)]
pub struct HakariCargoToml {
toml_path: Utf8PathBuf,
contents: String,
start_offset: usize,
end_offset: usize,
}
impl HakariCargoToml {
pub const BEGIN_SECTION: &'static str = "\n### BEGIN HAKARI SECTION\n";
pub const END_SECTION: &'static str = "\n### END HAKARI SECTION\n";
pub fn new(toml_path: impl Into<Utf8PathBuf>) -> Result<Self, CargoTomlError> {
let toml_path = toml_path.into();
let contents = match std::fs::read_to_string(&toml_path) {
Ok(contents) => contents,
Err(error) => return Err(CargoTomlError::Io { toml_path, error }),
};
Self::new_in_memory(toml_path, contents)
}
pub fn new_relative(
workspace_root: impl Into<Utf8PathBuf>,
crate_dir: impl AsRef<Utf8Path>,
) -> Result<Self, CargoTomlError> {
let mut toml_path = workspace_root.into();
toml_path.push(crate_dir);
toml_path.push("Cargo.toml");
Self::new(toml_path)
}
pub fn new_in_memory(
toml_path: impl Into<Utf8PathBuf>,
contents: String,
) -> Result<Self, CargoTomlError> {
let toml_path = toml_path.into();
let start_offset = match contents.find(Self::BEGIN_SECTION) {
Some(offset) => {
offset + Self::BEGIN_SECTION.len()
}
None => return Err(CargoTomlError::GeneratedSectionNotFound { toml_path }),
};
let end_offset = match contents[(start_offset - 1)..].find(Self::END_SECTION) {
Some(offset) => start_offset + offset,
None => return Err(CargoTomlError::GeneratedSectionNotFound { toml_path }),
};
Ok(Self {
toml_path,
contents,
start_offset,
end_offset,
})
}
pub fn toml_path(&self) -> &Utf8Path {
&self.toml_path
}
pub fn contents(&self) -> &str {
&self.contents
}
pub fn generated_offsets(&self) -> (usize, usize) {
(self.start_offset, self.end_offset)
}
pub fn generated_contents(&self) -> &str {
&self.contents[self.start_offset..self.end_offset]
}
pub fn is_changed(&self, toml: &str) -> bool {
self.generated_contents() != toml
}
pub fn diff_toml<'a>(&'a self, toml: &'a str) -> Patch<'a, str> {
diffy::create_patch(self.generated_contents(), toml)
}
pub fn write_to_file(self, toml: &str) -> Result<bool, CargoTomlError> {
if !self.is_changed(toml) {
return Ok(false);
}
let try_block = || {
let atomic_file = AtomicFile::new(&self.toml_path, OverwriteBehavior::AllowOverwrite);
atomic_file.write(|f| self.write(toml, f))
};
match (try_block)() {
Ok(()) => Ok(true),
Err(atomicwrites::Error::Internal(error)) | Err(atomicwrites::Error::User(error)) => {
Err(CargoTomlError::Io {
toml_path: self.toml_path,
error,
})
}
}
}
pub fn write(&self, toml: &str, mut out: impl io::Write) -> io::Result<()> {
write!(out, "{}", &self.contents[..self.start_offset])?;
write!(out, "{}", toml)?;
write!(out, "{}", &self.contents[self.end_offset..])
}
pub fn write_to_fmt(&self, toml: &str, mut out: impl fmt::Write) -> fmt::Result {
write!(out, "{}", &self.contents[..self.start_offset])?;
write!(out, "{}", toml)?;
write!(out, "{}", &self.contents[self.end_offset..])
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum CargoTomlError {
Io {
toml_path: Utf8PathBuf,
error: io::Error,
},
GeneratedSectionNotFound {
toml_path: Utf8PathBuf,
},
}
impl fmt::Display for CargoTomlError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CargoTomlError::Io { toml_path, .. } => {
write!(f, "error while reading path '{}'", toml_path)
}
CargoTomlError::GeneratedSectionNotFound { toml_path, .. } => {
write!(
f,
"in '{}', unable to find\n\
### BEGIN HAKARI SECTION\n\
...\n\
### END HAKARI SECTION",
toml_path
)
}
}
}
}
impl error::Error for CargoTomlError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
CargoTomlError::Io { error, .. } => Some(error),
CargoTomlError::GeneratedSectionNotFound { .. } => None,
}
}
}