made_core/entities/
validation.rs1use serde::{Deserialize, Serialize};
11
12use crate::error::DomainError;
13use crate::value_objects::{Attributes, Score};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct ValidatorReport {
22 kind: String,
23 passed: bool,
24 summary: String,
25 details: Attributes,
26}
27
28impl ValidatorReport {
29 pub fn new(
30 kind: impl Into<String>,
31 passed: bool,
32 summary: impl Into<String>,
33 details: Attributes,
34 ) -> Result<Self, DomainError> {
35 let kind = kind.into();
36 if kind.trim().is_empty() {
37 return Err(DomainError::EmptyField {
38 field: "validator_report.kind",
39 });
40 }
41 Ok(Self {
42 kind,
43 passed,
44 summary: summary.into(),
45 details,
46 })
47 }
48
49 #[must_use]
50 pub fn kind(&self) -> &str {
51 &self.kind
52 }
53 #[must_use]
54 pub fn passed(&self) -> bool {
55 self.passed
56 }
57 #[must_use]
58 pub fn summary(&self) -> &str {
59 &self.summary
60 }
61 #[must_use]
62 pub fn details(&self) -> &Attributes {
63 &self.details
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ValidationOutcome {
70 score: Score,
71 reports: Vec<ValidatorReport>,
72}
73
74impl ValidationOutcome {
75 #[must_use]
76 pub fn new(score: Score, reports: Vec<ValidatorReport>) -> Self {
77 Self { score, reports }
78 }
79
80 #[must_use]
81 pub fn score(&self) -> Score {
82 self.score
83 }
84 #[must_use]
85 pub fn reports(&self) -> &[ValidatorReport] {
86 &self.reports
87 }
88
89 #[must_use]
91 pub fn all_passed(&self) -> bool {
92 self.reports.iter().all(ValidatorReport::passed)
93 }
94
95 #[must_use]
97 pub fn failures(&self) -> usize {
98 self.reports.iter().filter(|r| !r.passed()).count()
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn report(kind: &str, passed: bool) -> ValidatorReport {
107 ValidatorReport::new(kind, passed, "summary", Attributes::empty()).unwrap()
108 }
109
110 #[test]
111 fn empty_kind_is_rejected() {
112 let err = ValidatorReport::new(" ", true, "", Attributes::empty()).unwrap_err();
113 assert!(matches!(
114 err,
115 DomainError::EmptyField {
116 field: "validator_report.kind"
117 }
118 ));
119 }
120
121 #[test]
122 fn arbitrary_kind_is_accepted() {
123 for kind in [
125 "lint",
126 "policy",
127 "dry-run",
128 "clinical-safety",
129 "sourcing-feasibility",
130 "fact-check",
131 ] {
132 assert_eq!(report(kind, true).kind(), kind);
133 }
134 }
135
136 #[test]
137 fn outcome_aggregates_reports() {
138 let o = ValidationOutcome::new(
139 Score::new(0.75).unwrap(),
140 vec![report("lint", true), report("policy", false)],
141 );
142 assert_eq!(o.reports().len(), 2);
143 assert!(!o.all_passed());
144 assert_eq!(o.failures(), 1);
145 assert_eq!(o.score().get(), 0.75);
146 }
147
148 #[test]
149 fn outcome_without_failures_reports_all_passed() {
150 let o = ValidationOutcome::new(Score::MAX, vec![report("a", true), report("b", true)]);
151 assert!(o.all_passed());
152 assert_eq!(o.failures(), 0);
153 }
154
155 #[test]
156 fn outcome_with_no_reports_trivially_passes() {
157 let o = ValidationOutcome::new(Score::MIN, vec![]);
158 assert!(o.all_passed());
159 assert_eq!(o.failures(), 0);
160 }
161}