paramodel_elements/
validation.rs1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(tag = "kind", rename_all = "snake_case")]
20pub enum ValidationResult {
21 Passed,
23
24 Failed {
26 message: String,
28 violations: Vec<String>,
30 },
31
32 Warning {
35 message: String,
37 underlying: Box<Self>,
39 },
40}
41
42impl ValidationResult {
43 #[must_use]
46 pub const fn passed() -> Self {
47 Self::Passed
48 }
49
50 #[must_use]
52 pub fn failed(message: impl Into<String>, violations: Vec<String>) -> Self {
53 Self::Failed {
54 message: message.into(),
55 violations,
56 }
57 }
58
59 #[must_use]
66 pub fn warn(message: impl Into<String>, inner: Self) -> Self {
67 let underlying = match inner {
68 Self::Warning { underlying, .. } => underlying,
69 other => Box::new(other),
70 };
71 Self::Warning {
72 message: message.into(),
73 underlying,
74 }
75 }
76
77 #[must_use]
80 pub fn is_passed(&self) -> bool {
81 match self {
82 Self::Passed => true,
83 Self::Warning { underlying, .. } => matches!(**underlying, Self::Passed),
84 Self::Failed { .. } => false,
85 }
86 }
87
88 #[must_use]
90 pub fn is_failed(&self) -> bool {
91 match self {
92 Self::Failed { .. } => true,
93 Self::Warning { underlying, .. } => matches!(**underlying, Self::Failed { .. }),
94 Self::Passed => false,
95 }
96 }
97
98 #[must_use]
100 pub fn violations(&self) -> &[String] {
101 match self {
102 Self::Failed { violations, .. } => violations,
103 Self::Warning { underlying, .. } => match underlying.as_ref() {
104 Self::Failed { violations, .. } => violations,
105 _ => &[],
106 },
107 Self::Passed => &[],
108 }
109 }
110
111 #[must_use]
113 pub fn message(&self) -> Option<&str> {
114 match self {
115 Self::Passed => None,
116 Self::Failed { message, .. } | Self::Warning { message, .. } => Some(message),
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn passed_is_passed() {
127 let r = ValidationResult::passed();
128 assert!(r.is_passed());
129 assert!(!r.is_failed());
130 assert!(r.violations().is_empty());
131 assert_eq!(r.message(), None);
132 }
133
134 #[test]
135 fn failed_is_failed() {
136 let r = ValidationResult::failed("bad", vec!["value < 0".into()]);
137 assert!(!r.is_passed());
138 assert!(r.is_failed());
139 assert_eq!(r.violations(), &["value < 0".to_owned()]);
140 assert_eq!(r.message(), Some("bad"));
141 }
142
143 #[test]
144 fn warn_around_passed_reports_passed() {
145 let r = ValidationResult::warn("fyi", ValidationResult::passed());
146 assert!(r.is_passed());
147 assert!(!r.is_failed());
148 assert_eq!(r.message(), Some("fyi"));
149 }
150
151 #[test]
152 fn warn_around_failed_reports_failed() {
153 let inner = ValidationResult::failed("bad", vec!["out of range".into()]);
154 let r = ValidationResult::warn("note", inner);
155 assert!(!r.is_passed());
156 assert!(r.is_failed());
157 assert_eq!(r.violations(), &["out of range".to_owned()]);
158 }
159
160 #[test]
161 fn warn_flattens_nested_warnings() {
162 let inner = ValidationResult::failed("bad", vec!["x".into()]);
163 let once = ValidationResult::warn("w1", inner);
164 let twice = ValidationResult::warn("w2", once);
165 match &twice {
166 ValidationResult::Warning { message, underlying } => {
167 assert_eq!(message, "w2");
168 assert!(matches!(
169 underlying.as_ref(),
170 ValidationResult::Failed { .. }
171 ));
172 }
173 _ => panic!("expected Warning"),
174 }
175 }
176
177 #[test]
178 fn serde_roundtrip_failed() {
179 let r = ValidationResult::failed("bad", vec!["v1".into(), "v2".into()]);
180 let json = serde_json::to_string(&r).unwrap();
181 let back: ValidationResult = serde_json::from_str(&json).unwrap();
182 assert_eq!(r, back);
183 }
184
185 #[test]
186 fn serde_roundtrip_warning() {
187 let r = ValidationResult::warn("fyi", ValidationResult::passed());
188 let json = serde_json::to_string(&r).unwrap();
189 let back: ValidationResult = serde_json::from_str(&json).unwrap();
190 assert_eq!(r, back);
191 }
192}