cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use crate::domain::model::body::Body;
use crate::domain::model::decision_record::DecisionRecordEdit;
use crate::domain::model::record_ref::DecisionRecordRef;
use crate::domain::usecases::decision_record::DecisionRecordRepository;
use crate::domain::usecases::edit::dr::commit_dr_edits;

/// Outcome of an `edit_decision_record_body` call.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditBodyOutcome {
    /// The body was changed and saved.
    Updated,
    /// The body was unchanged — no write was performed.
    NoOp,
}

/// Replace the body of an existing decision record.
///
/// Body edits are not tracked in the event log — use git history for that.
/// Returns `Err` if the record is not found.
pub fn edit_decision_record_body(
    repo: &dyn DecisionRecordRepository,
    id: &DecisionRecordRef,
    new_body: Body,
) -> anyhow::Result<EditBodyOutcome> {
    let record = repo
        .find_by_id(id)?
        .ok_or_else(|| anyhow::anyhow!("record {id} not found"))?;
    if record.content == new_body {
        return Ok(EditBodyOutcome::NoOp);
    }
    commit_dr_edits(
        repo,
        vec![DecisionRecordEdit::SetBody {
            record: id.clone(),
            body: new_body,
        }],
    )?;
    Ok(EditBodyOutcome::Updated)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::usecases::decision_record::tests::{
        adr, FakeDecisionRecordRepository, RecordFixture,
    };

    #[test]
    fn changing_the_body_saves_the_new_content() {
        scenario()
            .given(adr("Use Rust").with_id("ADR-0001"))
            .when_edit_body("ADR-0001", "New body content.")
            .then_saved_body("New body content.")
            .then_updated();
    }

    #[test]
    fn submitting_the_same_body_is_a_noop() {
        scenario()
            .given(adr("Use Rust").with_id("ADR-0001").with_body("Same body."))
            .when_edit_body("ADR-0001", "Same body.")
            .then_noop();
    }

    #[test]
    fn editing_the_body_of_an_unknown_record_returns_an_error() {
        scenario()
            .when_edit_body("ADR-0099", "body")
            .then_err_contains("not found");
    }

    fn scenario() -> Scenario {
        Scenario {
            repo: FakeDecisionRecordRepository::with_records(vec![]),
        }
    }

    struct Scenario {
        repo: FakeDecisionRecordRepository,
    }

    impl Scenario {
        fn given(mut self, fixture: RecordFixture) -> Self {
            let raw = fixture
                .id
                .as_deref()
                .expect("given() requires an explicit id — use .with_id()")
                .to_string();
            let numeric = DecisionRecordRef::new(&raw).unwrap();
            self.repo.push_record(fixture.build(numeric));
            self
        }

        fn when_edit_body(self, id: &str, new_body: &str) -> EditOutcome {
            let record_ref = DecisionRecordRef::new(id).unwrap();
            let result = edit_decision_record_body(&self.repo, &record_ref, Body::new(new_body));
            EditOutcome {
                repo: self.repo,
                result,
            }
        }
    }

    struct EditOutcome {
        repo: FakeDecisionRecordRepository,
        result: anyhow::Result<EditBodyOutcome>,
    }

    impl EditOutcome {
        fn then_saved_body(self, expected: &str) -> Self {
            let saved = self.repo.last_saved().expect("expected a save, got none");
            assert_eq!(saved.content, Body::new(expected));
            self
        }

        fn then_updated(self) -> Self {
            assert_eq!(
                self.result.as_ref().expect("expected Ok, got Err"),
                &EditBodyOutcome::Updated
            );
            self
        }

        fn then_noop(self) -> Self {
            assert_eq!(
                self.result.as_ref().expect("expected Ok, got Err"),
                &EditBodyOutcome::NoOp
            );
            assert!(self.repo.last_saved().is_none(), "expected nothing saved");
            self
        }

        fn then_err_contains(self, substring: &str) {
            let msg = self.result.expect_err("expected Err, got Ok").to_string();
            assert!(
                msg.contains(substring),
                "expected {substring:?}, got {msg:?}"
            );
        }
    }
}