use std::path::{Path, PathBuf};
use crate::change::ChangeSet;
use crate::config::WorkspaceConfig;
use crate::workspace::Workspace;
use prov_graph::error::Result;
use prov_store::fs::Storage;
use prov_store::index::IndexStore;
use super::AboutContext;
impl<FS: Storage, IdP, Ix: IndexStore> Workspace<FS, IdP, Ix> {
pub async fn write_about(
&self,
root_doc: &Path,
config: &WorkspaceConfig,
ctx: &AboutContext,
) -> Result<PathBuf> {
let page = super::generate(config, self.relations(), ctx)?;
let path = match self.about_path(root_doc).await? {
Some(existing) => existing,
None => PathBuf::from(default_about_name(config.content_format)),
};
let mut cs = ChangeSet::new();
cs.write(&path, page);
if self.about_path(root_doc).await?.is_none()
&& let Some(pointer) = self.relations().about_relation()
{
let (text, doc) = self.load(root_doc).await?;
let updated = prov_store::edit::set_in_text(
&text,
doc.carrier,
pointer,
prov_store::edit::infer_scalar(&path.to_string_lossy()),
)?;
cs.write(root_doc, updated);
}
cs.apply(self.fs(), self.root()).await?;
Ok(path)
}
pub async fn remove_about(&self, root_doc: &Path) -> Result<Option<PathBuf>> {
let Some(path) = self.about_path(root_doc).await? else {
return Ok(None);
};
let mut cs = ChangeSet::new();
if self.exists(&path).await? {
cs.remove(&path);
}
if let Some(pointer) = self.relations().about_relation() {
let (text, doc) = self.load(root_doc).await?;
let updated = prov_store::edit::unset_in_text(&text, doc.carrier, pointer)?;
cs.write(root_doc, updated);
}
cs.apply(self.fs(), self.root()).await?;
Ok(Some(path))
}
pub async fn about_diff(
&self,
root_doc: &Path,
config: &WorkspaceConfig,
ctx: &AboutContext,
) -> Result<Option<AboutDiff>> {
let expected = super::generate(config, self.relations(), ctx)?;
let Some(path) = self.about_path(root_doc).await? else {
return Ok(Some(AboutDiff {
path: PathBuf::from(default_about_name(config.content_format)),
expected,
actual: None,
}));
};
if !self.exists(&path).await? {
return Ok(Some(AboutDiff {
path,
expected,
actual: None,
}));
}
let actual = self.read_text(&path).await?;
if super::same_body(&actual, &expected, config.content_format) {
return Ok(None);
}
Ok(Some(AboutDiff {
path,
expected,
actual: Some(actual),
}))
}
pub async fn check_about(
&self,
root_doc: &Path,
config: &WorkspaceConfig,
ctx: &AboutContext,
) -> Result<Option<crate::validate::Finding>> {
let declared = self.about_path(root_doc).await?.is_some();
if !super::enabled(config) && !declared {
return Ok(None);
}
Ok(self.about_diff(root_doc, config, ctx).await?.map(|diff| {
crate::validate::Finding::AboutStale {
path: diff.path,
missing: diff.actual.is_none(),
expected: diff.expected,
}
}))
}
}
pub fn default_about_name(format: prov_graph::content::ContentFormat) -> String {
format!("about.{}", format.extension())
}
#[derive(Debug, Clone)]
pub struct AboutDiff {
pub path: PathBuf,
pub expected: String,
pub actual: Option<String>,
}
#[cfg(all(test, feature = "yaml"))]
mod tests {
use super::*;
use crate::config::WorkspaceConfig;
use crate::remedy::Fix;
use crate::validate::Finding;
use prov_graph::exec::block_on;
use prov_graph::fs::StdFs;
fn write(dir: &Path, rel: &str, text: &str) {
let p = dir.join(rel);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, text).unwrap();
}
fn tempdir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("prov-about-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn fixture(tag: &str) -> (std::path::PathBuf, WorkspaceConfig, AboutContext) {
let dir = tempdir(tag);
write(
&dir,
"index.md",
"---\ntitle: T\nabout: about.md\n---\nbody\n",
);
let config = WorkspaceConfig::default();
let ctx = AboutContext::new("index.md", "0.0.0");
(dir, config, ctx)
}
#[test]
fn a_current_page_is_not_a_finding() {
let (dir, config, ctx) = fixture("about-current");
let ws = Workspace::builder(StdFs).root(&dir).build();
block_on(ws.write_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
let finding =
block_on(ws.check_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
assert!(finding.is_none(), "{finding:?}");
}
#[test]
fn a_missing_page_the_pointer_promises_is_a_finding_but_not_a_broken_link() {
let (dir, config, ctx) = fixture("about-missing");
let ws = Workspace::builder(StdFs).root(&dir).build();
let finding =
block_on(ws.check_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
assert!(matches!(
finding,
Some(Finding::AboutStale { missing: true, .. })
));
let findings = block_on(ws.check("index.md")).unwrap();
assert!(
!findings
.iter()
.any(|f| matches!(f, Finding::BrokenLink { target, .. } if target == "about.md")),
"{findings:?}"
);
}
#[test]
fn a_hand_edited_page_is_stale_and_the_fix_restores_it() {
let (dir, config, ctx) = fixture("about-edited");
let mut ws = Workspace::builder(StdFs).root(&dir).build();
let path =
block_on(ws.write_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
let generated = std::fs::read_to_string(dir.join(&path)).unwrap();
std::fs::write(
dir.join(&path),
generated.replace("is the root", "is definitely the root"),
)
.unwrap();
let finding = block_on(ws.check_about(std::path::Path::new("index.md"), &config, &ctx))
.unwrap()
.expect("stale");
assert!(matches!(
finding,
Finding::AboutStale { missing: false, .. }
));
let fix = block_on(ws.suggest_fix(&finding)).unwrap().expect("a fix");
assert!(matches!(fix, Fix::RegenerateAbout { .. }));
block_on(ws.apply_fix(&fix)).unwrap();
assert_eq!(std::fs::read_to_string(dir.join(&path)).unwrap(), generated);
}
#[test]
fn a_version_bump_in_the_byline_is_not_staleness() {
let (dir, config, ctx) = fixture("about-version");
let ws = Workspace::builder(StdFs).root(&dir).build();
let path =
block_on(ws.write_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
let page = std::fs::read_to_string(dir.join(&path)).unwrap();
std::fs::write(
dir.join(&path),
page.replace("generated_by: prov 0.0.0", "generated_by: prov 99.0.0"),
)
.unwrap();
let finding =
block_on(ws.check_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
assert!(
finding.is_none(),
"a stale byline is not a stale page: {finding:?}"
);
}
#[test]
fn a_prov_upgrade_between_generation_and_check_is_not_staleness() {
let (dir, config, old_ctx) = fixture("about-upgrade");
let ws = Workspace::builder(StdFs).root(&dir).build();
block_on(ws.write_about(std::path::Path::new("index.md"), &config, &old_ctx)).unwrap();
let new_ctx = AboutContext::new("index.md", "99.0.0");
let finding =
block_on(ws.check_about(std::path::Path::new("index.md"), &config, &new_ctx)).unwrap();
assert!(
finding.is_none(),
"a page generated under one prov version must read as current under \
another: {finding:?}"
);
}
#[test]
fn a_workspace_that_asked_for_no_page_is_silent() {
let dir = tempdir("about-off");
write(&dir, "index.md", "---\ntitle: T\n---\nbody\n");
let config = WorkspaceConfig {
about: crate::config::About::Off,
..WorkspaceConfig::default()
};
let ctx = AboutContext::new("index.md", "0.0.0");
let ws = Workspace::builder(StdFs).root(&dir).build();
let finding =
block_on(ws.check_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
assert!(finding.is_none(), "{finding:?}");
}
#[test]
fn the_derived_page_is_never_parked_in_the_history_store() {
let (dir, config, ctx) = fixture("about-not-captured");
write(
&dir,
"index.md",
"---\ntitle: T\nabout: about.md\n---\nbody\n",
);
let ws = Workspace::builder(StdFs).root(&dir).build();
block_on(ws.write_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
let set = block_on(ws.history_capture_set(std::path::Path::new("index.md"))).unwrap();
assert!(
!set.iter().any(|p| p == std::path::Path::new("about.md")),
"the derived page must stay out of the capture set: {set:?}"
);
assert!(
set.iter().any(|p| p == std::path::Path::new("index.md")),
"{set:?}"
);
}
#[test]
fn a_pointer_left_behind_is_still_checked_even_with_the_axis_off() {
let (dir, _, ctx) = fixture("about-off-but-pointed");
let config = WorkspaceConfig {
about: crate::config::About::Off,
..WorkspaceConfig::default()
};
let ws = Workspace::builder(StdFs).root(&dir).build();
let finding =
block_on(ws.check_about(std::path::Path::new("index.md"), &config, &ctx)).unwrap();
assert!(matches!(
finding,
Some(Finding::AboutStale { missing: true, .. })
));
}
}