Skip to main content

ifc_resource/resource/
type.rs

1//! IFC4 `IfcConstructionResourceType` occurrence-kind projections.
2
3use ifc_model::EntityId;
4
5use crate::error::{ResourceError, ResourceResult};
6use crate::view::Record;
7
8/// Concrete `IfcConstructionResourceType` specialization, matching
9/// `ResourceKind` but for the type-level (catalog) entity rather than the
10/// occurrence.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ResourceTypeKind {
14    /// `IfcLaborResourceType`.
15    Labor,
16    /// `IfcConstructionEquipmentResourceType`.
17    Equipment,
18    /// `IfcCrewResourceType`.
19    Crew,
20    /// `IfcConstructionMaterialResourceType`.
21    Material,
22    /// `IfcConstructionProductResourceType`.
23    Product,
24    /// `IfcSubContractResourceType`.
25    Subcontract,
26}
27
28impl ResourceTypeKind {
29    fn from_type(type_name: &str) -> Option<Self> {
30        if type_name.eq_ignore_ascii_case("IfcLaborResourceType") {
31            Some(Self::Labor)
32        } else if type_name.eq_ignore_ascii_case("IfcConstructionEquipmentResourceType") {
33            Some(Self::Equipment)
34        } else if type_name.eq_ignore_ascii_case("IfcCrewResourceType") {
35            Some(Self::Crew)
36        } else if type_name.eq_ignore_ascii_case("IfcConstructionMaterialResourceType") {
37            Some(Self::Material)
38        } else if type_name.eq_ignore_ascii_case("IfcConstructionProductResourceType") {
39            Some(Self::Product)
40        } else if type_name.eq_ignore_ascii_case("IfcSubContractResourceType") {
41            Some(Self::Subcontract)
42        } else {
43            None
44        }
45    }
46}
47
48#[derive(Debug, Clone, Copy)]
49/// Borrowed projection of a concrete `IfcConstructionResourceType` catalog
50/// entity.
51pub struct ConstructionResourceType<'m, 's> {
52    record: Record<'m, 's>,
53    kind: ResourceTypeKind,
54}
55
56impl<'m, 's> ConstructionResourceType<'m, 's> {
57    pub(crate) fn from_record(record: Record<'m, 's>) -> ResourceResult<Self> {
58        let kind = ResourceTypeKind::from_type(&record.entity.type_name).ok_or_else(|| {
59            ResourceError::WrongType {
60                id: record.id,
61                expected: "concrete IfcConstructionResourceType occurrence",
62                actual: record.entity.type_name.to_string(),
63            }
64        })?;
65        Ok(Self { record, kind })
66    }
67
68    /// The entity id of the projected type.
69    #[must_use]
70    pub fn id(&self) -> EntityId {
71        self.record.id
72    }
73
74    /// Which concrete `IfcConstructionResourceType` subtype this is.
75    #[must_use]
76    pub fn kind(&self) -> ResourceTypeKind {
77        self.kind
78    }
79
80    /// The `Name` attribute.
81    pub fn name(&self) -> ResourceResult<&'m str> {
82        self.record.required_text("Name")
83    }
84
85    /// The `Identification` attribute, when authored.
86    pub fn identification(&self) -> ResourceResult<Option<&'m str>> {
87        self.record.optional_text("Identification")
88    }
89
90    /// The `LongDescription` attribute, when authored.
91    pub fn long_description(&self) -> ResourceResult<Option<&'m str>> {
92        self.record.optional_text("LongDescription")
93    }
94
95    /// The inherited `IfcTypeResource.ResourceType` label, when authored.
96    pub fn resource_type(&self) -> ResourceResult<Option<&'m str>> {
97        self.record.optional_text("ResourceType")
98    }
99
100    /// Enforces the shared `CorrectPredefinedType` WHERE rule across all six
101    /// concrete IFC4 resource-type entities: USERDEFINED requires the
102    /// inherited `IfcTypeResource.ResourceType` label.
103    pub fn predefined_type(&self) -> ResourceResult<Option<&'m str>> {
104        let value = self.record.optional_enum("PredefinedType")?;
105        if value.is_some_and(|value| value.eq_ignore_ascii_case("USERDEFINED"))
106            && self
107                .record
108                .optional_text("ResourceType")?
109                .is_none_or(|value| value.trim().is_empty())
110        {
111            return Err(ResourceError::SemanticViolation {
112                entity: Some(self.record.id),
113                rule: "CorrectPredefinedType requires ResourceType for USERDEFINED",
114            });
115        }
116        Ok(value)
117    }
118
119    /// The `BaseCosts` attribute: applied-value references, when authored.
120    pub fn base_costs(&self) -> ResourceResult<Vec<EntityId>> {
121        self.record
122            .refs("BaseCosts", "IfcAppliedValue", 1, true, false)
123    }
124
125    /// The `BaseQuantity` attribute: the physical-quantity reference, when
126    /// authored.
127    pub fn base_quantity(&self) -> ResourceResult<Option<EntityId>> {
128        self.record
129            .optional_ref("BaseQuantity", "IfcPhysicalQuantity")
130    }
131}
132
133impl<'m, 's> crate::view::ResourceView<'m, 's> {
134    /// Projects a resource-type catalog entry by entity id.
135    pub fn resource_type(&self, id: EntityId) -> ResourceResult<ConstructionResourceType<'m, 's>> {
136        ConstructionResourceType::from_record(self.record(id, "IfcConstructionResourceType")?)
137    }
138
139    /// Resolves the `IfcConstructionResourceType` assigned to a construction
140    /// resource occurrence through `IfcRelDefinesByType`, if any.
141    ///
142    /// `IfcTypeObject.Types` is an inverse `SET [0:1]`: an occurrence may be
143    /// typed by at most one relation. A second authored relation naming the
144    /// same occurrence is a modeling defect, reported as a typed refusal
145    /// rather than silently picking the first or last match.
146    pub fn assigned_resource_type(&self, occurrence: EntityId) -> ResourceResult<Option<EntityId>> {
147        self.resource(occurrence)?;
148        let mut found = None;
149        for relation in self.ids_of_ancestor("IfcRelDefinesByType") {
150            let record = self.record(relation, "IfcRelDefinesByType")?;
151            let related = record.refs("RelatedObjects", "IfcObject", 1, false, true)?;
152            if !related.contains(&occurrence) {
153                continue;
154            }
155            let relating_type =
156                record.required_ref("RelatingType", "IfcConstructionResourceType")?;
157            if let Some(existing) = found.replace(relating_type) {
158                if existing != relating_type {
159                    return Err(ResourceError::SemanticViolation {
160                        entity: Some(occurrence),
161                        rule: "IfcTypeObject.Types permits at most one type assignment",
162                    });
163                }
164            }
165        }
166        Ok(found)
167    }
168}