use std::collections::HashSet;
use std::sync::Arc;
use ifc_model::guid::Guid;
use ifc_model::{Edit, EntityId, Model, Transaction, Value};
use ifc_schema::Schema;
use crate::release::{bind, require_owner_history};
use crate::{ApprovalError, ApprovalResult};
const APPROVAL: &str = "IFCAPPROVAL";
const APPROVAL_REL: &str = "IFCAPPROVALRELATIONSHIP";
const RESOURCE_REL: &str = "IFCRESOURCEAPPROVALRELATIONSHIP";
const ASSIGNMENT: &str = "IFCRELASSOCIATESAPPROVAL";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DateTimeInput<'a> {
Text(&'a str),
Record(EntityId),
}
impl<'a> From<&'a str> for DateTimeInput<'a> {
fn from(text: &'a str) -> Self {
Self::Text(text)
}
}
impl From<EntityId> for DateTimeInput<'_> {
fn from(record: EntityId) -> Self {
Self::Record(record)
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct ApprovalDraft<'a> {
pub identifier: Option<&'a str>,
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub time_of_approval: Option<DateTimeInput<'a>>,
pub status: Option<&'a str>,
pub level: Option<&'a str>,
pub qualifier: Option<&'a str>,
pub requesting_approval: Option<EntityId>,
pub giving_approval: Option<EntityId>,
}
impl<'a> ApprovalDraft<'a> {
#[must_use]
pub fn new() -> Self {
Self {
identifier: None,
name: None,
description: None,
time_of_approval: None,
status: None,
level: None,
qualifier: None,
requesting_approval: None,
giving_approval: None,
}
}
#[must_use]
pub fn identifier(mut self, value: &'a str) -> Self {
self.identifier = Some(value);
self
}
#[must_use]
pub fn name(mut self, value: &'a str) -> Self {
self.name = Some(value);
self
}
#[must_use]
pub fn description(mut self, value: &'a str) -> Self {
self.description = Some(value);
self
}
#[must_use]
pub fn time_of_approval(mut self, value: impl Into<DateTimeInput<'a>>) -> Self {
self.time_of_approval = Some(value.into());
self
}
#[must_use]
pub fn status(mut self, value: &'a str) -> Self {
self.status = Some(value);
self
}
#[must_use]
pub fn level(mut self, value: &'a str) -> Self {
self.level = Some(value);
self
}
#[must_use]
pub fn qualifier(mut self, value: &'a str) -> Self {
self.qualifier = Some(value);
self
}
#[must_use]
pub fn requesting_approval(mut self, value: EntityId) -> Self {
self.requesting_approval = Some(value);
self
}
#[must_use]
pub fn giving_approval(mut self, value: EntityId) -> Self {
self.giving_approval = Some(value);
self
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ApprovalRelationshipDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub relating_approval: EntityId,
pub related_approvals: &'a [EntityId],
}
impl<'a> ApprovalRelationshipDraft<'a> {
#[must_use]
pub fn new(relating_approval: EntityId, related_approvals: &'a [EntityId]) -> Self {
Self {
name: None,
description: None,
relating_approval,
related_approvals,
}
}
#[must_use]
pub fn name(mut self, value: &'a str) -> Self {
self.name = Some(value);
self
}
#[must_use]
pub fn description(mut self, value: &'a str) -> Self {
self.description = Some(value);
self
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ResourceApprovalDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub related_resources: &'a [EntityId],
pub relating_approval: EntityId,
}
impl<'a> ResourceApprovalDraft<'a> {
#[must_use]
pub fn new(related_resources: &'a [EntityId], relating_approval: EntityId) -> Self {
Self {
name: None,
description: None,
related_resources,
relating_approval,
}
}
#[must_use]
pub fn name(mut self, value: &'a str) -> Self {
self.name = Some(value);
self
}
#[must_use]
pub fn description(mut self, value: &'a str) -> Self {
self.description = Some(value);
self
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ApprovalAssociationDraft<'a> {
pub global_id: &'a str,
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub related_objects: &'a [EntityId],
pub relating_approval: EntityId,
}
impl<'a> ApprovalAssociationDraft<'a> {
#[must_use]
pub fn new(
global_id: &'a str,
related_objects: &'a [EntityId],
relating_approval: EntityId,
) -> Self {
Self {
global_id,
name: None,
description: None,
related_objects,
relating_approval,
}
}
#[must_use]
pub fn name(mut self, value: &'a str) -> Self {
self.name = Some(value);
self
}
#[must_use]
pub fn description(mut self, value: &'a str) -> Self {
self.description = Some(value);
self
}
}
pub fn create_approval(
tx: &mut Transaction,
model: &Model,
draft: ApprovalDraft<'_>,
) -> ApprovalResult<EntityId> {
let layout = bind(model)?;
if draft.identifier.is_none() && draft.name.is_none() {
return Err(ApprovalError::AuthoringInvalid {
entity: APPROVAL,
attribute: "WR",
value: "Identifier and Name are both absent".into(),
});
}
for target in [draft.requesting_approval, draft.giving_approval]
.into_iter()
.flatten()
{
validate_target_in(layout.schema(), tx, model, target, "IfcActorSelect")?;
}
let time = match draft.time_of_approval {
None => Value::Null,
Some(DateTimeInput::Text(text)) => Value::Text(Arc::from(text)),
Some(DateTimeInput::Record(target)) => {
if let Some((_, declared)) = layout.declared(APPROVAL, "TimeOfApproval") {
if layout.admits_entity(&declared.type_name, 8) {
validate_target_in(
layout.schema(),
tx,
model,
target,
declared.type_name.as_str(),
)?;
}
}
Value::Ref(target)
}
};
let record = layout.named_record(
APPROVAL,
vec![
("Identifier", optional_text(draft.identifier)),
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("TimeOfApproval", time),
("Status", optional_text(draft.status)),
("Level", optional_text(draft.level)),
("Qualifier", optional_text(draft.qualifier)),
(
"RequestingApproval",
optional_ref(draft.requesting_approval),
),
("GivingApproval", optional_ref(draft.giving_approval)),
],
)?;
Ok(tx.create(record))
}
pub fn relate_approvals(
tx: &mut Transaction,
model: &Model,
draft: ApprovalRelationshipDraft<'_>,
) -> ApprovalResult<EntityId> {
let layout = bind(model)?;
layout.require_entity(APPROVAL_REL)?;
validate_target_in(
layout.schema(),
tx,
model,
draft.relating_approval,
APPROVAL,
)?;
validate_set(
layout.schema(),
tx,
model,
APPROVAL_REL,
"RelatedApprovals",
draft.related_approvals,
APPROVAL,
Some(draft.relating_approval),
)?;
let single = layout
.declared(APPROVAL_REL, "RelatedApprovals")
.is_some_and(|(_, declared)| !declared.aggregate);
let related = match (single, draft.related_approvals) {
(false, related) => refs(related),
(true, [one]) => Value::Ref(*one),
(true, _) => {
return Err(ApprovalError::AuthoringValueType {
entity: APPROVAL_REL,
attribute: "RelatedApprovals",
declared: "IfcApproval",
schema: layout.version(),
})
}
};
let record = layout.named_record(
APPROVAL_REL,
vec![
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("RelatingApproval", Value::Ref(draft.relating_approval)),
("RelatedApprovals", related),
],
)?;
Ok(tx.create(record))
}
pub fn relate_resource_approval(
tx: &mut Transaction,
model: &Model,
draft: ResourceApprovalDraft<'_>,
) -> ApprovalResult<EntityId> {
let layout = bind(model)?;
layout.require_entity(RESOURCE_REL)?;
validate_target_in(
layout.schema(),
tx,
model,
draft.relating_approval,
APPROVAL,
)?;
validate_set(
layout.schema(),
tx,
model,
RESOURCE_REL,
"RelatedResourceObjects",
draft.related_resources,
"IfcResourceObjectSelect",
None,
)?;
let record = layout.named_record(
RESOURCE_REL,
vec![
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("RelatedResourceObjects", refs(draft.related_resources)),
("RelatingApproval", Value::Ref(draft.relating_approval)),
],
)?;
Ok(tx.create(record))
}
pub fn associate_approval(
tx: &mut Transaction,
model: &Model,
draft: ApprovalAssociationDraft<'_>,
) -> ApprovalResult<EntityId> {
associate(tx, model, draft, None)
}
pub fn associate_approval_with_owner_history(
tx: &mut Transaction,
model: &Model,
draft: ApprovalAssociationDraft<'_>,
owner_history: EntityId,
) -> ApprovalResult<EntityId> {
associate(tx, model, draft, Some(owner_history))
}
fn associate(
tx: &mut Transaction,
model: &Model,
draft: ApprovalAssociationDraft<'_>,
owner_history: Option<EntityId>,
) -> ApprovalResult<EntityId> {
let layout = bind(model)?;
if Guid::parse(draft.global_id).is_none() {
return Err(ApprovalError::AuthoringInvalid {
entity: ASSIGNMENT,
attribute: "GlobalId",
value: draft.global_id.into(),
});
}
validate_target_in(
layout.schema(),
tx,
model,
draft.relating_approval,
APPROVAL,
)?;
if draft.related_objects.is_empty() {
return Err(ApprovalError::AuthoringInvalid {
entity: ASSIGNMENT,
attribute: "RelatedObjects",
value: "empty SET [1:?]".into(),
});
}
let mut seen = HashSet::new();
for &target in draft.related_objects {
if !seen.insert(target) {
return Err(ApprovalError::AuthoringInvalid {
entity: ASSIGNMENT,
attribute: "RelatedObjects",
value: format!("duplicate {target}"),
});
}
let actual =
final_type(tx, model, target).ok_or(ApprovalError::UnknownEntity { id: target })?;
if !layout.is_definition(actual) {
return Err(ApprovalError::AuthoringReferenceType {
target,
expected: "IfcDefinitionSelect",
actual: actual.into(),
});
}
}
if let Some(owner_history) = owner_history {
require_owner_history(tx, model, owner_history)?;
}
let record = layout.named_record(
ASSIGNMENT,
vec![
("GlobalId", text(draft.global_id)),
("OwnerHistory", optional_ref(owner_history)),
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("RelatedObjects", refs(draft.related_objects)),
("RelatingApproval", Value::Ref(draft.relating_approval)),
],
)?;
Ok(tx.create(record))
}
#[allow(clippy::too_many_arguments)]
fn validate_set(
schema: &Schema,
tx: &Transaction,
model: &Model,
kind: &'static str,
attribute: &'static str,
targets: &[EntityId],
expected: &'static str,
disallow: Option<EntityId>,
) -> ApprovalResult<()> {
if targets.is_empty() {
return Err(ApprovalError::AuthoringInvalid {
entity: kind,
attribute,
value: "empty SET [1:?]".into(),
});
}
let mut seen = HashSet::new();
for &target in targets {
if !seen.insert(target) {
return Err(ApprovalError::AuthoringInvalid {
entity: kind,
attribute,
value: format!("duplicate {target}"),
});
}
if Some(target) == disallow {
return Err(ApprovalError::AuthoringInvalid {
entity: kind,
attribute,
value: format!("self reference {target}"),
});
}
validate_target_in(schema, tx, model, target, expected)?;
}
Ok(())
}
fn validate_target_in(
schema: &Schema,
tx: &Transaction,
model: &Model,
target: EntityId,
expected: &'static str,
) -> ApprovalResult<()> {
let actual =
final_type(tx, model, target).ok_or(ApprovalError::UnknownEntity { id: target })?;
if schema.accepts_type(expected, actual) {
Ok(())
} else {
Err(ApprovalError::AuthoringReferenceType {
target,
expected,
actual: actual.into(),
})
}
}
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())
}
fn text(value: &str) -> Value {
Value::Text(Arc::from(value))
}
fn optional_text(value: Option<&str>) -> Value {
value.map_or(Value::Null, text)
}
fn optional_ref(value: Option<EntityId>) -> Value {
value.map_or(Value::Null, Value::Ref)
}
fn refs(values: &[EntityId]) -> Value {
Value::List(values.iter().copied().map(Value::Ref).collect())
}