cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! `IssueView<'a>` — borrowed, ordered view over a subset of issues.
//!
//! A view is the standard input to read-only computations on issues
//! (stats, rollups, tree shaping). It carries `&Issue` references into
//! a backing [`IssueCollection`] or slice, so filtering and chaining
//! never clone the issues themselves.

use super::value::IssueFilter;
use super::{Issue, IssueCollection};

#[derive(Debug, Clone)]
pub struct IssueView<'a> {
    issues: Vec<&'a Issue>,
}

impl<'a> IssueView<'a> {
    pub fn from_refs(issues: Vec<&'a Issue>) -> Self {
        Self { issues }
    }

    pub fn from_slice(issues: &'a [Issue]) -> Self {
        Self {
            issues: issues.iter().collect(),
        }
    }

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

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

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

    /// Narrow the view to issues satisfying `predicate`. The result
    /// borrows from the same backing storage; chaining is allocation-
    /// free apart from the new pointer vec.
    pub fn filter<F>(&self, mut predicate: F) -> IssueView<'a>
    where
        F: FnMut(&Issue) -> bool,
    {
        IssueView {
            issues: self
                .issues
                .iter()
                .copied()
                .filter(|i| predicate(i))
                .collect(),
        }
    }

    /// Narrow the view to issues matching `filter`. Wraps the existing
    /// `Issue::matches` semantics so callers stop reconstructing the
    /// predicate inline.
    pub fn matching(&self, filter: &IssueFilter<'_>) -> IssueView<'a> {
        self.filter(|i| i.matches(filter))
    }
}

impl IssueCollection {
    /// Borrow every issue as an [`IssueView`].
    pub fn view(&self) -> IssueView<'_> {
        IssueView::from_refs(self.iter().collect())
    }
}

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

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

    fn corpus() -> Vec<Issue> {
        vec![
            feature("A").status("open").build(ir(1)),
            defect("B").status("closed").build(ir(2)),
            feature("C").status("open").build(ir(3)),
        ]
    }

    #[test]
    fn from_slice_yields_every_issue() {
        let issues = corpus();
        let view = IssueView::from_slice(&issues);
        assert_eq!(view.len(), 3);
        let ids: Vec<_> = view.iter().map(|i| i.id.clone()).collect();
        assert_eq!(ids, vec![ir(1), ir(2), ir(3)]);
    }

    #[test]
    fn collection_view_yields_every_issue() {
        let collection = IssueCollection::new(corpus());
        let view = collection.view();
        assert_eq!(view.len(), 3);
    }

    #[test]
    fn filter_narrows_the_view() {
        let issues = corpus();
        let view = IssueView::from_slice(&issues);
        let open = view.filter(|i| i.matches(&IssueFilter::default()));
        assert_eq!(open.len(), 3);
        let only_a = view.filter(|i| i.id == ir(1));
        assert_eq!(only_a.len(), 1);
    }

    #[test]
    fn matching_applies_an_issue_filter() {
        let issues = corpus();
        let view = IssueView::from_slice(&issues);
        let open = Status::new("open").unwrap();
        let f = IssueFilter {
            status: Some(&open),
            ..Default::default()
        };
        let narrowed = view.matching(&f);
        let ids: Vec<_> = narrowed.iter().map(|i| i.id.clone()).collect();
        assert_eq!(ids, vec![ir(1), ir(3)]);
    }

    #[test]
    fn filtering_is_allocation_light_and_can_chain() {
        let issues = corpus();
        let view = IssueView::from_slice(&issues);
        let chained = view.filter(|i| i.id != ir(2)).filter(|i| i.id != ir(3));
        assert_eq!(chained.len(), 1);
        assert_eq!(chained.iter().next().unwrap().id, ir(1));
    }

    #[test]
    fn empty_view_reports_zero_length() {
        let view = IssueView::from_refs(vec![]);
        assert!(view.is_empty());
        assert_eq!(view.len(), 0);
    }
}