cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Severity + `Violations` collection. The element type is
//! [`CheckViolation`] (the typed observation produced by a rule); the
//! textual rendering lives in the CLI layer.

use super::CheckViolation;

/// Severity of a check violation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum Severity {
    /// The file is invalid — `cartu check` exits 1.
    Error,
    /// The file is technically valid but has an issue worth reporting.
    /// `cartu check` exits 0; warnings are always printed (not gated by `--verbose`).
    Warning,
}

/// An ordered collection of `CheckViolation`s accumulated during validation.
///
/// An empty `Violations` means the record is fully valid.
/// Use `has_errors()` to determine whether `cartu check` should exit 1.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Violations(Vec<CheckViolation>);

impl Violations {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn push(&mut self, v: CheckViolation) {
        self.0.push(v);
    }

    pub fn extend(&mut self, other: Violations) {
        self.0.extend(other.0);
    }

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

    /// `true` if any violation has `Severity::Error` — drives `cartu check` exit status.
    pub fn has_errors(&self) -> bool {
        self.0.iter().any(|v| v.severity == Severity::Error)
    }

    pub fn iter(&self) -> impl Iterator<Item = &CheckViolation> {
        self.0.iter()
    }
}

impl IntoIterator for Violations {
    type Item = CheckViolation;
    type IntoIter = std::vec::IntoIter<CheckViolation>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl<'a> IntoIterator for &'a Violations {
    type Item = &'a CheckViolation;
    type IntoIter = std::slice::Iter<'a, CheckViolation>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.iter()
    }
}

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

#[cfg(test)]
pub mod strategy {
    use super::{Severity, Violations};
    use crate::domain::model::check::check_violation::strategy::check_violation;
    use proptest::prelude::*;

    pub fn severity() -> impl Strategy<Value = Severity> {
        prop_oneof![Just(Severity::Error), Just(Severity::Warning)]
    }

    pub fn violations() -> impl Strategy<Value = Violations> {
        proptest::collection::vec(check_violation(), 0..=4).prop_map(|v| v.into_iter().collect())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn has_errors_matches_any_error_severity(vs in strategy::violations()) {
            let any_error = vs.iter().any(|v| v.severity == Severity::Error);
            prop_assert_eq!(vs.has_errors(), any_error);
        }
    }
}