use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ConstraintLevel {
Assert,
Check,
}
#[derive(Debug, Clone)]
pub struct Constraint {
pub level: ConstraintLevel,
pub name: String,
pub description: String,
}
impl Constraint {
pub fn new(
level: ConstraintLevel,
name: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
level,
name: name.into(),
description: description.into(),
}
}
pub fn assert(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(ConstraintLevel::Assert, name, description)
}
pub fn check(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(ConstraintLevel::Check, name, description)
}
pub fn validate(&self, passed: bool) -> ConstraintResult {
ConstraintResult {
constraint: self.clone(),
passed,
}
}
pub const fn is_assert(&self) -> bool {
matches!(self.level, ConstraintLevel::Assert)
}
pub const fn is_check(&self) -> bool {
matches!(self.level, ConstraintLevel::Check)
}
}
#[derive(Debug, Clone)]
pub struct ConstraintResult {
pub constraint: Constraint,
pub passed: bool,
}
impl ConstraintResult {
pub const fn passed(&self) -> bool {
self.passed
}
pub const fn failed(&self) -> bool {
!self.passed
}
pub const fn is_failing_assert(&self) -> bool {
self.constraint.is_assert() && !self.passed
}
}
impl fmt::Display for ConstraintResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.passed { "PASS" } else { "FAIL" };
write!(
f,
"[{:?}] {}: {} ({})",
self.constraint.level, status, self.constraint.name, self.constraint.description
)
}
}
#[derive(Debug, Clone, Default)]
pub struct ConstraintResults {
results: Vec<ConstraintResult>,
}
impl ConstraintResults {
pub fn new() -> Self {
Self {
results: Vec::new(),
}
}
pub fn add(&mut self, result: ConstraintResult) {
self.results.push(result);
}
pub fn all(&self) -> &[ConstraintResult] {
&self.results
}
#[inline]
pub fn all_asserts_passed(&self) -> bool {
self.results
.iter()
.filter(|r| r.constraint.is_assert())
.all(|r| r.passed)
}
pub fn failing_asserts(&self) -> Vec<&ConstraintResult> {
self.results
.iter()
.filter(|r| r.is_failing_assert())
.collect()
}
pub fn checks(&self) -> Vec<&ConstraintResult> {
self.results
.iter()
.filter(|r| r.constraint.is_check())
.collect()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.results.len()
}
}
impl fmt::Display for ConstraintResults {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return write!(f, "No constraints");
}
writeln!(f, "Constraint Results ({} total):", self.len())?;
for result in &self.results {
writeln!(f, " {}", result)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constraint_creation() {
let constraint = Constraint::assert("age_positive", "age must be > 0");
assert_eq!(constraint.level, ConstraintLevel::Assert);
assert_eq!(constraint.name, "age_positive");
assert!(constraint.is_assert());
assert!(!constraint.is_check());
}
#[test]
fn test_constraint_validation_pass() {
let constraint = Constraint::assert("test", "test constraint");
let result = constraint.validate(true);
assert!(result.passed());
assert!(!result.failed());
}
#[test]
fn test_constraint_validation_fail() {
let constraint = Constraint::assert("test", "test constraint");
let result = constraint.validate(false);
assert!(!result.passed());
assert!(result.failed());
assert!(result.is_failing_assert());
}
#[test]
fn test_constraint_results() {
let mut results = ConstraintResults::new();
assert!(results.is_empty());
let c1 = Constraint::assert("c1", "first constraint");
let c2 = Constraint::check("c2", "second constraint");
results.add(c1.validate(true));
results.add(c2.validate(false));
assert_eq!(results.len(), 2);
assert!(results.all_asserts_passed());
assert_eq!(results.checks().len(), 1);
}
#[test]
fn test_failing_asserts() {
let mut results = ConstraintResults::new();
let c1 = Constraint::assert("c1", "should pass");
let c2 = Constraint::assert("c2", "should fail");
let c3 = Constraint::check("c3", "check that fails");
results.add(c1.validate(true));
results.add(c2.validate(false));
results.add(c3.validate(false));
assert!(!results.all_asserts_passed());
assert_eq!(results.failing_asserts().len(), 1);
assert_eq!(results.failing_asserts()[0].constraint.name, "c2");
}
#[test]
fn test_constraint_result_display() {
let constraint = Constraint::assert("age_check", "age must be positive");
let result = constraint.validate(false);
let display = format!("{}", result);
assert!(display.contains("FAIL"));
assert!(display.contains("age_check"));
assert!(display.contains("age must be positive"));
}
#[test]
fn test_constraint_results_display() {
let mut results = ConstraintResults::new();
results.add(Constraint::assert("c1", "test1").validate(true));
results.add(Constraint::check("c2", "test2").validate(false));
let display = format!("{}", results);
assert!(display.contains("2 total"));
}
}