use std::collections::HashSet;
use ifc_model::guid::Guid;
use ifc_model::{Entity, EntityId, Model, Transaction, Value};
use ifc_schema::Schema;
use crate::authoring::ControlKind;
use crate::error::{ControlError, ControlResult};
use crate::release::{bind, projected_type, Release};
const RELATION: &str = "IFCRELASSIGNSTOCONTROL";
#[derive(Debug, Clone, Copy)]
pub struct ControlAssignmentDraft<'a> {
pub global_id: &'a str,
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub control: EntityId,
pub related_objects: &'a [EntityId],
}
pub fn assign_to_control(
tx: &mut Transaction,
model: &Model,
schema: &Schema,
draft: ControlAssignmentDraft<'_>,
) -> ControlResult<EntityId> {
let record = assignment_record(tx, model, Release::of_schema(schema), draft, Value::Null)?;
Ok(tx.create(record))
}
pub fn assign_to_control_with_owner_history(
tx: &mut Transaction,
model: &Model,
draft: ControlAssignmentDraft<'_>,
owner_history: EntityId,
) -> ControlResult<EntityId> {
let release = bind(model)?;
let record = assignment_record(tx, model, release, draft, Value::Ref(owner_history))?;
release.require_owner_history(tx, model, RELATION, owner_history)?;
Ok(tx.create(record))
}
fn assignment_record(
tx: &Transaction,
model: &Model,
release: Release<'_>,
draft: ControlAssignmentDraft<'_>,
owner_history: Value,
) -> ControlResult<Entity> {
let schema = release.schema();
if schema.attributes(RELATION).is_empty() {
return Err(ControlError::UnsupportedEntity {
schema: schema.name().to_owned(),
entity: RELATION,
});
}
if Guid::parse(draft.global_id).is_none() {
return Err(invalid("GlobalId", draft.global_id));
}
let control_type = projected_type(tx, model, draft.control)
.ok_or(ControlError::UnknownEntity { id: draft.control })?;
if !ControlKind::ALL
.iter()
.any(|kind| control_type.eq_ignore_ascii_case(kind.type_name()))
{
return Err(ControlError::ForeignControl {
id: draft.control,
actual: control_type,
});
}
if draft.related_objects.is_empty() {
return Err(invalid("RelatedObjects", "expected at least one member"));
}
let mut seen = HashSet::new();
for &object in draft.related_objects {
if !seen.insert(object) {
return Err(invalid(
"RelatedObjects",
format!("{object} is listed twice"),
));
}
if object == draft.control {
return Err(invalid(
"RelatedObjects",
format!("{object} is the relating control"),
));
}
let actual =
projected_type(tx, model, object).ok_or(ControlError::UnknownEntity { id: object })?;
if !schema.is_a(&actual, "IFCOBJECTDEFINITION") {
return Err(invalid(
"RelatedObjects",
format!("{object} is {actual}, not an IfcObjectDefinition"),
));
}
}
release.record(
RELATION,
vec![
("GlobalId", Value::Text(draft.global_id.into())),
("OwnerHistory", owner_history),
("Name", text(draft.name)),
("Description", text(draft.description)),
(
"RelatedObjects",
Value::List(
draft
.related_objects
.iter()
.copied()
.map(Value::Ref)
.collect(),
),
),
("RelatedObjectsType", Value::Null),
("RelatingControl", Value::Ref(draft.control)),
],
)
}
fn invalid(attribute: &'static str, value: impl Into<String>) -> ControlError {
ControlError::AuthoringInvalid {
entity: RELATION,
attribute,
value: value.into(),
}
}
fn text(value: Option<&str>) -> Value {
value.map_or(Value::Null, |v| Value::Text(v.into()))
}