Skip to main content

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/// Equivalence for `Violation`s is not supported due to its tree structure of having children violations.
214/// This macro can be used for comparing if two violations are not equal and uses `flattened_violations` for the comparison.
215#[macro_export]
216macro_rules! assert_non_equivalent_violations {
217    ($left:expr, $right:expr $(,)?) => {
218        let mut left_strings: Vec<String> = $left
219                .flattened_violations()
220                .into_iter()
221                .map(|x| format!("{:?}", x))
222                .collect();
223        left_strings.sort();
224        let mut right_strings: Vec<String> = $right
225                .flattened_violations()
226                .into_iter()
227                .map(|x| format!("{:?}", x))
228                .collect();
229        right_strings.sort();
230        assert_ne!(left_strings, right_strings);
231    };
232    ($left:expr, $right:expr, $($arg:tt)+) => {
233        let mut left_strings: Vec<String> = $left
234                .flattened_violations()
235                .into_iter()
236                .map(|x| format!("{:?}", x))
237                .collect();
238        left_strings.sort();
239        let mut right_strings: Vec<String> = $right
240                .flattened_violations()
241                .into_iter()
242                .map(|x| format!("{:?}", x))
243                .collect();
244        right_strings.sort();
245        assert_ne!(left_strings, right_strings, $($arg)+);
246    };
247}
248
249#[cfg(test)]
250mod violation_tests {
251    use crate::ion_path::{IonPath, IonPathElement};
252    use crate::violation::{Violation, ViolationCode};
253    use rstest::rstest;
254
255    #[rstest(violation1, violation2,
256    case::unordered_violations(Violation::with_violations(
257            "type_constraint",
258            ViolationCode::TypeMismatched,
259            "type mismatched",
260            &mut IonPath::default(),
261            vec![
262                Violation::new(
263                    "regex",
264                    ViolationCode::RegexMismatched,
265                    "regex mismatched",
266                    &mut IonPath::default(),
267                ),
268                Violation::new(
269                    "container_length",
270                    ViolationCode::InvalidLength,
271                    "invalid length",
272                    &mut IonPath::default(),
273                ),
274            ],
275        ), Violation::with_violations(
276            "type_constraint",
277            ViolationCode::TypeMismatched,
278            "type mismatched",
279            &mut IonPath::default(),
280            vec![
281                Violation::new(
282                    "container_length",
283                    ViolationCode::InvalidLength,
284                    "invalid length",
285                    &mut IonPath::default(),
286                ),
287                Violation::new(
288                    "regex",
289                    ViolationCode::RegexMismatched,
290                    "regex mismatched",
291                    &mut IonPath::default(),
292                ),
293            ],
294        )
295    ),
296    case::nested_violations(
297        Violation::with_violations(
298            "type_constraint",
299            ViolationCode::TypeMismatched,
300            "type mismatched",
301            &mut IonPath::default(),
302            vec![
303                Violation::with_violations(
304                    "regex",
305                    ViolationCode::RegexMismatched,
306                    "regex mismatched",
307                    &mut IonPath::default(),
308                    vec![
309                        Violation::new(
310                            "container_length",
311                            ViolationCode::InvalidLength,
312                            "invalid length",
313                            &mut IonPath::default(),
314                        ),
315                        Violation::new(
316                            "codepoint_length",
317                            ViolationCode::InvalidLength,
318                            "invalid length",
319                            &mut IonPath::default(),
320                        )
321                    ]
322                )
323            ],
324        ),
325        Violation::with_violations(
326            "type_constraint",
327            ViolationCode::TypeMismatched,
328            "type mismatched",
329            &mut IonPath::default(),
330            vec![
331                Violation::with_violations(
332                    "regex",
333                    ViolationCode::RegexMismatched,
334                    "regex mismatched",
335                    &mut IonPath::default(),
336                    vec![
337                        Violation::new(
338                            "codepoint_length",
339                            ViolationCode::InvalidLength,
340                            "invalid length",
341                            &mut IonPath::default(),
342                        ),
343                        Violation::new(
344                            "container_length",
345                            ViolationCode::InvalidLength,
346                            "invalid length",
347                            &mut IonPath::default(),
348                        )
349                    ]
350                )
351            ],
352        )
353    ),
354    case::empty_violations(
355        Violation::with_violations(
356            "type_constraint",
357            ViolationCode::TypeMismatched,
358            "type mismatched",
359            &mut IonPath::default(),
360            vec![],
361        ),
362        Violation::with_violations(
363            "type_constraint",
364            ViolationCode::TypeMismatched,
365            "type mismatched",
366            &mut IonPath::default(),
367            vec![],
368        )
369    ),
370    case::multiple_violations_from_one_constraint(
371        Violation::with_violations(
372        "type_constraint",
373        ViolationCode::TypeMismatched,
374        "type mismatched",
375        &mut IonPath::default(),
376        vec![
377            Violation::new(
378                "element",
379                ViolationCode::ElementMismatched,
380                "element mismatched",
381                &mut IonPath::default(),
382            ),
383            Violation::new(
384                "element",
385                ViolationCode::ElementNotDistinct,
386                "element not distinct",
387                &mut IonPath::default(),
388            ),
389        ],
390        ),
391    Violation::with_violations(
392        "type_constraint",
393        ViolationCode::TypeMismatched,
394        "type mismatched",
395        &mut IonPath::default(),
396        vec![
397            Violation::new(
398                "element",
399                ViolationCode::ElementNotDistinct,
400                "element not distinct",
401                &mut IonPath::default(),
402            ),
403            Violation::new(
404                "element",
405                ViolationCode::ElementMismatched,
406                "element mismatched",
407                &mut IonPath::default(),
408            ),
409        ],
410        ),
411    )
412    )]
413    fn violation_equivalence(violation1: Violation, violation2: Violation) {
414        assert_equivalent_violations!(violation1, violation2);
415    }
416
417    #[rstest(violation1, violation2,
418    case::different_violations(
419        Violation::with_violations(
420            "type_constraint",
421            ViolationCode::TypeMismatched,
422            "type mismatched",
423            &mut IonPath::default(),
424            vec![
425                Violation::new(
426                    "regex",
427                    ViolationCode::RegexMismatched,
428                    "regex mismatched",
429                    &mut IonPath::default(),
430                ),
431                Violation::new(
432                    "container_length",
433                    ViolationCode::InvalidLength,
434                    "invalid length",
435                    &mut IonPath::default(),
436                ),
437            ],
438        ), Violation::with_violations(
439            "type_constraint",
440            ViolationCode::TypeMismatched,
441            "type mismatched",
442            &mut IonPath::default(),
443            vec![
444            Violation::new(
445                "container_length",
446                ViolationCode::InvalidLength,
447                "invalid length",
448                &mut IonPath::default(),
449            ),
450            ],
451        )
452    ),
453    case::different_constraints(
454        Violation::new(
455            "type_constraint",
456            ViolationCode::TypeMismatched,
457            "type mismatched",
458            &mut IonPath::default(),
459        ),
460        Violation::new(
461            "regex",
462            ViolationCode::TypeMismatched,
463            "type mismatched",
464            &mut IonPath::default(),
465        )
466    ),
467    case::different_violation_code(
468        Violation::new(
469            "type_constraint",
470            ViolationCode::TypeMismatched,
471            "type mismatched",
472            &mut IonPath::default(),
473        ),
474        Violation::new(
475            "type_constraint",
476            ViolationCode::RegexMismatched,
477            "type mismatched",
478            &mut IonPath::default(),
479        )
480    ),
481    case::different_violation_message(
482        Violation::new(
483            "type_constraint",
484            ViolationCode::TypeMismatched,
485            "regex mismatched",
486            &mut IonPath::default(),
487        ),
488        Violation::new(
489            "type_constraint",
490            ViolationCode::TypeMismatched,
491            "type mismatched",
492            &mut IonPath::default(),
493        )
494    ),
495    case::different_ion_path(
496        Violation::new(
497            "type_constraint",
498            ViolationCode::TypeMismatched,
499            "type mismatched",
500            &mut IonPath::new(vec![IonPathElement::Index(2)]),
501        ),
502        Violation::new(
503            "type_constraint",
504            ViolationCode::TypeMismatched,
505            "type mismatched",
506            &mut IonPath::default(),
507        )
508    ))]
509    fn non_equivalent_violations(violation1: Violation, violation2: Violation) {
510        assert_non_equivalent_violations!(violation1, violation2);
511    }
512}