1use std::{
19 collections::{BTreeMap, HashSet},
20 fmt::Display,
21 hash::Hash,
22};
23
24use indexmap::{
25 map::{Entry::Vacant, IntoIter},
26 IndexMap,
27};
28
29use crate::models::bom::SpecVersion;
30
31#[derive(Debug, Clone, PartialEq)]
33pub struct ValidationResult {
34 pub(crate) inner: IndexMap<String, ValidationErrorsKind>,
36}
37
38impl Default for ValidationResult {
39 fn default() -> Self {
40 ValidationResult::new()
41 }
42}
43
44impl From<Vec<ValidationResult>> for ValidationResult {
45 fn from(errors: Vec<ValidationResult>) -> Self {
46 let mut result = ValidationResult::new();
48 for error in errors.into_iter() {
49 for (key, value) in error.inner.into_iter() {
50 result.inner.insert(key, value);
51 }
52 }
53 result
54 }
55}
56
57impl From<Result<(), ValidationError>> for ValidationResult {
58 fn from(value: Result<(), ValidationError>) -> Self {
59 match value {
60 Ok(()) => ValidationResult::default(),
61 Err(error) => {
62 let mut result = ValidationResult::default();
63 result.add_custom("", error);
64 result
65 }
66 }
67 }
68}
69
70impl ValidationResult {
71 pub fn new() -> Self {
72 Self {
73 inner: IndexMap::new(),
74 }
75 }
76
77 pub fn passed(&self) -> bool {
79 self.inner.is_empty()
80 }
81
82 pub fn has_errors(&self) -> bool {
84 !self.inner.is_empty()
85 }
86
87 pub fn error(&self, field: &str) -> Option<&ValidationErrorsKind> {
89 self.inner.get(&field.to_string())
90 }
91
92 pub fn has_error(&self, field: &str) -> bool {
93 self.inner.contains_key(field)
94 }
95
96 pub fn errors(self) -> IntoIter<String, ValidationErrorsKind> {
98 self.inner.into_iter()
99 }
100
101 fn add_nested(&mut self, nested_name: &str, errors_kind: ValidationErrorsKind) {
103 if let Vacant(entry) = self.inner.entry(nested_name.to_string()) {
104 entry.insert(errors_kind);
105 } else {
106 panic!("Attempt to replace non-empty nested entry")
107 }
108 }
109
110 fn add_enum(&mut self, enum_name: &str, validation_error: ValidationError) {
112 if let Vacant(entry) = self.inner.entry(enum_name.to_string()) {
113 entry.insert(ValidationErrorsKind::Enum(validation_error));
114 } else {
115 panic!("Attempt to replace non-empty enum entry")
116 }
117 }
118
119 fn add_field(&mut self, field_name: &str, validation_error: ValidationError) {
121 if let ValidationErrorsKind::Field(ref mut vec) = self
122 .inner
123 .entry(field_name.to_string())
124 .or_insert_with(|| ValidationErrorsKind::Field(vec![]))
125 {
126 vec.push(validation_error);
127 } else {
128 panic!("Found a non-field ValidationErrorsKind");
129 }
130 }
131
132 fn add_custom(&mut self, custom_name: &str, validation_error: ValidationError) {
134 if let ValidationErrorsKind::Custom(ref mut vec) = self
135 .inner
136 .entry(custom_name.to_string())
137 .or_insert_with(|| ValidationErrorsKind::Custom(vec![]))
138 {
139 vec.push(validation_error);
140 } else {
141 panic!("Found a non-custom ValidationErrorsKind");
142 }
143 }
144}
145
146#[derive(Debug)]
148pub struct ValidationContext {
149 state: ValidationResult,
150}
151
152impl Default for ValidationContext {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158impl ValidationContext {
159 pub fn new() -> Self {
160 Self {
161 state: ValidationResult::default(),
162 }
163 }
164
165 pub fn add_field<T>(
166 &mut self,
167 field_name: &str,
168 field: T,
169 validation: impl FnOnce(T) -> Result<(), ValidationError>,
170 ) -> &mut Self {
171 if let Err(validation_error) = validation(field) {
172 self.state.add_field(field_name, validation_error);
173 }
174 self
175 }
176
177 pub fn add_field_option<T>(
178 &mut self,
179 field_name: &str,
180 field: Option<T>,
181 validation: impl FnOnce(T) -> Result<(), ValidationError>,
182 ) -> &mut Self {
183 if let Some(field) = field {
184 self.add_field(field_name, field, validation);
185 }
186 self
187 }
188
189 pub fn add_enum<T>(
190 &mut self,
191 enum_name: &str,
192 enum_type: &T,
193 validation: impl FnOnce(&T) -> Result<(), ValidationError>,
194 ) -> &mut Self {
195 if let Err(error) = validation(enum_type) {
196 self.state.add_enum(enum_name, error);
197 }
198 self
199 }
200
201 pub fn add_enum_option<T>(
202 &mut self,
203 enum_name: &str,
204 enum_type: Option<&T>,
205 validation: impl FnOnce(&T) -> Result<(), ValidationError>,
206 ) -> &mut Self {
207 if let Some(enum_type) = enum_type {
208 self.add_enum(enum_name, enum_type, validation);
209 }
210 self
211 }
212
213 pub fn add_list<'a, T, I, Output>(
214 &mut self,
215 field_name: &str,
216 list: T,
217 validation: impl Fn(&'a I) -> Output,
218 ) -> &mut Self
219 where
220 I: 'a,
221 T: IntoIterator<Item = &'a I>,
222 Output: Into<ValidationResult>,
223 {
224 let child_errors = list
225 .into_iter()
226 .map(|item| validation(item).into())
227 .enumerate()
228 .filter_map(|(index, result)| {
229 if result.has_errors() {
230 Some((index, result))
231 } else {
232 None
233 }
234 })
235 .collect::<BTreeMap<usize, ValidationResult>>();
236
237 if !child_errors.is_empty() {
238 self.state
239 .add_nested(field_name, ValidationErrorsKind::List(child_errors));
240 }
241 self
242 }
243 pub fn add_unique_list<'a, T, I, Output>(
244 &mut self,
245 field_name: &str,
246 list: T,
247 validation: impl Fn(&'a I) -> Output,
248 ) -> &mut Self
249 where
250 I: 'a + Eq + Hash,
251 T: IntoIterator<Item = &'a I>,
252 Output: Into<ValidationResult>,
253 {
254 let mut set = HashSet::new();
255 let mut child_errors = BTreeMap::new();
256
257 for (index, item) in list.into_iter().enumerate() {
258 if !set.insert(item) {
259 child_errors.insert(index, Err(ValidationError::new("repeated element")).into());
260 } else {
261 let result = validation(item).into();
262 if result.has_errors() {
263 child_errors.insert(index, result);
264 }
265 }
266 }
267
268 if !child_errors.is_empty() {
269 self.state
270 .add_nested(field_name, ValidationErrorsKind::List(child_errors));
271 }
272 self
273 }
274
275 pub fn add_list_option<'a, T, I, Output>(
276 &mut self,
277 list_name: &str,
278 list: Option<T>,
279 validation: impl Fn(&'a I) -> Output,
280 ) -> &mut Self
281 where
282 I: 'a,
283 T: IntoIterator<Item = &'a I>,
284 Output: Into<ValidationResult>,
285 {
286 if let Some(list) = list {
287 self.add_list(list_name, list, validation);
288 }
289 self
290 }
291
292 pub fn add_unique_list_option<'a, T, I, Output>(
293 &mut self,
294 list_name: &str,
295 list: Option<T>,
296 validation: impl Fn(&'a I) -> Output,
297 ) -> &mut Self
298 where
299 I: 'a + Eq + Hash,
300 T: IntoIterator<Item = &'a I>,
301 Output: Into<ValidationResult>,
302 {
303 if let Some(list) = list {
304 self.add_unique_list(list_name, list, validation);
305 }
306 self
307 }
308
309 pub fn add_struct<T>(
310 &mut self,
311 struct_name: &str,
312 r#struct: &T,
313 version: SpecVersion,
314 ) -> &mut Self
315 where
316 T: Validate,
317 {
318 let result = r#struct.validate_version(version);
319 if result.has_errors() {
320 self.state
321 .add_nested(struct_name, ValidationErrorsKind::Struct(result));
322 }
323 self
324 }
325
326 pub fn add_struct_option<T: Validate>(
327 &mut self,
328 struct_name: &str,
329 r#struct: Option<&T>,
330 version: SpecVersion,
331 ) -> &mut Self {
332 if let Some(r#struct) = r#struct {
333 self.add_struct(struct_name, r#struct, version);
334 }
335 self
336 }
337
338 pub fn add_custom(
343 &mut self,
344 custom_name: &str,
345 error: impl Into<ValidationError>,
346 ) -> &mut Self {
347 self.state.add_custom(custom_name, error.into());
348 self
349 }
350}
351
352impl From<ValidationContext> for ValidationResult {
353 fn from(context: ValidationContext) -> Self {
354 context.state
355 }
356}
357
358impl From<&mut ValidationContext> for ValidationResult {
359 fn from(context: &mut ValidationContext) -> Self {
360 context.state.clone()
361 }
362}
363
364pub trait Validate {
366 fn validate_version(&self, version: SpecVersion) -> ValidationResult;
367
368 fn validate(&self) -> ValidationResult {
369 self.validate_version(SpecVersion::default())
370 }
371}
372
373#[derive(Debug, Clone, PartialEq)]
375pub struct ValidationError {
376 pub message: String,
377}
378
379impl From<String> for ValidationError {
380 fn from(message: String) -> Self {
381 ValidationError { message }
382 }
383}
384
385impl From<&str> for ValidationError {
386 fn from(message: &str) -> Self {
387 ValidationError::new(message)
388 }
389}
390
391impl ValidationError {
392 pub fn new<D: Display>(message: D) -> Self {
393 Self {
394 message: message.to_string(),
395 }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq)]
401pub enum ValidationErrorsKind {
402 Struct(ValidationResult),
404 List(BTreeMap<usize, ValidationResult>),
406 Field(Vec<ValidationError>),
408 Enum(ValidationError),
410 Custom(Vec<ValidationError>),
412}
413
414#[cfg(test)]
418pub(crate) fn r#enum(enum_name: &str, error: impl Into<ValidationError>) -> ValidationResult {
419 let mut result = ValidationResult::default();
420 result.add_enum(enum_name, error.into());
421 result
422}
423
424#[cfg(test)]
425pub(crate) fn r#struct(struct_name: &str, errors: impl Into<ValidationResult>) -> ValidationResult {
426 let mut result = ValidationResult::default();
427 result.add_nested(struct_name, ValidationErrorsKind::Struct(errors.into()));
428 result
429}
430
431#[cfg(test)]
432pub(crate) fn list<T>(
433 field_name: &str,
434 validation_errors: impl IntoIterator<Item = (usize, T)>,
435) -> ValidationResult
436where
437 T: Into<ValidationResult>,
438{
439 let list = validation_errors
440 .into_iter()
441 .map(|(index, errors)| (index, errors.into()))
442 .collect::<BTreeMap<usize, ValidationResult>>();
443
444 let mut result = ValidationResult::default();
445 result.add_nested(field_name, ValidationErrorsKind::List(list));
446 result
447}
448
449#[cfg(test)]
450pub(crate) fn field(field_name: &str, error: impl Into<ValidationError>) -> ValidationResult {
451 let mut result = ValidationResult::default();
452 result.add_field(field_name, error.into());
453 result
454}
455
456#[cfg(test)]
457pub(crate) fn custom<I, T>(custom_name: &str, validation_errors: I) -> ValidationResult
458where
459 I: IntoIterator<Item = T>,
460 T: Into<ValidationError>,
461{
462 let validation_errors = validation_errors
463 .into_iter()
464 .map(|i| i.into())
465 .collect::<Vec<ValidationError>>();
466 let mut result = ValidationResult::default();
467 for error in validation_errors {
468 result.add_custom(custom_name, error);
469 }
470 result
471}
472
473#[cfg(test)]
474mod tests {
475 use crate::{
476 models::bom::SpecVersion,
477 validation::{field, r#enum, r#struct, Validate, ValidationErrorsKind, ValidationResult},
478 };
479
480 use super::{ValidationContext, ValidationError};
481
482 #[test]
483 fn has_error() {
484 let mut result = ValidationResult::new();
485 result.add_field("test", ValidationError::new("missing"));
486
487 assert!(result.has_error("test"));
488 assert!(!result.has_error("haha"));
489 }
490
491 #[test]
492 fn has_errors() {
493 let mut result = ValidationResult::new();
494 assert!(!result.has_errors());
495
496 result.add_field("hello", ValidationError::new("again"));
497 assert!(result.has_errors());
498 }
499
500 #[test]
501 fn build_validation_errors_enum() {
502 let result = r#enum("hello", "world");
503 assert_eq!(
504 result.error("hello"),
505 Some(&ValidationErrorsKind::Enum("world".into()))
506 );
507 }
508
509 #[test]
510 fn build_validation_errors_hierarchy() {
511 struct Nested {
512 name: String,
513 }
514
515 impl Validate for Nested {
516 fn validate_version(&self, _version: SpecVersion) -> ValidationResult {
517 ValidationContext::new()
518 .add_field("name", &self.name, |_name| {
519 Err(ValidationError::new("Failed"))
520 })
521 .into()
522 }
523 }
524
525 let validation_result: ValidationResult = ValidationContext::new()
526 .add_enum("test", &2, |_| Err("not a variant".into()))
527 .add_struct(
528 "nested",
529 &Nested {
530 name: "hello".to_string(),
531 },
532 SpecVersion::V1_3,
533 )
534 .into();
535
536 assert_eq!(
537 validation_result,
538 vec![
539 r#enum("test", "not a variant"),
540 r#struct("nested", field("name", "Failed")),
541 ]
542 .into()
543 );
544 }
545}