#![deny(unsafe_code)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![allow(clippy::missing_errors_doc)]
pub mod cache;
pub mod entity;
pub mod nulid_gen;
pub mod output;
pub mod parser;
pub mod registry;
pub mod relationship;
pub mod timeline;
pub mod verifier;
pub mod writeback;
use crate::entity::Entity;
use crate::parser::{ParseError, ParsedCase, SectionKind};
use crate::relationship::Rel;
pub fn parse_full(
content: &str,
reg: Option<®istry::EntityRegistry>,
) -> Result<(ParsedCase, Vec<Entity>, Vec<Rel>), Vec<ParseError>> {
let case = parser::parse(content)?;
let mut errors = Vec::new();
let mut all_entities = Vec::new();
for section in &case.sections {
if section.kind == SectionKind::Events {
let entities =
entity::parse_entities(§ion.body, section.kind, section.line, &mut errors);
all_entities.extend(entities);
}
}
let mut entity_names: Vec<&str> = all_entities.iter().map(|e| e.name.as_str()).collect();
if let Some(registry) = reg {
for name in registry.names() {
if !entity_names.contains(&name) {
entity_names.push(name);
}
}
}
let event_names: Vec<&str> = all_entities
.iter()
.filter(|e| e.label == entity::Label::PublicRecord)
.map(|e| e.name.as_str())
.collect();
let mut all_rels = Vec::new();
for section in &case.sections {
if section.kind == SectionKind::Relationships {
let rels = relationship::parse_relationships(
§ion.body,
section.line,
&entity_names,
&case.sources,
&mut errors,
);
all_rels.extend(rels);
}
}
for section in &case.sections {
if section.kind == SectionKind::Timeline {
let rels =
timeline::parse_timeline(§ion.body, section.line, &event_names, &mut errors);
all_rels.extend(rels);
}
}
if errors.is_empty() {
Ok((case, all_entities, all_rels))
} else {
Err(errors)
}
}
pub fn collect_referenced_registry_entities(
rels: &[Rel],
inline_entities: &[Entity],
reg: ®istry::EntityRegistry,
) -> Vec<Entity> {
let inline_names: Vec<&str> = inline_entities.iter().map(|e| e.name.as_str()).collect();
let mut referenced = Vec::new();
let mut seen_names: Vec<String> = Vec::new();
for rel in rels {
for name in [&rel.source_name, &rel.target_name] {
if !inline_names.contains(&name.as_str())
&& !seen_names.contains(name)
&& let Some(entry) = reg.get_by_name(name)
{
referenced.push(entry.entity.clone());
seen_names.push(name.clone());
}
}
}
referenced
}
pub fn build_case_output(
path: &str,
reg: ®istry::EntityRegistry,
) -> Result<output::CaseOutput, i32> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("{path}: error reading file: {e}");
return Err(2);
}
};
let (case, entities, rels) = match parse_full(&content, Some(reg)) {
Ok(result) => result,
Err(errors) => {
for err in &errors {
eprintln!("{path}:{err}");
}
return Err(1);
}
};
let referenced_entities = collect_referenced_registry_entities(&rels, &entities, reg);
let build_result = match output::build_output(
&case.id,
&case.title,
&case.summary,
&case.sources,
&entities,
&rels,
&referenced_entities,
) {
Ok(out) => out,
Err(errors) => {
for err in &errors {
eprintln!("{path}:{err}");
}
return Err(1);
}
};
let case_output = build_result.output;
if !build_result.case_pending.is_empty() {
let mut pending = build_result.case_pending;
if let Some(modified) = writeback::apply_writebacks(&content, &mut pending) {
if let Err(e) = writeback::write_file(std::path::Path::new(path), &modified) {
eprintln!("{e}");
return Err(2);
}
let count = pending.len();
eprintln!("{path}: wrote {count} generated ID(s) back to file");
}
}
if let Some(code) = writeback_registry_entities(&build_result.registry_pending, reg) {
return Err(code);
}
eprintln!(
"{path}: built ({} nodes, {} relationships)",
case_output.nodes.len(),
case_output.relationships.len()
);
Ok(case_output)
}
fn writeback_registry_entities(
pending: &[(String, writeback::PendingId)],
reg: ®istry::EntityRegistry,
) -> Option<i32> {
for (entity_name, pending_id) in pending {
let Some(entry) = reg.get_by_name(entity_name) else {
continue;
};
let entity_path = &entry.path;
let entity_content = match std::fs::read_to_string(entity_path) {
Ok(c) => c,
Err(e) => {
eprintln!("{}: error reading file: {e}", entity_path.display());
return Some(2);
}
};
let fm_end = writeback::find_front_matter_end(&entity_content);
let mut ids = vec![writeback::PendingId {
line: fm_end.unwrap_or(2),
id: pending_id.id.clone(),
kind: writeback::WriteBackKind::EntityFrontMatter,
}];
if let Some(modified) = writeback::apply_writebacks(&entity_content, &mut ids) {
if let Err(e) = writeback::write_file(entity_path, &modified) {
eprintln!("{e}");
return Some(2);
}
eprintln!("{}: wrote generated ID back to file", entity_path.display());
}
}
None
}
pub fn resolve_content_root(path: Option<&str>, root: Option<&str>) -> std::path::PathBuf {
if let Some(r) = root {
return std::path::PathBuf::from(r);
}
if let Some(p) = path {
let p = std::path::Path::new(p);
if p.is_file() {
if let Some(parent) = p.parent() {
for ancestor in parent.ancestors() {
if ancestor.join("cases").is_dir()
|| ancestor.join("actors").is_dir()
|| ancestor.join("institutions").is_dir()
{
return ancestor.to_path_buf();
}
}
return parent.to_path_buf();
}
} else if p.is_dir() {
return p.to_path_buf();
}
}
std::path::PathBuf::from(".")
}
pub fn load_registry(content_root: &std::path::Path) -> Result<registry::EntityRegistry, i32> {
match registry::EntityRegistry::load(content_root) {
Ok(reg) => Ok(reg),
Err(errors) => {
for err in &errors {
eprintln!("registry: {err}");
}
Err(1)
}
}
}
pub fn resolve_case_files(
path: Option<&str>,
content_root: &std::path::Path,
) -> Result<Vec<String>, i32> {
if let Some(p) = path {
let p_path = std::path::Path::new(p);
if p_path.is_file() {
return Ok(vec![p.to_string()]);
}
if !p_path.is_dir() {
eprintln!("{p}: not a file or directory");
return Err(2);
}
}
let cases_dir = content_root.join("cases");
if !cases_dir.is_dir() {
return Ok(Vec::new());
}
let mut files = Vec::new();
discover_md_files(&cases_dir, &mut files, 0);
files.sort();
Ok(files)
}
fn discover_md_files(dir: &std::path::Path, files: &mut Vec<String>, depth: usize) {
const MAX_DEPTH: usize = 3;
if depth > MAX_DEPTH {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
let mut entries: Vec<_> = entries.filter_map(Result::ok).collect();
entries.sort_by_key(std::fs::DirEntry::file_name);
for entry in entries {
let path = entry.path();
if path.is_dir() {
discover_md_files(&path, files, depth + 1);
} else if path.extension().and_then(|e| e.to_str()) == Some("md")
&& let Some(s) = path.to_str()
{
files.push(s.to_string());
}
}
}