use std::collections::HashSet;
use ifc_model::{Entity, EntityId, Model, Value};
use crate::release::{Binding, Layout};
use crate::{ConstraintError, ConstraintResult};
#[derive(Debug, Clone, Copy)]
pub struct ConstraintView<'m> {
model: &'m Model,
binding: Binding<'m>,
}
impl<'m> ConstraintView<'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) -> ConstraintResult<Layout> {
self.binding.layout()
}
}
pub(crate) fn wrong(expected: &'static str, entity: &Entity) -> ConstraintError {
ConstraintError::WrongEntityType {
expected,
actual: entity.type_name.to_string(),
}
}
pub(crate) fn invalid(
kind: &'static str,
id: EntityId,
attribute: &'static str,
value: &Value,
) -> ConstraintError {
ConstraintError::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> {
pub(crate) fn value(self, attribute: &'static str) -> ConstraintResult<Option<&'m Value>> {
let (slot, _) =
self.layout
.declared(self.kind, attribute)
.ok_or(ConstraintError::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()
}
pub(crate) 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 aggregate(self, attribute: &'static str) -> bool {
self.layout
.declared(self.kind, attribute)
.is_some_and(|(_, declared)| declared.aggregate)
}
pub(crate) fn declared_type(self, attribute: &'static str) -> &'static str {
self.layout
.declared(self.kind, attribute)
.map_or("", |(_, declared)| declared.type_name.as_str())
}
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,
) -> ConstraintResult<Option<&'m str>> {
match self.value(attribute)? {
None => Ok(None),
Some(Value::Ref(target)) if self.admits_record(attribute) => {
Err(ConstraintError::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) -> ConstraintResult<&'m str> {
self.optional_text(attribute)?
.ok_or(ConstraintError::MissingAttribute {
entity: self.kind,
id: self.id,
attribute,
})
}
pub(crate) fn optional_ref(
self,
attribute: &'static str,
) -> ConstraintResult<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) -> ConstraintResult<EntityId> {
self.optional_ref(attribute)?
.ok_or(ConstraintError::MissingAttribute {
entity: self.kind,
id: self.id,
attribute,
})
}
pub(crate) fn required_refs(self, attribute: &'static str) -> ConstraintResult<Vec<EntityId>> {
let value = self
.value(attribute)?
.ok_or(ConstraintError::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(ConstraintError::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,
) -> ConstraintResult<()> {
let actual = model
.get(target)
.ok_or(ConstraintError::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(ConstraintError::ReferenceType {
entity: self.kind,
id: self.id,
attribute,
target,
expected,
actual: actual.type_name.to_string(),
})
}
}
}