use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ValidationResult {
Passed,
Failed {
message: String,
violations: Vec<String>,
},
Warning {
message: String,
underlying: Box<Self>,
},
}
impl ValidationResult {
#[must_use]
pub const fn passed() -> Self {
Self::Passed
}
#[must_use]
pub fn failed(message: impl Into<String>, violations: Vec<String>) -> Self {
Self::Failed {
message: message.into(),
violations,
}
}
#[must_use]
pub fn warn(message: impl Into<String>, inner: Self) -> Self {
let underlying = match inner {
Self::Warning { underlying, .. } => underlying,
other => Box::new(other),
};
Self::Warning {
message: message.into(),
underlying,
}
}
#[must_use]
pub fn is_passed(&self) -> bool {
match self {
Self::Passed => true,
Self::Warning { underlying, .. } => matches!(**underlying, Self::Passed),
Self::Failed { .. } => false,
}
}
#[must_use]
pub fn is_failed(&self) -> bool {
match self {
Self::Failed { .. } => true,
Self::Warning { underlying, .. } => matches!(**underlying, Self::Failed { .. }),
Self::Passed => false,
}
}
#[must_use]
pub fn violations(&self) -> &[String] {
match self {
Self::Failed { violations, .. } => violations,
Self::Warning { underlying, .. } => match underlying.as_ref() {
Self::Failed { violations, .. } => violations,
_ => &[],
},
Self::Passed => &[],
}
}
#[must_use]
pub fn message(&self) -> Option<&str> {
match self {
Self::Passed => None,
Self::Failed { message, .. } | Self::Warning { message, .. } => Some(message),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn passed_is_passed() {
let r = ValidationResult::passed();
assert!(r.is_passed());
assert!(!r.is_failed());
assert!(r.violations().is_empty());
assert_eq!(r.message(), None);
}
#[test]
fn failed_is_failed() {
let r = ValidationResult::failed("bad", vec!["value < 0".into()]);
assert!(!r.is_passed());
assert!(r.is_failed());
assert_eq!(r.violations(), &["value < 0".to_owned()]);
assert_eq!(r.message(), Some("bad"));
}
#[test]
fn warn_around_passed_reports_passed() {
let r = ValidationResult::warn("fyi", ValidationResult::passed());
assert!(r.is_passed());
assert!(!r.is_failed());
assert_eq!(r.message(), Some("fyi"));
}
#[test]
fn warn_around_failed_reports_failed() {
let inner = ValidationResult::failed("bad", vec!["out of range".into()]);
let r = ValidationResult::warn("note", inner);
assert!(!r.is_passed());
assert!(r.is_failed());
assert_eq!(r.violations(), &["out of range".to_owned()]);
}
#[test]
fn warn_flattens_nested_warnings() {
let inner = ValidationResult::failed("bad", vec!["x".into()]);
let once = ValidationResult::warn("w1", inner);
let twice = ValidationResult::warn("w2", once);
match &twice {
ValidationResult::Warning { message, underlying } => {
assert_eq!(message, "w2");
assert!(matches!(
underlying.as_ref(),
ValidationResult::Failed { .. }
));
}
_ => panic!("expected Warning"),
}
}
#[test]
fn serde_roundtrip_failed() {
let r = ValidationResult::failed("bad", vec!["v1".into(), "v2".into()]);
let json = serde_json::to_string(&r).unwrap();
let back: ValidationResult = serde_json::from_str(&json).unwrap();
assert_eq!(r, back);
}
#[test]
fn serde_roundtrip_warning() {
let r = ValidationResult::warn("fyi", ValidationResult::passed());
let json = serde_json::to_string(&r).unwrap();
let back: ValidationResult = serde_json::from_str(&json).unwrap();
assert_eq!(r, back);
}
}