cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! `IssueCollection` — owned, ordered set of issues returned by the
//! repository port. A newtype around `Vec<Issue>` that names the
//! concept and hides the underlying container so call sites use the
//! explicit surface (`iter`, `len`, `get`) instead of slice tricks.

use crate::domain::model::record_ref::IssueRef;

use super::Issue;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IssueCollection {
    issues: Vec<Issue>,
}

impl IssueCollection {
    pub fn new(issues: Vec<Issue>) -> Self {
        Self { issues }
    }

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

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

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

    pub fn get(&self, id: &IssueRef) -> Option<&Issue> {
        self.issues.iter().find(|i| &i.id == id)
    }

    pub fn into_vec(self) -> Vec<Issue> {
        self.issues
    }
}

impl From<Vec<Issue>> for IssueCollection {
    fn from(issues: Vec<Issue>) -> Self {
        Self::new(issues)
    }
}

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

impl IntoIterator for IssueCollection {
    type Item = Issue;
    type IntoIter = std::vec::IntoIter<Issue>;
    fn into_iter(self) -> Self::IntoIter {
        self.issues.into_iter()
    }
}

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

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

    pub fn issue_collection() -> impl Strategy<Value = IssueCollection> {
        proptest::collection::vec(issue(), 0..6).prop_map(IssueCollection::new)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::model::issue::test_fixtures::{feature, ir};
    use proptest::prelude::*;

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

    #[test]
    fn iter_yields_each_issue_in_insertion_order() {
        let a = feature("A").status("open").build(ir(1));
        let b = feature("B").status("open").build(ir(2));
        let c = IssueCollection::new(vec![a.clone(), b.clone()]);
        let collected: Vec<&_> = c.iter().collect();
        assert_eq!(collected, vec![&a, &b]);
    }

    #[test]
    fn get_returns_the_matching_issue() {
        let a = feature("A").status("open").build(ir(1));
        let b = feature("B").status("open").build(ir(2));
        let c = IssueCollection::new(vec![a.clone(), b.clone()]);
        assert_eq!(c.get(&a.id), Some(&a));
        assert_eq!(c.get(&b.id), Some(&b));
    }

    #[test]
    fn get_returns_none_for_an_unknown_id() {
        let a = feature("A").status("open").build(ir(1));
        let c = IssueCollection::new(vec![a]);
        assert_eq!(c.get(&ir(99)), None);
    }

    #[test]
    fn into_vec_returns_the_underlying_storage() {
        let a = feature("A").status("open").build(ir(1));
        let c = IssueCollection::new(vec![a.clone()]);
        assert_eq!(c.into_vec(), vec![a]);
    }

    #[test]
    fn from_iter_collects_into_a_collection() {
        let a = feature("A").status("open").build(ir(1));
        let b = feature("B").status("open").build(ir(2));
        let c: IssueCollection = vec![a.clone(), b.clone()].into_iter().collect();
        assert_eq!(c.len(), 2);
    }

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

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