use ifc_model::{Entity, Value};
use ifc_schema::Schema;
use crate::check::{describe_value, value_matches};
use crate::error::{AuthorError, AuthorResult};
#[derive(Debug, Clone)]
pub struct EntityBuilder<'a> {
schema: &'a Schema,
entity: String,
set: Vec<(String, Value)>,
}
impl<'a> EntityBuilder<'a> {
pub fn new(schema: &'a Schema, entity: impl Into<String>) -> Self {
Self {
schema,
entity: entity.into(),
set: Vec::new(),
}
}
#[must_use]
pub fn set(mut self, attribute: impl Into<String>, value: Value) -> Self {
self.set.push((attribute.into(), value));
self
}
pub fn text(self, attribute: impl Into<String>, text: impl Into<std::sync::Arc<str>>) -> Self {
self.set(attribute, Value::Text(text.into()))
}
pub fn real(self, attribute: impl Into<String>, value: f64) -> Self {
self.set(attribute, Value::Real(value))
}
pub fn reference(self, attribute: impl Into<String>, id: ifc_model::EntityId) -> Self {
self.set(attribute, Value::Ref(id))
}
pub fn enumeration(
self,
attribute: impl Into<String>,
constant: impl Into<std::sync::Arc<str>>,
) -> Self {
self.set(attribute, Value::Enum(constant.into()))
}
}
impl EntityBuilder<'_> {
pub fn build(self) -> AuthorResult<Entity> {
let declared = self.schema.attributes(&self.entity);
if declared.is_empty() && self.schema.entity(&self.entity).is_none() {
return Err(AuthorError::UnknownEntity {
entity: self.entity,
});
}
let mut slots = vec![Value::Null; declared.len()];
let mut filled = vec![false; declared.len()];
for (name, value) in &self.set {
let Some(index) = declared
.iter()
.position(|a| a.name.eq_ignore_ascii_case(name))
else {
return Err(AuthorError::UnknownAttribute {
entity: self.entity.clone(),
attribute: name.clone(),
known: declared.iter().map(|a| a.name.clone()).collect(),
});
};
if filled[index] {
return Err(AuthorError::DuplicateAttribute {
entity: self.entity.clone(),
attribute: declared[index].name.clone(),
});
}
check_value(self.schema, &self.entity, declared[index], value)?;
slots[index] = value.clone();
filled[index] = true;
}
for (index, attribute) in declared.iter().enumerate() {
if !filled[index] && !attribute.optional {
return Err(AuthorError::MissingRequired {
entity: self.entity.clone(),
attribute: attribute.name.clone(),
});
}
}
Ok(Entity::new(self.entity.to_ascii_uppercase(), slots))
}
pub fn insert(self, model: &mut ifc_model::Model) -> AuthorResult<ifc_model::EntityId> {
let entity = self.build()?;
Ok(model.push(entity))
}
}
pub(crate) fn check_value(
schema: &Schema,
entity: &str,
attribute: &ifc_schema::Attribute,
value: &Value,
) -> AuthorResult<()> {
if !matches!(value, Value::Null | Value::Derived) {
let supplied_aggregate = matches!(value, Value::List(_));
if supplied_aggregate != attribute.aggregate {
return Err(AuthorError::AggregateMismatch {
entity: entity.to_owned(),
attribute: attribute.name.clone(),
expected_aggregate: attribute.aggregate,
});
}
}
if attribute.name.eq_ignore_ascii_case("GlobalId") {
if let Value::Text(text) = value {
if ifc_model::guid::Guid::parse(text).is_none() {
return Err(AuthorError::InvalidGlobalId {
entity: entity.to_owned(),
found: text.to_string(),
});
}
}
}
let admissible = match value {
Value::List(items) => items
.iter()
.all(|item| value_matches(schema, &attribute.type_name, item)),
scalar => value_matches(schema, &attribute.type_name, scalar),
};
if !admissible {
return Err(AuthorError::TypeMismatch {
entity: entity.to_owned(),
attribute: attribute.name.clone(),
expected: attribute.type_name.clone(),
found: describe_value(value),
});
}
Ok(())
}