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, 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/// Rows have four fields: `entity`, canonical entity name, nearest-first
23/// comma-separated supertypes, and inherited-first comma-separated Part 21
24/// attribute names. A `-` is the empty-sequence marker; IFC declaration names
25/// cannot contain the delimiters used by this format.
26pub fn write_structural_catalog(
27    version: SchemaVersion,
28    mut output: impl Write,
29) -> io::Result<StructuralCatalogSummary> {
30    let schema = for_version(version).expect("every SchemaVersion has a bundled schema");
31    let entity_count = schema.entity_count();
32    let type_count = schema.type_count();
33
34    assert_eq!(entity_count, version.expected_entity_count());
35    assert_eq!(type_count, version.expected_type_count());
36
37    writeln!(output, "# openbim.ifc structural-catalog v1")?;
38    writeln!(
39        output,
40        "schema\t{}\t{}\t{}",
41        version.release_id(),
42        entity_count,
43        type_count
44    )?;
45
46    let mut names = schema.entity_names().collect::<Vec<_>>();
47    names.sort_unstable();
48    for name in &names {
49        assert_catalog_token(name);
50        let supertypes = schema.supertypes(name);
51        let attributes = schema.attribute_names(name);
52        for value in supertypes.iter().chain(attributes.iter()) {
53            assert_catalog_token(value);
54        }
55        writeln!(
56            output,
57            "entity\t{}\t{}\t{}",
58            name,
59            joined_or_dash(&supertypes),
60            joined_or_dash(&attributes)
61        )?;
62    }
63
64    Ok(StructuralCatalogSummary {
65        entity_rows: names.len(),
66        type_count,
67    })
68}
69
70/// Write direct IFC entity declarations without repeated inherited structure.
71///
72/// Rows have four fields: `entity`, canonical name, immediate supertype, and
73/// directly declared Part 21 attribute names. `-` marks an absent parent or
74/// empty declaration list. Consumers reconstruct release-specific ancestry and
75/// inherited slots by following immediate parents in the same catalog.
76pub fn write_direct_structural_catalog(
77    version: SchemaVersion,
78    mut output: impl Write,
79) -> io::Result<StructuralCatalogSummary> {
80    let schema = for_version(version).expect("every SchemaVersion has a bundled schema");
81    let entity_count = schema.entity_count();
82    let type_count = schema.type_count();
83    assert_eq!(entity_count, version.expected_entity_count());
84    assert_eq!(type_count, version.expected_type_count());
85
86    writeln!(output, "# openbim.ifc direct-structural-catalog v1")?;
87    writeln!(
88        output,
89        "schema\t{}\t{}\t{}",
90        version.release_id(),
91        entity_count,
92        type_count
93    )?;
94
95    let mut names = schema.entity_names().collect::<Vec<_>>();
96    names.sort_unstable();
97    for name in &names {
98        assert_catalog_token(name);
99        let supertypes = schema.supertypes(name);
100        let attributes = schema.attribute_names(name);
101        let parent = supertypes.first().copied();
102        let inherited_count = if let Some(parent) = parent {
103            let parent_supertypes = schema.supertypes(parent);
104            assert_eq!(supertypes[1..], parent_supertypes);
105            let parent_attributes = schema.attribute_names(parent);
106            assert!(
107                attributes.starts_with(&parent_attributes),
108                "{name} attributes must begin with all inherited {parent} attributes"
109            );
110            parent_attributes.len()
111        } else {
112            0
113        };
114        let declared = &attributes[inherited_count..];
115        for value in parent.iter().copied().chain(declared.iter().copied()) {
116            assert_catalog_token(value);
117        }
118        writeln!(
119            output,
120            "entity\t{}\t{}\t{}",
121            name,
122            parent.unwrap_or("-"),
123            joined_or_dash(declared)
124        )?;
125    }
126
127    Ok(StructuralCatalogSummary {
128        entity_rows: names.len(),
129        type_count,
130    })
131}
132
133fn joined_or_dash(values: &[&str]) -> String {
134    if values.is_empty() {
135        "-".to_owned()
136    } else {
137        values.join(",")
138    }
139}
140
141fn assert_catalog_token(value: &str) {
142    assert!(
143        !value.contains(['\t', '\n', '\r', ',']),
144        "IFC declaration token contains a structural-catalog delimiter: {value:?}"
145    );
146}