use std::collections::HashSet;
use ifc_model::{Entity, EntityId, Model, Value};
use crate::release::{Binding, Layout};
use crate::{ApprovalError, ApprovalResult};
#[derive(Debug, Clone, Copy)]
pub struct ApprovalView<'m> {
model: &'m Model,
binding: Binding<'m>,
}
impl<'m> ApprovalView<'m> {
#[must_use]
pub fn new(model: &'m Model) -> Self {
Self {
model,
binding: Binding::of(model),
}
}
pub(crate) const fn model(self) -> &'m Model {
self.model
}
pub(crate) fn layout(self) -> ApprovalResult<Layout> {
self.binding.layout()
}
}
pub(crate) fn wrong(expected: &'static str, entity: &Entity) -> ApprovalError {
ApprovalError::WrongEntityType {
expected,
actual: entity.type_name.to_string(),
}
}
fn invalid(
kind: &'static str,
id: EntityId,
attribute: &'static str,
value: &Value,
) -> ApprovalError {
ApprovalError::InvalidValue {
entity: kind,
id,
attribute,
value: format!("{value:?}"),
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Record<'m> {
pub(crate) kind: &'static str,
pub(crate) id: EntityId,
pub(crate) entity: &'m Entity,
pub(crate) layout: Layout,
}
impl<'m> Record<'m> {
fn value(self, attribute: &'static str) -> ApprovalResult<Option<&'m Value>> {
let (slot, _) =
self.layout
.declared(self.kind, attribute)
.ok_or(ApprovalError::NotInSchema {
entity: self.kind,
id: self.id,
attribute,
schema: self.layout.version(),
})?;
Ok(match self.entity.attribute(slot) {
None | Some(Value::Null) => None,
Some(value) => Some(value),
})
}
pub(crate) fn declares(self, attribute: &'static str) -> bool {
self.layout.declared(self.kind, attribute).is_some()
}
fn admits_record(self, attribute: &'static str) -> bool {
self.layout
.declared(self.kind, attribute)
.is_some_and(|(_, declared)| self.layout.admits_entity(&declared.type_name, 8))
}
pub(crate) fn requires(self, attribute: &'static str) -> bool {
self.layout
.declared(self.kind, attribute)
.is_some_and(|(_, declared)| !declared.optional)
}
pub(crate) fn optional_text(self, attribute: &'static str) -> ApprovalResult<Option<&'m str>> {
match self.value(attribute)? {
None => Ok(None),
Some(Value::Ref(target)) if self.admits_record(attribute) => {
Err(ApprovalError::StructuredValue {
entity: self.kind,
id: self.id,
attribute,
target: *target,
})
}
Some(value) => value
.unwrap_typed()
.as_text()
.map(Some)
.ok_or_else(|| invalid(self.kind, self.id, attribute, value)),
}
}
pub(crate) fn required_text(self, attribute: &'static str) -> ApprovalResult<&'m str> {
self.optional_text(attribute)?
.ok_or(ApprovalError::MissingAttribute {
entity: self.kind,
id: self.id,
attribute,
})
}
pub(crate) fn optional_ref(self, attribute: &'static str) -> ApprovalResult<Option<EntityId>> {
match self.value(attribute)? {
None => Ok(None),
Some(Value::Ref(target)) => Ok(Some(*target)),
Some(value) => Err(invalid(self.kind, self.id, attribute, value)),
}
}
pub(crate) fn required_ref(self, attribute: &'static str) -> ApprovalResult<EntityId> {
self.optional_ref(attribute)?
.ok_or(ApprovalError::MissingAttribute {
entity: self.kind,
id: self.id,
attribute,
})
}
pub(crate) fn required_refs(self, attribute: &'static str) -> ApprovalResult<Vec<EntityId>> {
let value = self
.value(attribute)?
.ok_or(ApprovalError::MissingAttribute {
entity: self.kind,
id: self.id,
attribute,
})?;
let single = self
.layout
.declared(self.kind, attribute)
.is_some_and(|(_, declared)| !declared.aggregate);
let values = match value {
Value::Ref(target) if single => return Ok(vec![*target]),
Value::List(values) if !single && !values.is_empty() => values,
_ => return Err(invalid(self.kind, self.id, attribute, value)),
};
let mut seen = HashSet::new();
let mut out = Vec::with_capacity(values.len());
for item in values {
let Value::Ref(target) = item else {
return Err(invalid(self.kind, self.id, attribute, item));
};
if !seen.insert(*target) {
return Err(ApprovalError::InvalidValue {
entity: self.kind,
id: self.id,
attribute,
value: format!("duplicate {target}"),
});
}
out.push(*target);
}
Ok(out)
}
pub(crate) fn validate_target(
self,
model: &Model,
attribute: &'static str,
target: EntityId,
expected: &'static str,
) -> ApprovalResult<()> {
let actual = model.get(target).ok_or(ApprovalError::DanglingReference {
entity: self.kind,
id: self.id,
attribute,
target,
})?;
let accepted = if expected == "IfcDefinitionSelect" {
self.layout.is_definition(&actual.type_name)
} else {
self.layout
.schema()
.accepts_type(expected, &actual.type_name)
};
if accepted {
Ok(())
} else {
Err(ApprovalError::ReferenceType {
entity: self.kind,
id: self.id,
attribute,
target,
expected,
actual: actual.type_name.to_string(),
})
}
}
}