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