Skip to main content

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 checks the aggregate bounds, nesting and element uniqueness of
37//! explicit attributes, and every `UNIQUE` clause of the declared release
38//! ([`structure`]). A bound written as an expression, and the bounds of an
39//! aggregate reached through a defined type, are not evaluated.
40//!
41//! It does not derive `INVERSE` relationship semantics: the schema tables
42//! record every INVERSE clause, but no cardinality over them is checked. A
43//! selected rule whose IFC2X3 form depends on an inverse is therefore
44//! reported unsupported, even when a later schema revision exposes
45//! equivalent direct attributes.
46//!
47//! # Module map
48//!
49//! | Module | Role |
50//! |---|---|
51//! | [`header`] | Declared schema and implementation level |
52//! | [`structure`] | References, required slots, cardinality, bounds, UNIQUE clauses |
53//! | [`type_check`] | Values against their declared EXPRESS types |
54//! | [`where_rule`] | Native rules, and honest reporting of the rest |
55//! | [`report`] | Findings, paths, severities, summaries |
56//! | [`error`] | Why validation could not run |
57
58pub mod error;
59pub mod header;
60pub mod report;
61pub mod structure;
62pub mod type_check;
63pub mod where_rule;
64
65pub use error::ValidateError;
66pub use report::{Finding, Path, Report, Severity, Summary};
67pub use where_rule::Budget;
68
69use ifc_model::Model;
70use ifc_schema::Schema;
71
72/// Validate a model against a schema with the default budget.
73///
74/// Runs every check this crate implements: header, structure, types, and the
75/// natively implemented rules. Unsupported rules are recorded rather than
76/// skipped.
77#[must_use]
78pub fn validate(model: &Model, schema: &Schema) -> Report {
79    validate_with(model, schema, Budget::DEFAULT)
80}
81
82/// Validate under an explicit budget.
83///
84/// A budget bounds how many findings are recorded. Hitting it marks the
85/// report truncated: `12 errors` from a truncated report means "at least 12".
86#[must_use]
87pub fn validate_with(model: &Model, schema: &Schema, budget: Budget) -> Report {
88    let mut report = Report::with_max_findings(budget.max_findings);
89    header::check(model, &mut report);
90    structure::check(model, schema, budget, &mut report);
91    type_check::check(model, schema, budget, &mut report);
92    where_rule::evaluate(model, schema, budget, &mut report);
93    report
94}
95
96/// Validate against the schema the file itself declares.
97///
98/// # Errors
99///
100/// Returns [`ValidateError`] when the file declares no schema, or declares
101/// one this build has no tables for. Validating an IFC2X3 file against IFC4
102/// tables would produce confident nonsense, so it is refused rather than
103/// approximated.
104#[cfg(feature = "ifc4")]
105pub fn validate_declared(model: &Model) -> Result<Report, ValidateError> {
106    use ifc_schema::SchemaVersion;
107
108    let token = model
109        .header()
110        .schema_token()
111        .ok_or(ValidateError::NoSchemaDeclared)?;
112    let version = SchemaVersion::from_header_token(token)
113        .ok_or_else(|| ValidateError::UnknownSchema(token.to_string()))?;
114    // `for_version` refuses a recognised schema this build does not bundle
115    // with `NotBundled`. Both cases are refusals, but they are different
116    // facts: one is "no idea what that token is", the other is "known schema,
117    // no tables".
118    let schema = ifc_schema::for_version(version)
119        .map_err(|_| ValidateError::UnbundledSchema(token.to_string()))?;
120    Ok(validate(model, schema))
121}