ifc_validate/lib.rs
1//! `ifc-validate` -- Schema and model validation: is this file actually legal IFC?
2//!
3//! Split from parsing on purpose. A reader that rejects everything imperfect is
4//! useless on real data -- roughly half of production files violate something --
5//! so parsing is permissive and validation is an explicit, separate pass.
6//!
7//! # What a report means
8//!
9//! ```no_run
10//! # use ifc_model::Model;
11//! # fn demo(model: &Model) {
12//! let report = ifc_validate::validate(model, ifc_schema::ifc4());
13//! if report.is_conformant() {
14//! // No *errors*. There may still be warnings, and there are certainly
15//! // rules this validator does not evaluate -- `report.summary()`
16//! // says how many.
17//! }
18//! # }
19//! ```
20//!
21//! The four severities are not a mood scale. [`Severity::Error`] means the
22//! file breaks a schema requirement; [`Severity::EvaluationError`] means an
23//! implemented rule applied to an instance but could not be decided for it;
24//! [`Severity::Warning`] means it is legal but will behave badly;
25//! [`Severity::Unsupported`] is a statement about *this validator*, not about
26//! the file. Errors and evaluation errors affect [`Report::is_conformant`]:
27//! a rule nobody could decide is not a rule that passed.
28//!
29//! # What this crate deliberately does not do
30//!
31//! It does not evaluate arbitrary EXPRESS `WHERE` expressions -- there is no
32//! expression evaluator here. Rules that need one are registered as
33//! unsupported and reported, so a clean report never silently means
34//! "unchecked". See [`where_rule::RULES`].
35//!
36//! It does not check aggregate bounds (`LIST [3:?]`), because the schema
37//! parser retains whether an attribute is an aggregate but not its bounds.
38//! Claiming otherwise would be worse than the gap.
39//!
40//! It also does not derive `INVERSE` relationship semantics. A selected rule
41//! whose IFC2X3 form depends on an inverse is therefore reported unsupported,
42//! even when a later schema revision exposes equivalent direct attributes.
43//!
44//! # Module map
45//!
46//! | Module | Role |
47//! |---|---|
48//! | [`header`] | Declared schema and implementation level |
49//! | [`structure`] | References, required slots, cardinality, unique ids |
50//! | [`type_check`] | Values against their declared EXPRESS types |
51//! | [`where_rule`] | Native rules, and honest reporting of the rest |
52//! | [`report`] | Findings, paths, severities, summaries |
53//! | [`error`] | Why validation could not run |
54
55pub mod error;
56pub mod header;
57pub mod report;
58pub mod structure;
59pub mod type_check;
60pub mod where_rule;
61
62pub use error::ValidateError;
63pub use report::{Finding, Path, Report, Severity, Summary};
64pub use where_rule::Budget;
65
66use ifc_model::Model;
67use ifc_schema::Schema;
68
69/// Validate a model against a schema with the default budget.
70///
71/// Runs every check this crate implements: header, structure, types, and the
72/// natively implemented rules. Unsupported rules are recorded rather than
73/// skipped.
74#[must_use]
75pub fn validate(model: &Model, schema: &Schema) -> Report {
76 validate_with(model, schema, Budget::DEFAULT)
77}
78
79/// Validate under an explicit budget.
80///
81/// A budget bounds how many findings are recorded. Hitting it marks the
82/// report truncated: `12 errors` from a truncated report means "at least 12".
83#[must_use]
84pub fn validate_with(model: &Model, schema: &Schema, budget: Budget) -> Report {
85 let mut report = Report::with_max_findings(budget.max_findings);
86 header::check(model, &mut report);
87 structure::check(model, schema, budget, &mut report);
88 type_check::check(model, schema, budget, &mut report);
89 where_rule::evaluate(model, schema, budget, &mut report);
90 report
91}
92
93/// Validate against the schema the file itself declares.
94///
95/// # Errors
96///
97/// Returns [`ValidateError`] when the file declares no schema, or declares
98/// one this build has no tables for. Validating an IFC2X3 file against IFC4
99/// tables would produce confident nonsense, so it is refused rather than
100/// approximated.
101#[cfg(feature = "ifc4")]
102pub fn validate_declared(model: &Model) -> Result<Report, ValidateError> {
103 use ifc_schema::SchemaVersion;
104
105 let token = model
106 .header()
107 .schema_token()
108 .ok_or(ValidateError::NoSchemaDeclared)?;
109 let version = SchemaVersion::from_header_token(token)
110 .ok_or_else(|| ValidateError::UnknownSchema(token.to_string()))?;
111 // `for_version` returns None for a recognised schema this build does not
112 // bundle. Both cases are refusals, but they are different facts: one is
113 // "no idea what that token is", the other is "known schema, no tables".
114 let schema = ifc_schema::for_version(version)
115 .ok_or_else(|| ValidateError::UnbundledSchema(token.to_string()))?;
116 Ok(validate(model, schema))
117}