use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
pub struct LocalManifest {
pub path: PathBuf,
pub data: toml_edit::DocumentMut,
}
impl LocalManifest {
pub fn try_new(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("cannot read {}", path.display()))?;
let data: toml_edit::DocumentMut = content
.parse()
.with_context(|| format!("invalid TOML in {}", path.display()))?;
Ok(Self {
path: path.to_path_buf(),
data,
})
}
pub fn write(&self) -> Result<()> {
std::fs::write(&self.path, self.data.to_string())
.with_context(|| format!("cannot write {}", self.path.display()))
}
pub fn package_name(&self) -> Option<&str> {
self.data
.get("package")
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
}
pub fn package_version(&self) -> Option<&str> {
self.data
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
}
pub fn set_package_version(&mut self, version: &semver::Version) {
self.data["package"]["version"] = toml_edit::value(version.to_string());
}
pub fn version_is_inherited(&self) -> bool {
let Some(pkg) = self.data.get("package") else {
return false;
};
let Some(version_item) = pkg.get("version") else {
return false;
};
if let Some(tbl) = version_item.as_inline_table() {
return tbl
.get("workspace")
.and_then(|v| v.as_bool())
.unwrap_or(false);
}
if let Some(tbl) = version_item.as_table() {
return tbl
.get("workspace")
.and_then(|v| v.as_bool())
.unwrap_or(false);
}
false
}
pub fn get_workspace_version(&self) -> Option<semver::Version> {
let v_str = self
.data
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())?;
semver::Version::parse(v_str).ok()
}
pub fn set_workspace_version(&mut self, version: &semver::Version) {
self.data["workspace"]["package"]["version"] = toml_edit::value(version.to_string());
}
pub fn workspace_members(&self) -> Option<Vec<&str>> {
self.data
.get("workspace")
.and_then(|w| w.get("members"))
.and_then(|m| m.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_new_reads_valid_manifest() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"foo\"\nversion = \"1.2.3\"\n",
)
.unwrap();
let lm = LocalManifest::try_new(&manifest).unwrap();
assert_eq!(lm.package_name(), Some("foo"));
assert_eq!(lm.package_version(), Some("1.2.3"));
}
#[test]
fn version_is_inherited_detects_workspace_true() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"child\"\nversion.workspace = true\n",
)
.unwrap();
let lm = LocalManifest::try_new(&manifest).unwrap();
assert!(lm.version_is_inherited());
}
#[test]
fn version_is_inherited_false_for_string() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"foo\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let lm = LocalManifest::try_new(&manifest).unwrap();
assert!(!lm.version_is_inherited());
}
#[test]
fn set_package_version_and_write_round_trips() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"foo\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
let mut lm = LocalManifest::try_new(&manifest).unwrap();
lm.set_package_version(&semver::Version::new(0, 2, 0));
lm.write().unwrap();
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(content.contains("version = \"0.2.0\""));
assert!(content.contains("name = \"foo\""));
assert!(content.contains("edition = \"2024\""));
}
#[test]
fn package_name_returns_correct_name() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"my-crate\"\nversion = \"0.0.1\"\n",
)
.unwrap();
let lm = LocalManifest::try_new(&manifest).unwrap();
assert_eq!(lm.package_name(), Some("my-crate"));
}
#[test]
fn workspace_version_round_trip() {
let dir = tempfile::tempdir().unwrap();
let manifest = dir.path().join("Cargo.toml");
std::fs::write(
&manifest,
"[workspace.package]\nversion = \"1.0.0\"\n\n[workspace]\nmembers = [\"a\"]\n",
)
.unwrap();
let mut lm = LocalManifest::try_new(&manifest).unwrap();
assert_eq!(
lm.get_workspace_version(),
Some(semver::Version::new(1, 0, 0))
);
lm.set_workspace_version(&semver::Version::new(2, 0, 0));
lm.write().unwrap();
let content = std::fs::read_to_string(&manifest).unwrap();
assert!(content.contains("version = \"2.0.0\""));
}
}