cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Pure canonicalisers: build the exact Markdown bytes that
//! [`FsIssueRepository::save`] / [`FsDecisionRecordRepository::save`]
//! would write for an in-memory model. No filesystem access.
//!
//! Shared with the [`fmt`](crate::domain::usecases::fmt) use case so
//! current bytes on disk can be compared against the canonical form
//! without re-saving.
//!
//! [`FsIssueRepository::save`]: super::issue_repository::FsIssueRepository::save
//! [`FsDecisionRecordRepository::save`]: super::decision_record_repository::FsDecisionRecordRepository::save

use crate::domain::model::decision_record::DecisionRecord;
use crate::domain::model::issue::Issue;

use super::frontmatter_writer::{write_aliases, write_links, write_relates, write_tags};

pub fn issue_canonical(issue: &Issue) -> anyhow::Result<String> {
    let mut frontmatter = format!(
        "---\nid: {}\ntitle: \"{}\"\n",
        issue.id,
        issue.title.as_str(),
    );
    if let Some(desc) = &issue.description {
        frontmatter.push_str(&format!("description: \"{desc}\"\n"));
    }
    frontmatter.push_str(&format!(
        "status: {}\ndate: {}\n",
        issue.status.as_str(),
        issue.date
    ));

    if let Some(a) = &issue.assignee {
        frontmatter.push_str(&format!("assignee: {}\n", a.as_str()));
    }
    if let Some(d) = &issue.due_date {
        frontmatter.push_str(&format!("due_date: {d}\n"));
    }
    if !issue.tracker.is_local() {
        frontmatter.push_str(&format!("tracker: {}\n", issue.tracker));
    }

    write_tags(
        &mut frontmatter,
        issue.tags.iter().map(|t| t.as_str()),
        !issue.tags.is_empty(),
    );
    write_aliases(&mut frontmatter, issue.aliases.iter().map(String::as_str));
    write_links(
        &mut frontmatter,
        issue
            .links
            .iter()
            .map(|l| (l.target.as_entity_ref(), l.relationship.as_str())),
        !issue.links.is_empty(),
    );
    write_relates(&mut frontmatter, issue.relates().iter());

    frontmatter.push_str("---\n\n");

    let mut content = frontmatter + issue.content.as_str();
    if !content.ends_with('\n') {
        content.push('\n');
    }
    Ok(content)
}

pub fn decision_record_canonical(record: &DecisionRecord) -> anyhow::Result<String> {
    let mut frontmatter = format!(
        "---\nid: {}\ntitle: \"{}\"\n",
        record.id,
        record.title.as_str(),
    );
    if let Some(desc) = &record.description {
        frontmatter.push_str(&format!("description: \"{desc}\"\n"));
    }
    frontmatter.push_str(&format!(
        "status: {}\ndate: {}\n",
        record.status.as_str(),
        record.date
    ));

    write_tags(
        &mut frontmatter,
        record.tags.iter().map(|t| t.as_str()),
        !record.tags.is_empty(),
    );
    write_aliases(&mut frontmatter, record.aliases.iter().map(String::as_str));
    write_links(
        &mut frontmatter,
        record
            .links
            .iter()
            .map(|l| (l.target.as_entity_ref(), l.relationship.as_str())),
        !record.links.is_empty(),
    );
    write_relates(&mut frontmatter, record.relates().iter());

    frontmatter.push_str("---\n\n");
    frontmatter.push_str(record.content.as_str());
    if !frontmatter.ends_with('\n') {
        frontmatter.push('\n');
    }
    Ok(frontmatter)
}