cyclonedx_bom/
validation.rs

1/*
2 * This file is part of CycloneDX Rust Cargo.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 * SPDX-License-Identifier: Apache-2.0
17 */
18use 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/// Contains all collected validation errors.
32#[derive(Debug, Clone, PartialEq)]
33pub struct ValidationResult {
34    /// Maps names to validation errors.
35    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        // merge all errors into one struct.
47        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    /// Returns `true` if there are no errors.
78    pub fn passed(&self) -> bool {
79        self.inner.is_empty()
80    }
81
82    /// Returns `true` if there are errors.
83    pub fn has_errors(&self) -> bool {
84        !self.inner.is_empty()
85    }
86
87    /// Returns the error with given name, if available
88    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    /// Returns an Iterator over all errors, consumes the [`ValidationResult`].
97    pub fn errors(self) -> IntoIter<String, ValidationErrorsKind> {
98        self.inner.into_iter()
99    }
100
101    /// Adds a nested object kind
102    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    /// Adds a single [`ValidationError`] for an enum variant.
111    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    /// Adds a single field [`ValidationError`].
120    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    /// Adds a list of validation errors for a custom entry.
133    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/// Collects validation results in a hierarchy, recommended to use in `Validate` implementations.
147#[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    /// Adds a custom validation error.
339    ///
340    /// A custom field is useful for properties that are not directly part of the Bom hierarchy, but
341    /// should be validated too, for example: dependencies between fields, e.g. bom-ref.
342    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
364/// The trait that SBOM structs need to implement to validate their content.
365pub 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/// A single validation error with a message, useful to log / display for user.
374#[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/// Implements possible hierarchy of a structured SBOM to collect all [`ValidationError`] in.
400#[derive(Debug, Clone, PartialEq)]
401pub enum ValidationErrorsKind {
402    /// Collects all field validation errors in context of a struct
403    Struct(ValidationResult),
404    /// Collects all child elements in context of a list, the key is the index into the list, e.g. `Vec`
405    List(BTreeMap<usize, ValidationResult>),
406    /// Contains the list of validation errors for a single field, e.g. struct field.
407    Field(Vec<ValidationError>),
408    /// Represents a single error for an Enum variant.
409    Enum(ValidationError),
410    /// A list validation errors for a custom field.
411    Custom(Vec<ValidationError>),
412}
413
414// --------------------------- Helper functions for tests -------------------------
415
416/// Function to create an enum based error.
417#[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}