Skip to main content

ifc_validate/report/
finding.rs

1//! What a validator says when something is wrong.
2//!
3//! # Severity is about conformance, not about how annoyed you should be
4//!
5//! [`Severity::Error`] means the file violates the schema: a required
6//! attribute is absent, a reference points at nothing, a GUID is duplicated.
7//! [`Severity::Warning`] means the file is legal but suspicious.
8//! [`Severity::Unsupported`] means *this validator did not check* -- the rule
9//! exists in the schema and is not implemented here.
10//!
11//! That third variant is the important one. A validator that silently skips
12//! what it cannot evaluate reports a clean file and is worse than useless,
13//! because a clean report is exactly what a user acts on. Counting the
14//! unchecked rules is what makes "no errors" mean something.
15
16use std::fmt;
17
18use super::path::Path;
19
20/// How serious a finding is.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub enum Severity {
23    /// The file violates the schema.
24    Error,
25    /// Legal, but very likely a mistake.
26    Warning,
27    /// A rule this validator does not implement. Not a verdict on the file.
28    Unsupported,
29}
30
31impl fmt::Display for Severity {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        let text = match self {
34            Self::Error => "error",
35            Self::Warning => "warning",
36            Self::Unsupported => "unsupported",
37        };
38        formatter.write_str(text)
39    }
40}
41
42/// One thing a validator has to say about a file.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Finding {
45    /// How serious it is.
46    pub severity: Severity,
47    /// A stable identifier for the check that produced this, e.g.
48    /// `structure.reference.dangling` or `where.IfcRoot.WR1`. Callers filter
49    /// and suppress on this, so it is part of the contract.
50    pub rule: String,
51    /// Where the problem is.
52    pub path: Path,
53    /// What is wrong, in one sentence, without restating the rule id.
54    pub message: String,
55}
56
57impl Finding {
58    /// A schema violation.
59    #[must_use]
60    pub fn error(rule: impl Into<String>, path: Path, message: impl Into<String>) -> Self {
61        Self {
62            severity: Severity::Error,
63            rule: rule.into(),
64            path,
65            message: message.into(),
66        }
67    }
68
69    /// A legal but suspicious condition.
70    #[must_use]
71    pub fn warning(rule: impl Into<String>, path: Path, message: impl Into<String>) -> Self {
72        Self {
73            severity: Severity::Warning,
74            rule: rule.into(),
75            path,
76            message: message.into(),
77        }
78    }
79
80    /// A rule this validator did not evaluate.
81    #[must_use]
82    pub fn unsupported(rule: impl Into<String>, path: Path, message: impl Into<String>) -> Self {
83        Self {
84            severity: Severity::Unsupported,
85            rule: rule.into(),
86            path,
87            message: message.into(),
88        }
89    }
90}
91
92impl fmt::Display for Finding {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(
95            formatter,
96            "{}: {} at {}: {}",
97            self.severity, self.rule, self.path, self.message
98        )
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use ifc_model::EntityId;
106
107    /// Severity ordering is what report sorting relies on.
108    #[test]
109    fn errors_sort_before_warnings_before_unsupported() {
110        let mut severities = [Severity::Unsupported, Severity::Warning, Severity::Error];
111        severities.sort();
112        assert_eq!(
113            severities,
114            [Severity::Error, Severity::Warning, Severity::Unsupported]
115        );
116    }
117
118    /// A finding renders its path so a reader can find the entity.
119    #[test]
120    fn a_finding_names_where_it_applies() {
121        let finding = Finding::error(
122            "structure.dangling",
123            Path::Attribute {
124                entity: EntityId(12),
125                index: 3,
126                name: Some("Representation".into()),
127            },
128            "points at #99, which does not exist",
129        );
130        assert_eq!(finding.path.to_string(), "#12.Representation");
131        assert_eq!(finding.severity, Severity::Error);
132    }
133}