use std::path::Path;
use anyhow::Context;
fn toml_edit_value_as_f64(value: &toml_edit::Value) -> Option<f64> {
value.as_float().or_else(|| {
#[expect(
clippy::as_conversions,
clippy::cast_precision_loss,
reason = "integer from toml_edit is i64; fits exactly in f64 for values <= 2^53"
)]
value.as_integer().map(|i| i as f64)
})
}
pub(crate) fn set_facet(
doc: &mut toml_edit::DocumentMut,
table: &str,
fields: &[(&str, f64)],
) -> anyhow::Result<bool> {
let root = doc.as_table_mut();
match root.get_mut(table) {
None => {
let mut t = toml_edit::Table::new();
for &(k, v) in fields {
t.insert(k, toml_edit::value(v));
}
root.insert(table, toml_edit::Item::Table(t));
Ok(true)
}
Some(item) => {
let is_aot = item.is_array_of_tables();
let tbl = item.as_table_mut().with_context(|| {
if is_aot {
format!("{table}: expected a standard table, found an array-of-tables")
} else {
format!("{table}: expected a standard table, found a scalar or array-of-tables")
}
})?;
let mut changed = false;
for &(k, v) in fields {
let current_float = tbl
.get(k)
.and_then(toml_edit::Item::as_value)
.and_then(toml_edit_value_as_f64);
if current_float != Some(v) {
changed = true;
break;
}
}
if !changed {
return Ok(false);
}
for &(k, v) in fields {
tbl.insert(k, toml_edit::value(v));
}
Ok(true)
}
}
}
pub(crate) fn clear_facet(doc: &mut toml_edit::DocumentMut, table: &str) -> bool {
doc.as_table_mut().remove(table).is_some()
}
fn edit_in_place(
path: &Path,
f: impl FnOnce(&mut toml_edit::DocumentMut) -> anyhow::Result<bool>,
) -> anyhow::Result<bool> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("entity not found at {}", path.display()))?;
let mut doc = text
.parse::<toml_edit::DocumentMut>()
.with_context(|| format!("Failed to parse {}", path.display()))?;
let changed = f(&mut doc)?;
if changed {
crate::fsutil::write_atomic(path, doc.to_string().as_bytes())
.with_context(|| format!("Failed to write {}", path.display()))?;
}
Ok(changed)
}
pub(crate) fn apply_set(path: &Path, table: &str, fields: &[(&str, f64)]) -> anyhow::Result<bool> {
edit_in_place(path, |doc| set_facet(doc, table, fields))
}
pub(crate) fn apply_clear(path: &Path, table: &str) -> anyhow::Result<bool> {
edit_in_place(path, |doc| Ok(clear_facet(doc, table)))
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_doc() -> toml_edit::DocumentMut {
"".parse::<toml_edit::DocumentMut>().unwrap()
}
fn doc_from(s: &str) -> toml_edit::DocumentMut {
s.parse::<toml_edit::DocumentMut>().unwrap()
}
fn write_tmp(body: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("entity.toml");
std::fs::write(&path, body).unwrap();
(dir, path)
}
#[test]
fn vt1_set_allocates_absent_table() {
let mut doc = empty_doc();
let changed = set_facet(&mut doc, "estimate", &[("lower", 1.0), ("upper", 3.0)]).unwrap();
assert!(changed, "allocating a new table returns true");
let out = doc.to_string();
assert!(
out.contains("[estimate]"),
"missing [estimate] header in:\n{out}"
);
assert!(out.contains("lower = 1.0"), "missing lower in:\n{out}");
assert!(out.contains("upper = 3.0"), "missing upper in:\n{out}");
}
#[test]
fn vt2_set_overwrites_present() {
let mut doc = doc_from("[estimate]\nlower = 1\nupper = 3\n");
let changed = set_facet(&mut doc, "estimate", &[("lower", 2.0), ("upper", 4.0)]).unwrap();
assert!(changed, "overwriting returns true");
let out = doc.to_string();
assert!(out.contains("lower = 2.0"), "lower not updated:\n{out}");
assert!(out.contains("upper = 4.0"), "upper not updated:\n{out}");
}
#[test]
fn vt3_set_idempotent_noop() {
let mut doc = doc_from("[estimate]\nlower = 1.0\nupper = 3.0\n");
let changed = set_facet(&mut doc, "estimate", &[("lower", 1.0), ("upper", 3.0)]).unwrap();
assert!(!changed, "identical values → no-op (false)");
let out = doc.to_string();
assert!(out.contains("lower = 1.0"));
assert!(out.contains("upper = 3.0"));
}
#[test]
fn vt3_set_noop_preserves_integer_form() {
let mut doc = doc_from("[estimate]\nlower = 1\nupper = 3\n");
let changed = set_facet(&mut doc, "estimate", &[("lower", 1.0), ("upper", 3.0)]).unwrap();
assert!(!changed, "integer 1 == float 1.0 → no-op");
}
#[test]
fn vt4_clear_removes_table() {
let mut doc = doc_from("[estimate]\nlower = 1.0\nupper = 3.0\n");
let removed = clear_facet(&mut doc, "estimate");
assert!(removed, "table present → removed → true");
let out = doc.to_string();
assert!(
!out.contains("[estimate]"),
"estimate table should be gone:\n{out}"
);
}
#[test]
fn vt4_clear_absent_noop() {
let mut doc = empty_doc();
let removed = clear_facet(&mut doc, "estimate");
assert!(!removed, "table absent → false");
}
#[test]
fn vt5_golden_roundtrip_preserve() {
let body = concat!(
"# top comment\n",
"id = 1\n",
"slug = \"a\"\n",
"title = \"A\"\n",
"\n",
"[estimate]\n",
"lower = 1\n",
"upper = 3\n",
"\n",
"[relationships]\n",
"needs = [\"ISS-002\"]\n",
"after = []\n",
);
let mut doc = doc_from(body);
let changed = set_facet(&mut doc, "estimate", &[("lower", 2.0), ("upper", 5.0)]).unwrap();
assert!(changed);
let out = doc.to_string();
assert!(out.contains("# top comment"), "comment lost:\n{out}");
assert!(
out.contains("[relationships]"),
"relationships lost:\n{out}"
);
assert!(
out.contains("needs = [\"ISS-002\"]"),
"needs content lost:\n{out}"
);
assert!(out.contains("lower = 2.0"), "lower not updated:\n{out}");
assert!(out.contains("upper = 5.0"), "upper not updated:\n{out}");
let removed = clear_facet(&mut doc, "estimate");
assert!(removed);
let out2 = doc.to_string();
assert!(
!out2.contains("[estimate]"),
"estimate should be gone:\n{out2}"
);
assert!(
out2.contains("[relationships]"),
"relationships lost on clear:\n{out2}"
);
assert!(
out2.contains("# top comment"),
"comment lost on clear:\n{out2}"
);
}
#[test]
fn vt6_scalar_errors() {
let mut doc = doc_from("estimate = 7\n");
let err = set_facet(&mut doc, "estimate", &[("lower", 1.0), ("upper", 3.0)])
.expect_err("scalar should error");
let msg = format!("{err:#}").to_lowercase();
assert!(
msg.contains("expected a standard table"),
"scalar error message: {msg}"
);
assert!(
msg.contains("scalar") || msg.contains("found"),
"should identify scalar shape: {msg}"
);
assert_eq!(doc.to_string(), "estimate = 7\n", "doc untouched on error");
}
#[test]
fn vt6_array_of_tables_errors() {
let mut doc = doc_from("[[estimate]]\nlower = 1\nupper = 3\n");
let err = set_facet(&mut doc, "estimate", &[("lower", 2.0), ("upper", 4.0)])
.expect_err("array-of-tables should error");
let msg = format!("{err:#}").to_lowercase();
assert!(msg.contains("array-of-tables"), "AoT error message: {msg}");
assert!(
doc.to_string().contains("[[estimate]]"),
"doc untouched on error"
);
}
#[test]
fn vt7_forward_compat_preserves_unknown_estimate() {
let mut doc = doc_from("[estimate]\nlower = 1\nupper = 3\nhistory = \"old\"\n");
let changed = set_facet(&mut doc, "estimate", &[("lower", 2.0), ("upper", 4.0)]).unwrap();
assert!(changed);
let out = doc.to_string();
assert!(out.contains("lower = 2.0"), "lower not updated:\n{out}");
assert!(out.contains("upper = 4.0"), "upper not updated:\n{out}");
assert!(
out.contains("history = \"old\""),
"unknown sibling lost:\n{out}"
);
}
#[test]
fn vt7_forward_compat_preserves_unknown_value() {
let mut doc = doc_from("[value]\nvalue = 5\nhistory = \"old\"\n");
let changed = set_facet(&mut doc, "value", &[("value", 10.0)]).unwrap();
assert!(changed);
let out = doc.to_string();
assert!(out.contains("value = 10.0"), "value not updated:\n{out}");
assert!(
out.contains("history = \"old\""),
"unknown sibling lost:\n{out}"
);
}
#[test]
fn vt8_layering_exercise_all_public_fns() {
let (_dir, path) = write_tmp("id = 1\nslug = \"a\"\ntitle = \"A\"\n");
let changed = apply_set(&path, "estimate", &[("lower", 1.0), ("upper", 5.0)]).unwrap();
assert!(changed, "apply_set allocates");
let after_set = std::fs::read_to_string(&path).unwrap();
assert!(after_set.contains("[estimate]"));
let changed2 = apply_set(&path, "estimate", &[("lower", 1.0), ("upper", 5.0)]).unwrap();
assert!(!changed2, "apply_set idempotent");
let cleared = apply_clear(&path, "estimate").unwrap();
assert!(cleared, "apply_clear removes");
let after_clear = std::fs::read_to_string(&path).unwrap();
assert!(!after_clear.contains("[estimate]"));
let cleared2 = apply_clear(&path, "estimate").unwrap();
assert!(!cleared2, "apply_clear absent → false");
}
#[test]
fn edit_in_place_no_change_holds_mtime() {
let (_dir, path) = write_tmp("[estimate]\nlower = 1.0\nupper = 3.0\n");
let before_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
let changed = apply_set(&path, "estimate", &[("lower", 1.0), ("upper", 3.0)]).unwrap();
assert!(!changed, "no-op returns false");
assert_eq!(
std::fs::metadata(&path).unwrap().modified().unwrap(),
before_mtime,
"mtime held on no-op"
);
}
#[test]
fn apply_set_on_value_facet() {
let (_dir, path) = write_tmp("id = 1\nslug = \"a\"\ntitle = \"A\"\n");
let changed = apply_set(&path, "value", &[("value", 42.0)]).unwrap();
assert!(changed);
let out = std::fs::read_to_string(&path).unwrap();
assert!(out.contains("[value]"), "value table missing:\n{out}");
assert!(out.contains("value = 42.0"), "value field missing:\n{out}");
}
#[test]
fn set_facet_no_fields_is_valid() {
let mut doc = empty_doc();
let changed = set_facet(&mut doc, "estimate", &[]).unwrap();
assert!(changed, "allocating empty table returns true");
assert!(doc.to_string().contains("[estimate]"));
}
#[test]
fn set_facet_zero_fields_idempotent() {
let mut doc = doc_from("[estimate]\nlower = 1\nupper = 3\n");
let changed = set_facet(&mut doc, "estimate", &[]).unwrap();
assert!(!changed, "zero fields on present table → no-op");
}
#[test]
fn set_facet_partial_overwrite_preserves_other_managed() {
let mut doc = doc_from("[estimate]\nlower = 1.0\nupper = 3.0\n");
let changed = set_facet(&mut doc, "estimate", &[("lower", 5.0)]).unwrap();
assert!(changed);
let out = doc.to_string();
assert!(out.contains("lower = 5.0"), "lower not updated:\n{out}");
assert!(out.contains("upper = 3.0"), "upper lost:\n{out}");
}
}