cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use crate::domain::model::issue::IssueView;
use crate::domain::model::status::Status;

/// Overview counts extracted from a filtered issue set.
pub(super) struct Overview {
    pub total: u32,
    pub by_status: Vec<(Status, u32)>,
}

pub(super) fn compute_overview(filtered: &IssueView<'_>) -> Overview {
    let total = filtered.len() as u32;

    let mut status_map: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
    for issue in filtered {
        *status_map
            .entry(issue.status.as_str().to_string())
            .or_insert(0) += 1;
    }
    let mut by_status: Vec<(Status, u32)> = status_map
        .into_iter()
        .map(|(s, c)| (Status::unresolved(s), c))
        .collect();
    by_status.sort_by(|a, b| a.0.cmp(&b.0));

    Overview { total, by_status }
}

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

    #[test]
    fn empty_slice_returns_zero_total() {
        scenario().when_overview().then_empty();
    }

    #[test]
    fn counts_total_correctly() {
        scenario()
            .given(feature("A").status("open"))
            .given(defect("B").status("open"))
            .given(feature("C").status("closed"))
            .when_overview()
            .then_total(3);
    }

    #[test]
    fn groups_by_status() {
        scenario()
            .given(feature("A").status("open"))
            .given(feature("B").status("open"))
            .given(feature("C").status("closed"))
            .when_overview()
            .then_status_count("open", 2)
            .then_status_count("closed", 1);
    }

    #[test]
    fn by_status_is_sorted_alphabetically() {
        scenario()
            .given(feature("A").status("open"))
            .given(feature("B").status("closed"))
            .when_overview()
            .then_by_status_is_sorted();
    }

    struct Scenario {
        issues: Vec<Issue>,
    }

    fn scenario() -> Scenario {
        Scenario { issues: vec![] }
    }

    impl Scenario {
        fn given(mut self, fixture: IssueFixture) -> Self {
            let id = self.issues.len() as u64 + 1;
            self.issues.push(fixture.build(ir(id)));
            self
        }

        fn when_overview(self) -> OverviewOutcome {
            let view = IssueView::from_slice(&self.issues);
            let result = compute_overview(&view);
            OverviewOutcome { result }
        }
    }

    struct OverviewOutcome {
        result: Overview,
    }

    impl OverviewOutcome {
        fn then_total(self, expected: u32) -> Self {
            assert_eq!(self.result.total, expected);
            self
        }

        fn then_empty(self) -> Self {
            assert_eq!(self.result.total, 0);
            assert!(self.result.by_status.is_empty());
            self
        }

        fn then_status_count(self, status: &str, expected: u32) -> Self {
            let count = self
                .result
                .by_status
                .iter()
                .find(|(s, _)| s.as_str() == status)
                .map(|(_, c)| *c);
            assert_eq!(count, Some(expected));
            self
        }

        fn then_by_status_is_sorted(self) -> Self {
            let labels: Vec<&str> = self
                .result
                .by_status
                .iter()
                .map(|(s, _)| s.as_str())
                .collect();
            let mut sorted = labels.clone();
            sorted.sort();
            assert_eq!(labels, sorted);
            self
        }
    }
}