use std::collections::HashSet;
use std::sync::Arc;
use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use ifc_schema::Schema;
use crate::types::{
Benchmark, ConstraintGrade, LogicalOperator, MetricValueDraft, ObjectiveQualifier,
};
use crate::{ConstraintError, ConstraintResult};
const METRIC: &str = "IFCMETRIC";
const OBJECTIVE: &str = "IFCOBJECTIVE";
const RESOURCE_REL: &str = "IFCRESOURCECONSTRAINTRELATIONSHIP";
#[derive(Debug, Clone, Copy)]
pub struct ConstraintBaseDraft<'a> {
pub name: &'a str,
pub description: Option<&'a str>,
pub grade: ConstraintGrade,
pub source: Option<&'a str>,
pub creating_actor: Option<EntityId>,
pub creation_time: Option<&'a str>,
pub user_defined_grade: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct MetricDraft<'a> {
pub base: ConstraintBaseDraft<'a>,
pub benchmark: Benchmark,
pub value_source: Option<&'a str>,
pub data_value: Option<MetricValueDraft<'a>>,
pub reference_path: Option<EntityId>,
}
#[derive(Debug, Clone, Copy)]
pub struct ObjectiveDraft<'a> {
pub base: ConstraintBaseDraft<'a>,
pub benchmark_values: Option<&'a [EntityId]>,
pub logical_aggregator: Option<LogicalOperator>,
pub qualifier: ObjectiveQualifier,
pub user_defined_qualifier: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct ResourceConstraintDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub relating_constraint: EntityId,
pub related_resources: &'a [EntityId],
}
pub fn create_metric(
tx: &mut Transaction,
model: &Model,
draft: MetricDraft<'_>,
) -> ConstraintResult<EntityId> {
validate_base(tx, model, METRIC, draft.base)?;
if let Some(path) = draft.reference_path {
validate_target(tx, model, path, "IfcReference")?;
}
let data_value = match draft.data_value {
None => Value::Null,
Some(MetricValueDraft::Entity(target)) => {
validate_target(tx, model, target, "IfcMetricValueSelect")?;
Value::Ref(target)
}
Some(MetricValueDraft::Typed { type_name, value }) => {
if !ifc_schema::ifc4().accepts_type("IfcMetricValueSelect", type_name) {
return Err(ConstraintError::AuthoringInvalid {
entity: METRIC,
attribute: "DataValue",
value: format!("type {type_name} is outside IfcMetricValueSelect"),
});
}
Value::Typed {
type_name: Arc::from(type_name.to_ascii_uppercase()),
value: Box::new(value.clone()),
}
}
};
Ok(tx.create(Entity::new(
METRIC,
vec![
text(draft.base.name),
optional_text(draft.base.description),
enumeration(draft.base.grade.token()),
optional_text(draft.base.source),
optional_ref(draft.base.creating_actor),
optional_text(draft.base.creation_time),
optional_text(draft.base.user_defined_grade),
enumeration(draft.benchmark.token()),
optional_text(draft.value_source),
data_value,
optional_ref(draft.reference_path),
],
)))
}
pub fn create_objective(
tx: &mut Transaction,
model: &Model,
draft: ObjectiveDraft<'_>,
) -> ConstraintResult<EntityId> {
validate_base(tx, model, OBJECTIVE, draft.base)?;
if draft.qualifier == ObjectiveQualifier::UserDefined && draft.user_defined_qualifier.is_none()
{
return Err(ConstraintError::AuthoringInvalid {
entity: OBJECTIVE,
attribute: "WR21",
value: "USERDEFINED qualifier requires UserDefinedQualifier".into(),
});
}
let benchmarks = match draft.benchmark_values {
None => Value::Null,
Some([]) => {
return Err(ConstraintError::AuthoringInvalid {
entity: OBJECTIVE,
attribute: "BenchmarkValues",
value: "empty LIST [1:?]".into(),
});
}
Some(values) => {
for &target in values {
validate_target(tx, model, target, "IfcConstraint")?;
}
refs(values)
}
};
Ok(tx.create(Entity::new(
OBJECTIVE,
vec![
text(draft.base.name),
optional_text(draft.base.description),
enumeration(draft.base.grade.token()),
optional_text(draft.base.source),
optional_ref(draft.base.creating_actor),
optional_text(draft.base.creation_time),
optional_text(draft.base.user_defined_grade),
benchmarks,
draft
.logical_aggregator
.map_or(Value::Null, |value| enumeration(value.token())),
enumeration(draft.qualifier.token()),
optional_text(draft.user_defined_qualifier),
],
)))
}
pub fn relate_resource_constraint(
tx: &mut Transaction,
model: &Model,
draft: ResourceConstraintDraft<'_>,
) -> ConstraintResult<EntityId> {
validate_target(tx, model, draft.relating_constraint, "IfcConstraint")?;
validate_set(
tx,
model,
RESOURCE_REL,
"RelatedResourceObjects",
draft.related_resources,
"IfcResourceObjectSelect",
)?;
Ok(tx.create(Entity::new(
RESOURCE_REL,
vec![
optional_text(draft.name),
optional_text(draft.description),
Value::Ref(draft.relating_constraint),
refs(draft.related_resources),
],
)))
}
fn validate_base(
tx: &Transaction,
model: &Model,
kind: &'static str,
draft: ConstraintBaseDraft<'_>,
) -> ConstraintResult<()> {
if draft.grade == ConstraintGrade::UserDefined && draft.user_defined_grade.is_none() {
return Err(ConstraintError::AuthoringInvalid {
entity: kind,
attribute: "WR11",
value: "USERDEFINED grade requires UserDefinedGrade".into(),
});
}
if let Some(actor) = draft.creating_actor {
validate_target(tx, model, actor, "IfcActorSelect")?;
}
Ok(())
}
fn validate_set(
tx: &Transaction,
model: &Model,
kind: &'static str,
attribute: &'static str,
targets: &[EntityId],
expected: &'static str,
) -> ConstraintResult<()> {
if targets.is_empty() {
return Err(ConstraintError::AuthoringInvalid {
entity: kind,
attribute,
value: "empty SET [1:?]".into(),
});
}
let mut seen = HashSet::new();
for &target in targets {
if !seen.insert(target) {
return Err(ConstraintError::AuthoringInvalid {
entity: kind,
attribute,
value: format!("duplicate {target}"),
});
}
validate_target(tx, model, target, expected)?;
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct ReferenceDraft<'a> {
pub type_identifier: Option<&'a str>,
pub attribute_identifier: Option<&'a str>,
pub instance_name: Option<&'a str>,
pub list_positions: &'a [i64],
pub inner_reference: Option<EntityId>,
}
pub fn create_reference(
tx: &mut Transaction,
model: &Model,
draft: ReferenceDraft<'_>,
) -> ConstraintResult<EntityId> {
const ENTITY: &str = "IfcReference";
let empty = draft.type_identifier.is_none()
&& draft.attribute_identifier.is_none()
&& draft.instance_name.is_none()
&& draft.list_positions.is_empty()
&& draft.inner_reference.is_none();
if empty {
return Err(ConstraintError::AuthoringInvalid {
entity: ENTITY,
attribute: "TypeIdentifier",
value: "a reference with every slot unset addresses nothing".to_owned(),
});
}
for position in draft.list_positions {
if *position < 1 {
return Err(ConstraintError::AuthoringInvalid {
entity: ENTITY,
attribute: "ListPositions",
value: format!("{position} is not a 1-based list index"),
});
}
}
if let Some(inner) = draft.inner_reference {
validate_target(tx, model, inner, ENTITY)?;
}
let positions = if draft.list_positions.is_empty() {
Value::Null
} else {
Value::List(
draft
.list_positions
.iter()
.copied()
.map(Value::Integer)
.collect(),
)
};
Ok(tx.create(Entity::new(
"IFCREFERENCE",
vec![
optional_text(draft.type_identifier),
optional_text(draft.attribute_identifier),
optional_text(draft.instance_name),
positions,
draft.inner_reference.map_or(Value::Null, Value::Ref),
],
)))
}
fn validate_target(
tx: &Transaction,
model: &Model,
target: EntityId,
expected: &'static str,
) -> ConstraintResult<()> {
validate_target_in(ifc_schema::ifc4(), tx, model, target, expected)
}
pub(crate) fn validate_target_in(
schema: &Schema,
tx: &Transaction,
model: &Model,
target: EntityId,
expected: &'static str,
) -> ConstraintResult<()> {
let actual =
final_type(tx, model, target).ok_or(ConstraintError::UnknownEntity { id: target })?;
if schema.accepts_type(expected, actual) {
Ok(())
} else {
Err(ConstraintError::AuthoringReferenceType {
target,
expected,
actual: actual.into(),
})
}
}
pub(crate) fn final_type<'a>(
tx: &'a Transaction,
model: &'a Model,
id: EntityId,
) -> Option<&'a str> {
for edit in tx.edits().iter().rev() {
match edit {
Edit::Create {
id: edit_id,
entity,
} if *edit_id == id => return Some(&entity.type_name),
Edit::Remove { id: edit_id } if *edit_id == id => return None,
Edit::Retype {
id: edit_id,
type_name,
} if *edit_id == id => return Some(type_name),
_ => {}
}
}
model.get(id).map(|entity| entity.type_name.as_ref())
}
pub(crate) fn text(value: &str) -> Value {
Value::Text(Arc::from(value))
}
pub(crate) fn optional_text(value: Option<&str>) -> Value {
value.map_or(Value::Null, text)
}
pub(crate) fn optional_ref(value: Option<EntityId>) -> Value {
value.map_or(Value::Null, Value::Ref)
}
fn enumeration(value: &str) -> Value {
Value::Enum(Arc::from(value))
}
pub(crate) fn refs(values: &[EntityId]) -> Value {
Value::List(values.iter().copied().map(Value::Ref).collect())
}