cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! `DecisionRecordCollection` — owned, ordered set of decision records
//! returned by the repository port. A newtype around
//! `Vec<DecisionRecord>` that names the concept and keeps the surface
//! explicit (`iter`, `len`, `get`).

use crate::domain::model::record_ref::DecisionRecordRef;

use super::DecisionRecord;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DecisionRecordCollection {
    records: Vec<DecisionRecord>,
}

impl DecisionRecordCollection {
    pub fn new(records: Vec<DecisionRecord>) -> Self {
        Self { records }
    }

    pub fn iter(&self) -> std::slice::Iter<'_, DecisionRecord> {
        self.records.iter()
    }

    pub fn len(&self) -> usize {
        self.records.len()
    }

    pub fn is_empty(&self) -> bool {
        self.records.is_empty()
    }

    pub fn get(&self, id: &DecisionRecordRef) -> Option<&DecisionRecord> {
        self.records.iter().find(|r| &r.id == id)
    }

    pub fn into_vec(self) -> Vec<DecisionRecord> {
        self.records
    }
}

impl From<Vec<DecisionRecord>> for DecisionRecordCollection {
    fn from(records: Vec<DecisionRecord>) -> Self {
        Self::new(records)
    }
}

impl FromIterator<DecisionRecord> for DecisionRecordCollection {
    fn from_iter<I: IntoIterator<Item = DecisionRecord>>(iter: I) -> Self {
        Self::new(iter.into_iter().collect())
    }
}

impl IntoIterator for DecisionRecordCollection {
    type Item = DecisionRecord;
    type IntoIter = std::vec::IntoIter<DecisionRecord>;
    fn into_iter(self) -> Self::IntoIter {
        self.records.into_iter()
    }
}

impl<'a> IntoIterator for &'a DecisionRecordCollection {
    type Item = &'a DecisionRecord;
    type IntoIter = std::slice::Iter<'a, DecisionRecord>;
    fn into_iter(self) -> Self::IntoIter {
        self.records.iter()
    }
}

#[cfg(test)]
pub mod strategy {
    use super::DecisionRecordCollection;
    use crate::domain::model::decision_record::strategy::decision_record;
    use proptest::prelude::*;

    pub fn decision_record_collection() -> impl Strategy<Value = DecisionRecordCollection> {
        proptest::collection::vec(decision_record(), 0..6).prop_map(DecisionRecordCollection::new)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::model::body::Body;
    use crate::domain::model::decision_record::record_link::RecordLinks;
    use crate::domain::model::decision_record::DrStatus;
    use crate::domain::model::entry_locator::EntryLocator;
    use crate::domain::model::entry_origin::EntryOrigin;
    use crate::domain::model::event::EventLog;
    use crate::domain::model::record_kind::RecordKind;
    use crate::domain::model::relates::Relates;
    use crate::domain::model::tag_list::TagList;
    use crate::domain::model::temporal::iso_date::IsoDate;
    use crate::domain::model::title::Title;
    use proptest::prelude::*;

    fn record(n: u64, title: &str) -> DecisionRecord {
        DecisionRecord {
            id: DecisionRecordRef::new(format!("ADR-{n:04}")).unwrap(),
            kind: RecordKind::new("adr").unwrap(),
            title: Title::new(title).unwrap(),
            description: None,
            status: DrStatus::Proposed,
            date: IsoDate::new("2026-01-01").unwrap(),
            tags: TagList::new(),
            aliases: Vec::new(),
            content: Body::default(),
            events: EventLog::new(),
            links: RecordLinks::new(),
            relates: Relates::default(),
            origin: EntryOrigin::Local,
            location: EntryLocator::default(),
        }
    }

    #[test]
    fn empty_collection_is_empty() {
        let c = DecisionRecordCollection::default();
        assert_eq!(c.len(), 0);
        assert!(c.is_empty());
    }

    #[test]
    fn iter_yields_each_record_in_insertion_order() {
        let a = record(1, "A");
        let b = record(2, "B");
        let c = DecisionRecordCollection::new(vec![a.clone(), b.clone()]);
        let collected: Vec<&DecisionRecord> = c.iter().collect();
        assert_eq!(collected, vec![&a, &b]);
    }

    #[test]
    fn get_returns_the_matching_record() {
        let a = record(1, "A");
        let c = DecisionRecordCollection::new(vec![a.clone()]);
        assert_eq!(c.get(&a.id), Some(&a));
    }

    #[test]
    fn into_vec_returns_the_underlying_storage() {
        let a = record(1, "A");
        let c = DecisionRecordCollection::new(vec![a.clone()]);
        assert_eq!(c.into_vec(), vec![a]);
    }

    proptest! {
        #[test]
        fn len_matches_iter_count(c in strategy::decision_record_collection()) {
            prop_assert_eq!(c.len(), c.iter().count());
        }

        #[test]
        fn round_trip_through_vec(c in strategy::decision_record_collection()) {
            let v = c.clone().into_vec();
            prop_assert_eq!(DecisionRecordCollection::new(v), c);
        }
    }
}