use ifc_model::guid::Guid;
use ifc_model::{Entity, EntityId, Value};
use ifc_schema::SchemaVersion;
use crate::release::Layout;
use crate::types::{Benchmark, ConstraintGrade, LogicalOperator, MetricValue, ObjectiveQualifier};
use crate::view::{invalid, wrong, ConstraintView, Record};
use crate::{ConstraintError, ConstraintResult};
const METRIC: &str = "IFCMETRIC";
const OBJECTIVE: &str = "IFCOBJECTIVE";
const RESOURCE_REL: &str = "IFCRESOURCECONSTRAINTRELATIONSHIP";
const ASSIGNMENT: &str = "IFCRELASSOCIATESCONSTRAINT";
macro_rules! projection {
($name:ident, $kind:expr) => {
#[derive(Debug, Clone, Copy)]
pub struct $name<'m> {
record: Record<'m>,
}
impl<'m> $name<'m> {
pub fn try_new(
id: EntityId,
entity: &'m Entity,
release: SchemaVersion,
) -> ConstraintResult<Self> {
Self::bound(id, entity, Layout::of_version(release)?)
}
pub(crate) fn bound(
id: EntityId,
entity: &'m Entity,
layout: Layout,
) -> ConstraintResult<Self> {
if !entity.is_type($kind) {
return Err(wrong($kind, entity));
}
layout.require_entity($kind)?;
Ok(Self {
record: Record {
kind: $kind,
id,
entity,
layout,
},
})
}
#[must_use]
pub const fn id(self) -> EntityId {
self.record.id
}
#[must_use]
pub const fn release(self) -> SchemaVersion {
self.record.layout.version()
}
}
};
}
projection!(Metric, METRIC);
projection!(Objective, OBJECTIVE);
projection!(ResourceConstraintRelationship, RESOURCE_REL);
projection!(ConstraintAssignment, ASSIGNMENT);
fn enumeration<T>(
record: Record<'_>,
attribute: &'static str,
parse: fn(&str) -> Option<T>,
) -> ConstraintResult<Option<T>> {
match record.value(attribute)? {
None => Ok(None),
Some(Value::Enum(value)) => parse(value).map(Some).ok_or_else(|| {
invalid(
record.kind,
record.id,
attribute,
&Value::Enum(value.clone()),
)
}),
Some(value) => Err(invalid(record.kind, record.id, attribute, value)),
}
}
fn required<T>(
record: Record<'_>,
attribute: &'static str,
value: Option<T>,
) -> ConstraintResult<T> {
value.ok_or(ConstraintError::MissingAttribute {
entity: record.kind,
id: record.id,
attribute,
})
}
fn validate_base(view: ConstraintView<'_>, record: Record<'_>) -> ConstraintResult<()> {
record.required_text("Name")?;
let grade = required(
record,
"ConstraintGrade",
enumeration(record, "ConstraintGrade", ConstraintGrade::parse)?,
)?;
if grade == ConstraintGrade::UserDefined && record.optional_text("UserDefinedGrade")?.is_none()
{
return Err(ConstraintError::Semantic {
entity: record.kind,
id: record.id,
rule: "WR11",
detail: "USERDEFINED grade requires UserDefinedGrade".into(),
});
}
if let Some(actor) = record.optional_ref("CreatingActor")? {
record.validate_target(view.model(), "CreatingActor", actor, "IfcActorSelect")?;
}
match record.optional_text("CreationTime") {
Ok(_) => {}
Err(ConstraintError::StructuredValue { target, .. }) => {
record.validate_target(view.model(), "CreationTime", target, "IfcDateTimeSelect")?;
}
Err(error) => return Err(error),
}
Ok(())
}
macro_rules! base_accessors {
($name:ident) => {
impl<'m> $name<'m> {
pub fn name(self) -> ConstraintResult<&'m str> {
self.record.required_text("Name")
}
pub fn description(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("Description")
}
pub fn grade(self) -> ConstraintResult<ConstraintGrade> {
required(
self.record,
"ConstraintGrade",
enumeration(self.record, "ConstraintGrade", ConstraintGrade::parse)?,
)
}
pub fn source(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("ConstraintSource")
}
pub fn creating_actor(self) -> ConstraintResult<Option<EntityId>> {
self.record.optional_ref("CreatingActor")
}
pub fn creation_time(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("CreationTime")
}
pub fn user_defined_grade(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("UserDefinedGrade")
}
}
};
}
base_accessors!(Metric);
base_accessors!(Objective);
impl<'m> Metric<'m> {
pub fn benchmark(self) -> ConstraintResult<Benchmark> {
required(
self.record,
"Benchmark",
enumeration(self.record, "Benchmark", Benchmark::parse)?,
)
}
pub fn value_source(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("ValueSource")
}
pub fn data_value(self) -> ConstraintResult<Option<MetricValue<'m>>> {
let record = self.record;
match record.value("DataValue")? {
None => Ok(None),
Some(Value::Ref(target)) => Ok(Some(MetricValue::Entity(*target))),
Some(Value::Typed { type_name, value })
if record
.layout
.schema()
.accepts_type("IfcMetricValueSelect", type_name) =>
{
Ok(Some(MetricValue::Typed {
type_name,
value: value.as_ref(),
}))
}
Some(value) => Err(invalid(METRIC, record.id, "DataValue", value)),
}
}
pub fn reference_path(self) -> ConstraintResult<Option<EntityId>> {
self.record.optional_ref("ReferencePath")
}
fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
let record = self.record;
validate_base(view, record)?;
self.benchmark()?;
match self.data_value()? {
None if record.requires("DataValue") => {
return Err(ConstraintError::MissingAttribute {
entity: METRIC,
id: record.id,
attribute: "DataValue",
})
}
Some(MetricValue::Entity(target)) => {
record.validate_target(
view.model(),
"DataValue",
target,
"IfcMetricValueSelect",
)?;
}
_ => {}
}
if record.declares("ReferencePath") {
if let Some(reference) = self.reference_path()? {
record.validate_target(view.model(), "ReferencePath", reference, "IfcReference")?;
}
}
Ok(self)
}
}
impl<'m> Objective<'m> {
pub fn benchmark_values(self) -> ConstraintResult<Option<Vec<EntityId>>> {
let record = self.record;
match record.value("BenchmarkValues")? {
None => Ok(None),
Some(Value::Ref(target)) if !record.aggregate("BenchmarkValues") => {
Ok(Some(vec![*target]))
}
Some(Value::List(values))
if record.aggregate("BenchmarkValues") && !values.is_empty() =>
{
values
.iter()
.map(|value| match value {
Value::Ref(target) => Ok(*target),
other => Err(invalid(OBJECTIVE, record.id, "BenchmarkValues", other)),
})
.collect::<ConstraintResult<Vec<_>>>()
.map(Some)
}
Some(value) => Err(invalid(OBJECTIVE, record.id, "BenchmarkValues", value)),
}
}
pub fn logical_aggregator(self) -> ConstraintResult<Option<LogicalOperator>> {
enumeration(self.record, "LogicalAggregator", LogicalOperator::parse)
}
pub fn qualifier(self) -> ConstraintResult<ObjectiveQualifier> {
required(
self.record,
"ObjectiveQualifier",
enumeration(self.record, "ObjectiveQualifier", ObjectiveQualifier::parse)?,
)
}
pub fn user_defined_qualifier(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("UserDefinedQualifier")
}
fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
let record = self.record;
validate_base(view, record)?;
if record.declares("LogicalAggregator") {
self.logical_aggregator()?;
}
if self.qualifier()? == ObjectiveQualifier::UserDefined
&& self.user_defined_qualifier()?.is_none()
{
return Err(ConstraintError::Semantic {
entity: OBJECTIVE,
id: record.id,
rule: "WR21",
detail: "USERDEFINED qualifier requires UserDefinedQualifier".into(),
});
}
if let Some(values) = self.benchmark_values()? {
let expected = record.declared_type("BenchmarkValues");
for target in values {
record.validate_target(view.model(), "BenchmarkValues", target, expected)?;
}
}
Ok(self)
}
}
impl<'m> ResourceConstraintRelationship<'m> {
pub fn name(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("Name")
}
pub fn description(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("Description")
}
pub fn relating_constraint(self) -> ConstraintResult<EntityId> {
self.record.required_ref("RelatingConstraint")
}
pub fn related_resources(self) -> ConstraintResult<Vec<EntityId>> {
self.record.required_refs("RelatedResourceObjects")
}
fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
let record = self.record;
record.validate_target(
view.model(),
"RelatingConstraint",
self.relating_constraint()?,
"IfcConstraint",
)?;
for target in self.related_resources()? {
record.validate_target(
view.model(),
"RelatedResourceObjects",
target,
"IfcResourceObjectSelect",
)?;
}
Ok(self)
}
}
impl<'m> ConstraintAssignment<'m> {
pub fn global_id(self) -> ConstraintResult<&'m str> {
self.record.required_text("GlobalId")
}
pub fn intent(self) -> ConstraintResult<Option<&'m str>> {
self.record.optional_text("Intent")
}
pub fn related_objects(self) -> ConstraintResult<Vec<EntityId>> {
self.record.required_refs("RelatedObjects")
}
pub fn relating_constraint(self) -> ConstraintResult<EntityId> {
self.record.required_ref("RelatingConstraint")
}
fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
let record = self.record;
if Guid::parse(self.global_id()?).is_none() {
return Err(ConstraintError::InvalidValue {
entity: ASSIGNMENT,
id: record.id,
attribute: "GlobalId",
value: self.global_id()?.into(),
});
}
record.validate_target(
view.model(),
"RelatingConstraint",
self.relating_constraint()?,
"IfcConstraint",
)?;
for target in self.related_objects()? {
record.validate_target(
view.model(),
"RelatedObjects",
target,
"IfcDefinitionSelect",
)?;
}
Ok(self)
}
}
impl<'m> ConstraintView<'m> {
fn entity(self, id: EntityId) -> ConstraintResult<&'m Entity> {
self.model()
.get(id)
.ok_or(ConstraintError::UnknownEntity { id })
}
pub fn metric(self, id: EntityId) -> ConstraintResult<Metric<'m>> {
Metric::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
}
pub fn objective(self, id: EntityId) -> ConstraintResult<Objective<'m>> {
Objective::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
}
pub fn resource_constraint_relationship(
self,
id: EntityId,
) -> ConstraintResult<ResourceConstraintRelationship<'m>> {
ResourceConstraintRelationship::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
}
pub fn constraint_assignment(self, id: EntityId) -> ConstraintResult<ConstraintAssignment<'m>> {
ConstraintAssignment::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
}
pub fn resources_constrained_by(self, constraint: EntityId) -> ConstraintResult<Vec<EntityId>> {
self.constraint(constraint)?;
let layout = self.layout()?;
let mut out = Vec::new();
if layout.require_entity(RESOURCE_REL).is_err() {
return Ok(out);
}
for (id, entity) in self.model().of_type(RESOURCE_REL) {
let relationship = ResourceConstraintRelationship::bound(id, entity, layout)?;
if relationship.relating_constraint()? == constraint {
out.extend(relationship.validate(self)?.related_resources()?);
}
}
Ok(out)
}
pub fn objects_constrained_by(self, constraint: EntityId) -> ConstraintResult<Vec<EntityId>> {
self.constraint(constraint)?;
let layout = self.layout()?;
let mut out = Vec::new();
for (id, entity) in self.model().of_type(ASSIGNMENT) {
let relationship = ConstraintAssignment::bound(id, entity, layout)?;
if relationship.relating_constraint()? == constraint {
out.extend(relationship.validate(self)?.related_objects()?);
}
}
Ok(out)
}
fn constraint(self, id: EntityId) -> ConstraintResult<()> {
let entity = self.entity(id)?;
if entity.is_type(METRIC) {
self.metric(id).map(|_| ())
} else if entity.is_type(OBJECTIVE) {
self.objective(id).map(|_| ())
} else {
Err(wrong("IfcConstraint", entity))
}
}
}