Skip to main content

ifc_schema/
export.rs

1//! Deterministic, schema-derived structural catalog export.
2//!
3//! The export contains only release identity, entity names, inheritance, and
4//! ordered attribute-slot names. It deliberately excludes normative prose and
5//! never substitutes one IFC release for another.
6
7use std::io::{self, Write};
8
9use crate::{for_version, Schema, SchemaVersion};
10
11/// Counts written to one structural catalog.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct StructuralCatalogSummary {
14    /// Number of entity rows written.
15    pub entity_rows: usize,
16    /// Number of defined types declared by the source schema.
17    pub type_count: usize,
18}
19
20/// Write a deterministic tab-separated structural catalog for a bundled IFC release.
21///
22/// # Errors
23///
24/// An I/O error from `output`, or one of kind `Unsupported` wrapping
25/// [`crate::NotBundled`] when `version`'s release feature is off.
26///
27/// Rows have four fields: `entity`, canonical entity name, nearest-first
28/// comma-separated supertypes, and inherited-first comma-separated Part 21
29/// attribute names. A `-` is the empty-sequence marker; IFC declaration names
30/// cannot contain the delimiters used by this format.
31pub fn write_structural_catalog(
32    version: SchemaVersion,
33    mut output: impl Write,
34) -> io::Result<StructuralCatalogSummary> {
35    let schema = bundled(version)?;
36    let entity_count = schema.entity_count();
37    let type_count = schema.type_count();
38
39    assert_eq!(entity_count, version.expected_entity_count());
40    assert_eq!(type_count, version.expected_type_count());
41
42    writeln!(output, "# openbim.ifc structural-catalog v1")?;
43    writeln!(
44        output,
45        "schema\t{}\t{}\t{}",
46        version.release_id(),
47        entity_count,
48        type_count
49    )?;
50
51    let mut names = schema.entity_names().collect::<Vec<_>>();
52    names.sort_unstable();
53    for name in &names {
54        assert_catalog_token(name);
55        let supertypes = schema.supertypes(name);
56        let attributes = schema.attribute_names(name);
57        for value in supertypes.iter().chain(attributes.iter()) {
58            assert_catalog_token(value);
59        }
60        writeln!(
61            output,
62            "entity\t{}\t{}\t{}",
63            name,
64            joined_or_dash(&supertypes),
65            joined_or_dash(&attributes)
66        )?;
67    }
68
69    Ok(StructuralCatalogSummary {
70        entity_rows: names.len(),
71        type_count,
72    })
73}
74
75/// Write direct IFC entity declarations without repeated inherited structure.
76///
77/// # Errors
78///
79/// As [`write_structural_catalog`].
80///
81/// Rows have four fields: `entity`, canonical name, immediate supertype, and
82/// directly declared Part 21 attribute names. `-` marks an absent parent or
83/// empty declaration list. Consumers reconstruct release-specific ancestry and
84/// inherited slots by following immediate parents in the same catalog.
85pub fn write_direct_structural_catalog(
86    version: SchemaVersion,
87    mut output: impl Write,
88) -> io::Result<StructuralCatalogSummary> {
89    let schema = bundled(version)?;
90    let entity_count = schema.entity_count();
91    let type_count = schema.type_count();
92    assert_eq!(entity_count, version.expected_entity_count());
93    assert_eq!(type_count, version.expected_type_count());
94
95    writeln!(output, "# openbim.ifc direct-structural-catalog v1")?;
96    writeln!(
97        output,
98        "schema\t{}\t{}\t{}",
99        version.release_id(),
100        entity_count,
101        type_count
102    )?;
103
104    let mut names = schema.entity_names().collect::<Vec<_>>();
105    names.sort_unstable();
106    for name in &names {
107        assert_catalog_token(name);
108        let supertypes = schema.supertypes(name);
109        let attributes = schema.attribute_names(name);
110        let parent = supertypes.first().copied();
111        let inherited_count = if let Some(parent) = parent {
112            let parent_supertypes = schema.supertypes(parent);
113            assert_eq!(supertypes[1..], parent_supertypes);
114            let parent_attributes = schema.attribute_names(parent);
115            assert!(
116                attributes.starts_with(&parent_attributes),
117                "{name} attributes must begin with all inherited {parent} attributes"
118            );
119            parent_attributes.len()
120        } else {
121            0
122        };
123        let declared = &attributes[inherited_count..];
124        for value in parent.iter().copied().chain(declared.iter().copied()) {
125            assert_catalog_token(value);
126        }
127        writeln!(
128            output,
129            "entity\t{}\t{}\t{}",
130            name,
131            parent.unwrap_or("-"),
132            joined_or_dash(declared)
133        )?;
134    }
135
136    Ok(StructuralCatalogSummary {
137        entity_rows: names.len(),
138        type_count,
139    })
140}
141
142/// The bundled table for `version`, or an `Unsupported` I/O error wrapping
143/// [`crate::NotBundled`] when this build does not compile it in.
144fn bundled(version: SchemaVersion) -> io::Result<&'static Schema> {
145    for_version(version).map_err(|refused| io::Error::new(io::ErrorKind::Unsupported, refused))
146}
147
148fn joined_or_dash(values: &[&str]) -> String {
149    if values.is_empty() {
150        "-".to_owned()
151    } else {
152        values.join(",")
153    }
154}
155
156fn assert_catalog_token(value: &str) {
157    assert!(
158        !value.contains(['\t', '\n', '\r', ',']),
159        "IFC declaration token contains a structural-catalog delimiter: {value:?}"
160    );
161}