Skip to main content

paramodel_elements/
validation.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Parameter / value validation outcomes.
5//!
6//! Direct port of the upstream Java sealed interface, but with the
7//! "no nested `Warning`" invariant enforced by the [`ValidationResult::warn`]
8//! constructor: any caller that hands a `Warning` as the inner result has
9//! that inner `Warning` unwrapped before construction.
10
11use serde::{Deserialize, Serialize};
12
13/// The outcome of validating a value against a parameter or constraint.
14///
15/// See SRD-0004 ยง`ValidationResult`. The `Warning` variant is flattened by
16/// construction (never `Warning { underlying: Warning { .. } }`); callers
17/// should use [`Self::warn`] rather than constructing the variant directly.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(tag = "kind", rename_all = "snake_case")]
20pub enum ValidationResult {
21    /// The value passed all checks.
22    Passed,
23
24    /// The value failed at least one check.
25    Failed {
26        /// High-level summary of the failure.
27        message:    String,
28        /// Per-check violation messages (in encounter order).
29        violations: Vec<String>,
30    },
31
32    /// The value is acceptable but worth flagging. Exactly one level of
33    /// warning may wrap an inner result.
34    Warning {
35        /// High-level summary of the warning.
36        message:    String,
37        /// The wrapped result (never itself a `Warning`).
38        underlying: Box<Self>,
39    },
40}
41
42impl ValidationResult {
43    /// Construct a passing result. Provided for symmetry with the other
44    /// constructors.
45    #[must_use]
46    pub const fn passed() -> Self {
47        Self::Passed
48    }
49
50    /// Construct a failing result.
51    #[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    /// Wrap an inner result with a warning. Flattens nested warnings.
60    ///
61    /// If `inner` is itself a `Warning`, its own inner result is lifted
62    /// so the returned value has exactly one warning layer. Messages
63    /// from discarded inner warnings are lost; use the outer `message`
64    /// to carry anything the caller needs to preserve.
65    #[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    /// `true` when the effective outcome is a pass (either `Passed` or a
78    /// warning whose underlying result is `Passed`).
79    #[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    /// `true` when the effective outcome is a failure.
89    #[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    /// Borrow the violation list, if any. Descends one level of warning.
99    #[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    /// Borrow the top-level summary message, if any.
112    #[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}