use std::path::{Path, PathBuf};
use dbmd_core::parser::{read_file, write_file};
use dbmd_core::validate::codes;
use dbmd_core::Store;
use crate::cli::FormatArgs;
use crate::context::Context;
use crate::error::{CliError, CliResult, ExitCode};
pub fn run(ctx: &Context, args: &FormatArgs) -> CliResult {
let file = Path::new(&args.file);
let store = locate_store(file)?;
let rel = store_relative(&store, file);
if store.config.is_frozen(&rel) {
return Err(CliError::new(
ExitCode::Policy,
codes::POLICY_FROZEN_PAGE,
format!("`{}` is a frozen page; refusing to format", rel.display()),
)
.with_hint("remove it from DB.md ## Policies → ### Frozen pages to allow writes"));
}
let original = std::fs::read_to_string(file).map_err(CliError::from)?;
let (frontmatter, body) = read_file(file).map_err(|e| {
CliError::from(dbmd_core::Error::from(e))
.with_hint(format!("could not read `{}`", args.file))
})?;
write_file(file, &frontmatter, &body).map_err(|e| {
CliError::from(dbmd_core::Error::from(e))
.with_hint(format!("could not write `{}`", args.file))
})?;
let canonical = format!("---\n{}---\n{}", frontmatter.to_yaml(), body);
let changed = canonical != original;
if ctx.json {
let obj = serde_json::json!({
"file": rel.to_string_lossy(),
"changed": changed,
});
let mut s = serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{}".to_string());
s.push('\n');
print!("{s}");
} else if changed {
println!("formatted {}", rel.display());
} else {
println!("{} already canonical", rel.display());
}
Ok(())
}
fn locate_store(file: &Path) -> Result<Store, CliError> {
let start = file.parent().unwrap_or(Path::new("."));
let start = std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf());
let mut outermost: Option<&Path> = None;
let mut dir: Option<&Path> = Some(start.as_path());
while let Some(d) = dir {
if Store::is_db_md_store(d) {
outermost = Some(d);
}
dir = d.parent();
}
match outermost {
Some(root) => Store::open_strict(root).map_err(CliError::from),
None => Store::open_strict(&start).map_err(CliError::from),
}
}
fn store_relative(store: &Store, file: &Path) -> PathBuf {
let canonical_file = std::fs::canonicalize(file).unwrap_or_else(|_| file.to_path_buf());
let canonical_root = std::fs::canonicalize(&store.root).unwrap_or_else(|_| store.root.clone());
canonical_file
.strip_prefix(&canonical_root)
.map(|p| p.to_path_buf())
.unwrap_or_else(|_| file.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locate_store_anchors_to_outermost_store_past_interior_db_md() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path();
std::fs::write(
root.join("DB.md"),
"---\ntype: db-md\nscope: company\nowner: T\n---\n\n# Store\n\n## Policies\n\n### Frozen pages\n- sources/docs/contract.md\n",
)
.unwrap();
let docs = root.join("sources").join("docs");
std::fs::create_dir_all(&docs).unwrap();
std::fs::write(
docs.join("contract.md"),
"---\ntype: pdf-source\nsummary: A frozen contract page\n---\n# Contract\n",
)
.unwrap();
std::fs::write(
docs.join("DB.md"),
"---\ntype: pdf-source\nsummary: An ingested doc named DB.md\n---\n# Doc\n",
)
.unwrap();
let contract = docs.join("contract.md");
let store = locate_store(&contract).expect("store must resolve");
assert_eq!(
std::fs::canonicalize(&store.root).unwrap(),
std::fs::canonicalize(root).unwrap(),
"interior DB.md must not become the store root"
);
let rel = store_relative(&store, &contract);
assert_eq!(rel, PathBuf::from("sources/docs/contract.md"));
assert!(
store.config.is_frozen(&rel),
"outermost store's frozen-page policy must apply to the contract"
);
}
}