use std::collections::HashSet;
use crate::release::Release;
use crate::{ClassificationError, ClassificationResult};
use ifc_model::{Entity, EntityId, Model, Value};
#[derive(Debug, Clone, Copy)]
pub struct ClassificationView<'m> {
model: &'m Model,
}
impl<'m> ClassificationView<'m> {
#[must_use]
pub const fn new(model: &'m Model) -> Self {
Self { model }
}
#[must_use]
pub(crate) const fn model(self) -> &'m Model {
self.model
}
pub(crate) fn release(self) -> Release<'m> {
Release::of(self.model)
}
}
macro_rules! borrowed_entity {
($name:ident, $kind:literal) => {
#[doc = concat!("Borrowed projection of an `", $kind, "` entity.")]
#[doc = ""]
#[doc = "Projections handed out by a [`crate::ClassificationView`] read against"]
#[doc = "the release the model declares (IFC2X3, IFC4 or IFC4X3); one built with"]
#[doc = "`try_new` has no model and reads against IFC4, as in 0.2.0."]
#[derive(Debug, Clone, Copy)]
pub struct $name<'m> {
id: ifc_model::EntityId,
entity: &'m ifc_model::Entity,
release: crate::release::Release<'m>,
}
impl<'m> $name<'m> {
#[doc = concat!(
"Project `entity` as `",
$kind,
"`, checking its runtime type; fails with `WrongEntityType` if it is not."
)]
#[doc = ""]
#[doc = "Without a model there is no header to bind, so the projection reads"]
#[doc = "against IFC4. Use the view's lookups to read another release."]
pub fn try_new(
id: ifc_model::EntityId,
entity: &'m ifc_model::Entity,
) -> crate::ClassificationResult<Self> {
Self::try_bound(id, entity, crate::release::Release::LEGACY)
}
pub(crate) fn try_bound(
id: ifc_model::EntityId,
entity: &'m ifc_model::Entity,
release: crate::release::Release<'m>,
) -> crate::ClassificationResult<Self> {
if entity.is_type($kind) {
Ok(Self {
id,
entity,
release,
})
} else {
Err(crate::ClassificationError::WrongEntityType {
expected: $kind,
actual: entity.type_name.to_string(),
})
}
}
#[doc = concat!(
"Wrap `entity` as `",
$kind,
"` without re-checking its type; caller must already know it matches."
)]
pub(crate) const fn from_known(
id: ifc_model::EntityId,
entity: &'m ifc_model::Entity,
release: crate::release::Release<'m>,
) -> Self {
Self {
id,
entity,
release,
}
}
#[must_use]
pub const fn id(self) -> ifc_model::EntityId {
self.id
}
#[must_use]
pub const fn entity(self) -> &'m ifc_model::Entity {
self.entity
}
#[allow(dead_code)]
pub(crate) fn slot(
self,
attribute: &'static str,
) -> crate::ClassificationResult<usize> {
self.release.slot($kind, self.id, attribute)
}
#[allow(dead_code)]
pub(crate) fn text_slot(
self,
attribute: &'static str,
) -> crate::ClassificationResult<usize> {
self.release
.text_slot($kind, self.id, self.entity, attribute)
}
#[allow(dead_code)]
pub(crate) const fn release(self) -> crate::release::Release<'m> {
self.release
}
}
};
}
pub(crate) use borrowed_entity;
fn invalid(
entity: &'static str,
id: EntityId,
attribute: &'static str,
value: &Value,
) -> ClassificationError {
ClassificationError::InvalidValue {
entity,
id,
attribute,
value: format!("{value:?}"),
}
}
fn unique_refs(
kind: &'static str,
id: EntityId,
attr: &'static str,
values: &[Value],
) -> ClassificationResult<Vec<EntityId>> {
let mut seen = HashSet::new();
let mut out = Vec::with_capacity(values.len());
for value in values {
let Value::Ref(target) = value else {
return Err(invalid(kind, id, attr, value));
};
if !seen.insert(*target) {
return Err(ClassificationError::InvalidValue {
entity: kind,
id,
attribute: attr,
value: format!("duplicate reference {target}"),
});
}
out.push(*target);
}
Ok(out)
}
pub(crate) fn required_text<'m>(
kind: &'static str,
id: EntityId,
e: &'m Entity,
slot: usize,
attr: &'static str,
) -> ClassificationResult<&'m str> {
match e.attribute(slot) {
None | Some(Value::Null) => Err(ClassificationError::MissingAttribute {
entity: kind,
id,
attribute: attr,
}),
Some(v) => v
.unwrap_typed()
.as_text()
.ok_or_else(|| invalid(kind, id, attr, v)),
}
}
pub(crate) fn optional_text<'m>(
kind: &'static str,
id: EntityId,
e: &'m Entity,
slot: usize,
attr: &'static str,
) -> ClassificationResult<Option<&'m str>> {
match e.attribute(slot) {
None | Some(Value::Null) => Ok(None),
Some(v) => v
.unwrap_typed()
.as_text()
.map(Some)
.ok_or_else(|| invalid(kind, id, attr, v)),
}
}
pub(crate) fn optional_enum<'m>(
kind: &'static str,
id: EntityId,
e: &'m Entity,
slot: usize,
attr: &'static str,
allowed: &[&str],
) -> ClassificationResult<Option<&'m str>> {
match e.attribute(slot) {
None | Some(Value::Null) => Ok(None),
Some(Value::Enum(v)) if allowed.iter().any(|a| v.eq_ignore_ascii_case(a)) => Ok(Some(v)),
Some(v) => Err(invalid(kind, id, attr, v)),
}
}
pub(crate) fn optional_ref(
kind: &'static str,
id: EntityId,
e: &Entity,
slot: usize,
attr: &'static str,
) -> ClassificationResult<Option<EntityId>> {
match e.attribute(slot) {
None | Some(Value::Null) => Ok(None),
Some(Value::Ref(target)) => Ok(Some(*target)),
Some(v) => Err(invalid(kind, id, attr, v)),
}
}
pub(crate) fn required_ref(
kind: &'static str,
id: EntityId,
e: &Entity,
slot: usize,
attr: &'static str,
) -> ClassificationResult<EntityId> {
optional_ref(kind, id, e, slot, attr)?.ok_or(ClassificationError::MissingAttribute {
entity: kind,
id,
attribute: attr,
})
}
pub(crate) fn required_refs(
kind: &'static str,
id: EntityId,
e: &Entity,
slot: usize,
attr: &'static str,
) -> ClassificationResult<Vec<EntityId>> {
let value = e
.attribute(slot)
.ok_or(ClassificationError::MissingAttribute {
entity: kind,
id,
attribute: attr,
})?;
let Value::List(values) = value else {
return Err(invalid(kind, id, attr, value));
};
if values.is_empty() {
return Err(invalid(kind, id, attr, value));
}
unique_refs(kind, id, attr, values)
}
pub(crate) fn optional_refs(
kind: &'static str,
id: EntityId,
e: &Entity,
slot: usize,
attr: &'static str,
) -> ClassificationResult<Option<Vec<EntityId>>> {
match e.attribute(slot) {
None | Some(Value::Null) => Ok(None),
Some(Value::List(values)) if !values.is_empty() => {
unique_refs(kind, id, attr, values).map(Some)
}
Some(v) => Err(invalid(kind, id, attr, v)),
}
}
pub(crate) fn optional_texts<'m>(
kind: &'static str,
id: EntityId,
e: &'m Entity,
slot: usize,
attr: &'static str,
) -> ClassificationResult<Option<Vec<&'m str>>> {
match e.attribute(slot) {
None | Some(Value::Null) => Ok(None),
Some(Value::List(values)) if !values.is_empty() => values
.iter()
.map(|v| {
v.unwrap_typed()
.as_text()
.ok_or_else(|| invalid(kind, id, attr, v))
})
.collect::<ClassificationResult<Vec<_>>>()
.map(Some),
Some(v) => Err(invalid(kind, id, attr, v)),
}
}