cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Read-only decorators around the issue and decision-record repository
//! ports. Used when the binary runs in degraded mode (malformed or
//! outdated `cartulary.toml`): every read delegates to the wrapped
//! repository, every `save` is a silent no-op.
//!
//! Silent on purpose: the framing error printed by the CLI runner
//! already names the condition and the recovery; per-write errors
//! would only repeat the message.

use crate::domain::model::decision_record::{DecisionRecord, DecisionRecordCollection};
use crate::domain::model::issue::companion::{
    CompanionContent, CompanionIdentifier, IssueCompanions,
};
use crate::domain::model::issue::{Issue, IssueCollection};
use crate::domain::model::record_ref::{DecisionRecordRef, IssueRef};
use crate::domain::usecases::decision_record::DecisionRecordRepository;
use crate::domain::usecases::issue::IssueRepository;

pub struct ReadOnlyIssueRepo<R> {
    inner: R,
}

impl<R> ReadOnlyIssueRepo<R> {
    pub fn new(inner: R) -> Self {
        Self { inner }
    }
}

impl<R: IssueRepository> IssueRepository for ReadOnlyIssueRepo<R> {
    fn save(&self, _issue: &Issue) -> anyhow::Result<()> {
        Ok(())
    }

    fn list(&self) -> anyhow::Result<IssueCollection> {
        self.inner.list()
    }

    fn find_by_id(&self, id: &IssueRef) -> anyhow::Result<Option<Issue>> {
        self.inner.find_by_id(id)
    }

    fn issue_companions(&self, id: &IssueRef) -> anyhow::Result<IssueCompanions> {
        self.inner.issue_companions(id)
    }

    fn read_companion(
        &self,
        id: &IssueRef,
        identifier: &CompanionIdentifier,
    ) -> anyhow::Result<Option<CompanionContent>> {
        self.inner.read_companion(id, identifier)
    }

    fn configured_id_prefix(&self) -> Option<&str> {
        self.inner.configured_id_prefix()
    }
}

pub struct ReadOnlyDrRepo<R> {
    inner: R,
}

impl<R> ReadOnlyDrRepo<R> {
    pub fn new(inner: R) -> Self {
        Self { inner }
    }
}

impl<R: DecisionRecordRepository> DecisionRecordRepository for ReadOnlyDrRepo<R> {
    fn save(&self, _record: &DecisionRecord) -> anyhow::Result<()> {
        Ok(())
    }

    fn list(&self) -> anyhow::Result<DecisionRecordCollection> {
        self.inner.list()
    }

    fn find_by_id(&self, id: &DecisionRecordRef) -> anyhow::Result<Option<DecisionRecord>> {
        self.inner.find_by_id(id)
    }

    fn configured_id_prefix(&self) -> Option<&str> {
        self.inner.configured_id_prefix()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::model::decision_record::DrStatus;
    use crate::domain::model::issue::test_fixtures::{make_issue, st};
    use crate::domain::usecases::decision_record::test_support::make_record;
    use std::cell::Cell;

    // ── Issue stub ────────────────────────────────────────────────────────────

    struct StubIssueRepo {
        save_calls: Cell<usize>,
        list_calls: Cell<usize>,
        issues: Vec<Issue>,
    }

    impl IssueRepository for StubIssueRepo {
        fn save(&self, _issue: &Issue) -> anyhow::Result<()> {
            self.save_calls.set(self.save_calls.get() + 1);
            Ok(())
        }
        fn list(&self) -> anyhow::Result<IssueCollection> {
            self.list_calls.set(self.list_calls.get() + 1);
            Ok(IssueCollection::new(self.issues.clone()))
        }
        fn find_by_id(&self, _id: &IssueRef) -> anyhow::Result<Option<Issue>> {
            Ok(None)
        }
        fn issue_companions(&self, _id: &IssueRef) -> anyhow::Result<IssueCompanions> {
            Ok(IssueCompanions::new())
        }
        fn read_companion(
            &self,
            _id: &IssueRef,
            _identifier: &CompanionIdentifier,
        ) -> anyhow::Result<Option<CompanionContent>> {
            Ok(None)
        }
        fn configured_id_prefix(&self) -> Option<&str> {
            Some("ISSUE-")
        }
    }

    fn issue_stub(issues: Vec<Issue>) -> StubIssueRepo {
        StubIssueRepo {
            save_calls: Cell::new(0),
            list_calls: Cell::new(0),
            issues,
        }
    }

    #[test]
    fn save_on_decorator_does_not_reach_inner_repo() {
        let issue = make_issue(1, "t", st("open"));
        let ro = ReadOnlyIssueRepo::new(issue_stub(vec![]));
        ro.save(&issue).unwrap();
        ro.save(&issue).unwrap();
        assert_eq!(ro.inner.save_calls.get(), 0);
    }

    #[test]
    fn reads_delegate_to_inner_repo() {
        let issue = make_issue(1, "t", st("open"));
        let ro = ReadOnlyIssueRepo::new(issue_stub(vec![issue]));
        assert_eq!(ro.list().unwrap().len(), 1);
        assert_eq!(ro.inner.list_calls.get(), 1);
        assert_eq!(ro.configured_id_prefix(), Some("ISSUE-"));
    }

    // ── DR stub ───────────────────────────────────────────────────────────────

    struct StubDrRepo {
        save_calls: Cell<usize>,
        records: Vec<DecisionRecord>,
    }

    impl DecisionRecordRepository for StubDrRepo {
        fn save(&self, _record: &DecisionRecord) -> anyhow::Result<()> {
            self.save_calls.set(self.save_calls.get() + 1);
            Ok(())
        }
        fn list(&self) -> anyhow::Result<DecisionRecordCollection> {
            Ok(DecisionRecordCollection::new(self.records.clone()))
        }
        fn find_by_id(&self, _id: &DecisionRecordRef) -> anyhow::Result<Option<DecisionRecord>> {
            Ok(None)
        }
        fn configured_id_prefix(&self) -> Option<&str> {
            Some("ADR-")
        }
    }

    fn dr_stub(records: Vec<DecisionRecord>) -> StubDrRepo {
        StubDrRepo {
            save_calls: Cell::new(0),
            records,
        }
    }

    #[test]
    fn dr_save_on_decorator_is_silenced() {
        let dr = make_record(1, "t", DrStatus::Proposed);
        let ro = ReadOnlyDrRepo::new(dr_stub(vec![]));
        ro.save(&dr).unwrap();
        assert_eq!(ro.inner.save_calls.get(), 0);
    }

    #[test]
    fn dr_reads_delegate_to_inner_repo() {
        let dr = make_record(1, "t", DrStatus::Proposed);
        let ro = ReadOnlyDrRepo::new(dr_stub(vec![dr]));
        assert_eq!(ro.list().unwrap().len(), 1);
        assert_eq!(ro.configured_id_prefix(), Some("ADR-"));
    }
}