ion_schema/
violation.rs

1use crate::ion_path::IonPath;
2use std::fmt;
3use std::fmt::Formatter;
4use thiserror::Error;
5
6/// Represents [Violation] found during validation with detailed error message, error code and the constraint for which the validation failed
7/// Equivalence of `Violation` is not supported due to its tree structure of having children `Violation`s.
8/// Please use macro `assert_equivalent_violations!(violation1, violation2)` for comparing if two violations are equal.
9/// This macro uses `flattened_violations` and does not depend on the order of the children `Violation`s.
10/// For non-equivalent violations use macro `assert_non_equivalent_violations!(violation1, violation2)`.
11#[derive(Debug, Clone, Error)]
12pub struct Violation {
13    constraint: String,  // represents the constraint that created this violation
14    code: ViolationCode, // represents an error code that indicates the type of the violation
15    message: String,     // represents the detailed error message for this violation
16    ion_path: IonPath,   // represents the path to Ion value for which violation occurred
17    violations: Vec<Violation>,
18}
19
20impl Violation {
21    pub fn new<A: AsRef<str>, B: AsRef<str>>(
22        constraint: A,
23        code: ViolationCode,
24        message: B,
25        ion_path: &mut IonPath,
26    ) -> Self {
27        Self {
28            constraint: constraint.as_ref().to_owned(),
29            code,
30            message: message.as_ref().to_owned(),
31            ion_path: ion_path.to_owned(),
32            violations: Vec::new(),
33        }
34    }
35
36    pub fn with_violations<A: AsRef<str>, B: AsRef<str>>(
37        constraint: A,
38        code: ViolationCode,
39        message: B,
40        ion_path: &mut IonPath,
41        violations: Vec<Violation>,
42    ) -> Self {
43        Self {
44            constraint: constraint.as_ref().to_owned(),
45            code,
46            message: message.as_ref().to_owned(),
47            ion_path: ion_path.to_owned(),
48            violations,
49        }
50    }
51
52    pub fn ion_path(&self) -> &IonPath {
53        &self.ion_path
54    }
55
56    pub fn message(&self) -> &String {
57        &self.message
58    }
59
60    pub fn code(&self) -> &ViolationCode {
61        &self.code
62    }
63
64    /// Provides flattened list of leaf violations which represent the root cause of the top-level violation.
65    pub fn flattened_violations(&self) -> Vec<&Violation> {
66        let mut flattened_violations = Vec::new();
67        self.flatten_violations(&mut flattened_violations);
68        flattened_violations
69    }
70
71    fn flatten_violations<'a>(&'a self, flattened: &mut Vec<&'a Violation>) {
72        if self.violations.is_empty() {
73            flattened.push(self);
74        }
75        for violation in &self.violations {
76            if violation.violations.is_empty() {
77                flattened.push(violation);
78            } else {
79                violation.flatten_violations(flattened)
80            }
81        }
82    }
83
84    pub fn violations(&self) -> impl Iterator<Item = &Violation> {
85        self.violations.iter()
86    }
87}
88
89impl fmt::Display for Violation {
90    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91        f.write_str(self.message.as_str())?;
92
93        let mut stack = vec![];
94        let mut violations_iter = self.violations.iter();
95        let mut violation = violations_iter.next();
96
97        let mut indent = "  ".to_string();
98
99        while let Some(v) = violation {
100            f.write_fmt(format_args!("\n{}- {}", &indent, v.message))?;
101
102            if !v.violations.is_empty() {
103                stack.push(violations_iter);
104                violations_iter = v.violations.iter();
105                indent.push_str("  ");
106            }
107            violation = violations_iter.next();
108            while violation.is_none() && !stack.is_empty() {
109                violations_iter = stack.pop().unwrap();
110                indent.truncate(indent.len() - 2);
111                violation = violations_iter.next();
112            }
113        }
114        Ok(())
115    }
116}
117
118/// Represents violation code that indicates the type of the violation
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120#[non_exhaustive]
121pub enum ViolationCode {
122    AllTypesNotMatched,
123    AnnotationMismatched,
124    ElementMismatched,     // this is used for mismatched elements in containers
125    ElementNotDistinct,    // this is used for elements that are not distinct in containers
126    FieldNamesMismatched,  // this is used for mismatched field names in a struct
127    FieldNamesNotDistinct, // this is used for field names that are not distinct in a struct
128    FieldsNotMatched,
129    InvalidIeee754Float, // this is used for ieee754_float constraint
130    InvalidLength, // this is used for any length related constraints (e.g. container_length, byte_length, codepoint_length)
131    InvalidNull,   // if the value is a null for type references that doesn't allow null
132    InvalidOpenContent, // if a container contains open content when `content: closed` is specified
133    InvalidValue,  // this is used for valid_values constraint
134    MissingAnnotation, // if the annotation is missing for annotations constraint
135    MissingValue,  // if the ion value is missing for a particular constraint
136    MoreThanOneTypeMatched,
137    NoTypesMatched,
138    RegexMismatched, // this is used for regex constraint
139    TypeConstraintsUnsatisfied,
140    TypeMatched,
141    TypeMismatched,
142    UnexpectedAnnotation, // if unexpected annotation is found for annotations constraint
143}
144
145impl fmt::Display for ViolationCode {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(
148            f,
149            "{}",
150            match self {
151                ViolationCode::AllTypesNotMatched => "all_types_not_matched",
152                ViolationCode::AnnotationMismatched => "annotation_mismatched",
153                ViolationCode::ElementMismatched => "element_mismatched",
154                ViolationCode::ElementNotDistinct => "element_not_distinct",
155                ViolationCode::FieldNamesMismatched => "field_names_mismatched",
156                ViolationCode::FieldNamesNotDistinct => "field_names_not_distinct",
157                ViolationCode::FieldsNotMatched => "fields_not_matched",
158                ViolationCode::InvalidIeee754Float => "invalid_ieee754_float",
159                ViolationCode::InvalidLength => "invalid_length",
160                ViolationCode::InvalidNull => "invalid_null",
161                ViolationCode::InvalidOpenContent => "invalid_open_content",
162                ViolationCode::InvalidValue => "invalid_value",
163                ViolationCode::MissingAnnotation => "missing_annotation",
164                ViolationCode::MissingValue => "missing_value",
165                ViolationCode::MoreThanOneTypeMatched => "more_than_one_type_matched",
166                ViolationCode::NoTypesMatched => "no_types_matched",
167                ViolationCode::RegexMismatched => "regex_mismatched",
168                ViolationCode::TypeConstraintsUnsatisfied => "type_constraints_unsatisfied",
169                ViolationCode::TypeMatched => "type_matched",
170                ViolationCode::TypeMismatched => "type_mismatched",
171                ViolationCode::UnexpectedAnnotation => "unexpected_annotation",
172            }
173        )
174    }
175}
176
177#[macro_export]
178/// Equivalence for `Violation`s is not supported due to its tree structure of having children violations.
179/// This macro can be used for comparing if two violations are equal and uses `flattened_violations` for the comparison.
180macro_rules! assert_equivalent_violations {
181    ($left:expr, $right:expr $(,)?) => {
182        let mut left_strings: Vec<String> = $left
183                .flattened_violations()
184                .into_iter()
185                .map(|x| format!("{:?}", x))
186                .collect();
187        left_strings.sort();
188        let mut right_strings: Vec<String> = $right
189                .flattened_violations()
190                .into_iter()
191                .map(|x| format!("{:?}", x))
192                .collect();
193        right_strings.sort();
194        assert_eq!(left_strings, right_strings);
195    };
196    ($left:expr, $right:expr, $($arg:tt)+) => {
197        let mut left_strings: Vec<String> = $left
198                .flattened_violations()
199                .into_iter()
200                .map(|x| format!("{:?}", x))
201                .collect();
202        left_strings.sort();
203        let mut right_strings: Vec<String> = $right
204                .flattened_violations()
205                .into_iter()
206                .map(|x| format!("{:?}", x))
207                .collect();
208        right_strings.sort();
209        assert_eq!(left_strings, right_strings, $($arg)+);
210    };
211}
212
213#[macro_export]
214
215/// Equivalence for `Violation`s is not supported due to its tree structure of having children violations.
216/// This macro can be used for comparing if two violations are not equal and uses `flattened_violations` for the comparison.
217macro_rules! assert_non_equivalent_violations {
218    ($left:expr, $right:expr $(,)?) => {
219        let mut left_strings: Vec<String> = $left
220                .flattened_violations()
221                .into_iter()
222                .map(|x| format!("{:?}", x))
223                .collect();
224        left_strings.sort();
225        let mut right_strings: Vec<String> = $right
226                .flattened_violations()
227                .into_iter()
228                .map(|x| format!("{:?}", x))
229                .collect();
230        right_strings.sort();
231        assert_ne!(left_strings, right_strings);
232    };
233    ($left:expr, $right:expr, $($arg:tt)+) => {
234        let mut left_strings: Vec<String> = $left
235                .flattened_violations()
236                .into_iter()
237                .map(|x| format!("{:?}", x))
238                .collect();
239        left_strings.sort();
240        let mut right_strings: Vec<String> = $right
241                .flattened_violations()
242                .into_iter()
243                .map(|x| format!("{:?}", x))
244                .collect();
245        right_strings.sort();
246        assert_ne!(left_strings, right_strings, $($arg)+);
247    };
248}
249
250#[cfg(test)]
251mod violation_tests {
252    use crate::ion_path::{IonPath, IonPathElement};
253    use crate::violation::{Violation, ViolationCode};
254    use rstest::rstest;
255
256    #[rstest(violation1, violation2,
257    case::unordered_violations(Violation::with_violations(
258            "type_constraint",
259            ViolationCode::TypeMismatched,
260            "type mismatched",
261            &mut IonPath::default(),
262            vec![
263                Violation::new(
264                    "regex",
265                    ViolationCode::RegexMismatched,
266                    "regex mismatched",
267                    &mut IonPath::default(),
268                ),
269                Violation::new(
270                    "container_length",
271                    ViolationCode::InvalidLength,
272                    "invalid length",
273                    &mut IonPath::default(),
274                ),
275            ],
276        ), Violation::with_violations(
277            "type_constraint",
278            ViolationCode::TypeMismatched,
279            "type mismatched",
280            &mut IonPath::default(),
281            vec![
282                Violation::new(
283                    "container_length",
284                    ViolationCode::InvalidLength,
285                    "invalid length",
286                    &mut IonPath::default(),
287                ),
288                Violation::new(
289                    "regex",
290                    ViolationCode::RegexMismatched,
291                    "regex mismatched",
292                    &mut IonPath::default(),
293                ),
294            ],
295        )
296    ),
297    case::nested_violations(
298        Violation::with_violations(
299            "type_constraint",
300            ViolationCode::TypeMismatched,
301            "type mismatched",
302            &mut IonPath::default(),
303            vec![
304                Violation::with_violations(
305                    "regex",
306                    ViolationCode::RegexMismatched,
307                    "regex mismatched",
308                    &mut IonPath::default(),
309                    vec![
310                        Violation::new(
311                            "container_length",
312                            ViolationCode::InvalidLength,
313                            "invalid length",
314                            &mut IonPath::default(),
315                        ),
316                        Violation::new(
317                            "codepoint_length",
318                            ViolationCode::InvalidLength,
319                            "invalid length",
320                            &mut IonPath::default(),
321                        )
322                    ]
323                )
324            ],
325        ),
326        Violation::with_violations(
327            "type_constraint",
328            ViolationCode::TypeMismatched,
329            "type mismatched",
330            &mut IonPath::default(),
331            vec![
332                Violation::with_violations(
333                    "regex",
334                    ViolationCode::RegexMismatched,
335                    "regex mismatched",
336                    &mut IonPath::default(),
337                    vec![
338                        Violation::new(
339                            "codepoint_length",
340                            ViolationCode::InvalidLength,
341                            "invalid length",
342                            &mut IonPath::default(),
343                        ),
344                        Violation::new(
345                            "container_length",
346                            ViolationCode::InvalidLength,
347                            "invalid length",
348                            &mut IonPath::default(),
349                        )
350                    ]
351                )
352            ],
353        )
354    ),
355    case::empty_violations(
356        Violation::with_violations(
357            "type_constraint",
358            ViolationCode::TypeMismatched,
359            "type mismatched",
360            &mut IonPath::default(),
361            vec![],
362        ),
363        Violation::with_violations(
364            "type_constraint",
365            ViolationCode::TypeMismatched,
366            "type mismatched",
367            &mut IonPath::default(),
368            vec![],
369        )
370    ),
371    case::multiple_violations_from_one_constraint(
372        Violation::with_violations(
373        "type_constraint",
374        ViolationCode::TypeMismatched,
375        "type mismatched",
376        &mut IonPath::default(),
377        vec![
378            Violation::new(
379                "element",
380                ViolationCode::ElementMismatched,
381                "element mismatched",
382                &mut IonPath::default(),
383            ),
384            Violation::new(
385                "element",
386                ViolationCode::ElementNotDistinct,
387                "element not distinct",
388                &mut IonPath::default(),
389            ),
390        ],
391        ),
392    Violation::with_violations(
393        "type_constraint",
394        ViolationCode::TypeMismatched,
395        "type mismatched",
396        &mut IonPath::default(),
397        vec![
398            Violation::new(
399                "element",
400                ViolationCode::ElementNotDistinct,
401                "element not distinct",
402                &mut IonPath::default(),
403            ),
404            Violation::new(
405                "element",
406                ViolationCode::ElementMismatched,
407                "element mismatched",
408                &mut IonPath::default(),
409            ),
410        ],
411        ),
412    )
413    )]
414    fn violation_equivalence(violation1: Violation, violation2: Violation) {
415        assert_equivalent_violations!(violation1, violation2);
416    }
417
418    #[rstest(violation1, violation2,
419    case::different_violations(
420        Violation::with_violations(
421            "type_constraint",
422            ViolationCode::TypeMismatched,
423            "type mismatched",
424            &mut IonPath::default(),
425            vec![
426                Violation::new(
427                    "regex",
428                    ViolationCode::RegexMismatched,
429                    "regex mismatched",
430                    &mut IonPath::default(),
431                ),
432                Violation::new(
433                    "container_length",
434                    ViolationCode::InvalidLength,
435                    "invalid length",
436                    &mut IonPath::default(),
437                ),
438            ],
439        ), Violation::with_violations(
440            "type_constraint",
441            ViolationCode::TypeMismatched,
442            "type mismatched",
443            &mut IonPath::default(),
444            vec![
445            Violation::new(
446                "container_length",
447                ViolationCode::InvalidLength,
448                "invalid length",
449                &mut IonPath::default(),
450            ),
451            ],
452        )
453    ),
454    case::different_constraints(
455        Violation::new(
456            "type_constraint",
457            ViolationCode::TypeMismatched,
458            "type mismatched",
459            &mut IonPath::default(),
460        ),
461        Violation::new(
462            "regex",
463            ViolationCode::TypeMismatched,
464            "type mismatched",
465            &mut IonPath::default(),
466        )
467    ),
468    case::different_violation_code(
469        Violation::new(
470            "type_constraint",
471            ViolationCode::TypeMismatched,
472            "type mismatched",
473            &mut IonPath::default(),
474        ),
475        Violation::new(
476            "type_constraint",
477            ViolationCode::RegexMismatched,
478            "type mismatched",
479            &mut IonPath::default(),
480        )
481    ),
482    case::different_violation_message(
483        Violation::new(
484            "type_constraint",
485            ViolationCode::TypeMismatched,
486            "regex mismatched",
487            &mut IonPath::default(),
488        ),
489        Violation::new(
490            "type_constraint",
491            ViolationCode::TypeMismatched,
492            "type mismatched",
493            &mut IonPath::default(),
494        )
495    ),
496    case::different_ion_path(
497        Violation::new(
498            "type_constraint",
499            ViolationCode::TypeMismatched,
500            "type mismatched",
501            &mut IonPath::new(vec![IonPathElement::Index(2)]),
502        ),
503        Violation::new(
504            "type_constraint",
505            ViolationCode::TypeMismatched,
506            "type mismatched",
507            &mut IonPath::default(),
508        )
509    ))]
510    fn non_equivalent_violations(violation1: Violation, violation2: Violation) {
511        assert_non_equivalent_violations!(violation1, violation2);
512    }
513}