use ifc_model::guid::Guid;
use ifc_model::{EntityId, Model, Transaction, Value};
use crate::error::{ElementTypeError, ElementTypeResult};
use crate::release::{bind, require_owner_history, Layout};
use crate::table::{ElementType, Family};
pub(crate) fn invalid(
entity: &'static str,
attribute: &'static str,
value: impl Into<String>,
) -> ElementTypeError {
ElementTypeError::Invalid {
entity,
attribute,
value: value.into(),
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct TypeDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub applicable_occurrence: Option<&'a str>,
pub maps_or_identification: Option<Slot6<'a>>,
pub tag_or_long_description: Option<&'a str>,
pub fallback: Option<&'a str>,
pub operation_type: Option<&'a str>,
pub user_defined_operation_type: Option<&'a str>,
pub partitioning_type: Option<&'a str>,
pub user_defined_partitioning_type: Option<&'a str>,
pub parameter_takes_precedence: Option<bool>,
pub event_trigger_type: Option<&'a str>,
pub user_defined_event_trigger_type: Option<&'a str>,
pub assembly_place: Option<&'a str>,
}
impl<'a> TypeDraft<'a> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[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 applicable_occurrence(mut self, value: &'a str) -> Self {
self.applicable_occurrence = Some(value);
self
}
#[must_use]
pub fn maps_or_identification(mut self, value: Slot6<'a>) -> Self {
self.maps_or_identification = Some(value);
self
}
#[must_use]
pub fn tag_or_long_description(mut self, value: &'a str) -> Self {
self.tag_or_long_description = Some(value);
self
}
#[must_use]
pub fn fallback(mut self, value: &'a str) -> Self {
self.fallback = Some(value);
self
}
#[must_use]
pub fn operation_type(mut self, value: &'a str) -> Self {
self.operation_type = Some(value);
self
}
#[must_use]
pub fn user_defined_operation_type(mut self, value: &'a str) -> Self {
self.user_defined_operation_type = Some(value);
self
}
#[must_use]
pub fn partitioning_type(mut self, value: &'a str) -> Self {
self.partitioning_type = Some(value);
self
}
#[must_use]
pub fn user_defined_partitioning_type(mut self, value: &'a str) -> Self {
self.user_defined_partitioning_type = Some(value);
self
}
#[must_use]
pub fn parameter_takes_precedence(mut self, value: bool) -> Self {
self.parameter_takes_precedence = Some(value);
self
}
#[must_use]
pub fn event_trigger_type(mut self, value: &'a str) -> Self {
self.event_trigger_type = Some(value);
self
}
#[must_use]
pub fn user_defined_event_trigger_type(mut self, value: &'a str) -> Self {
self.user_defined_event_trigger_type = Some(value);
self
}
#[must_use]
pub fn assembly_place(mut self, value: &'a str) -> Self {
self.assembly_place = Some(value);
self
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum Slot6<'a> {
RepresentationMaps(&'a [EntityId]),
Identification(&'a str),
}
pub fn create_type(
tx: &mut Transaction,
kind: ElementType,
global_id: &str,
predefined_type: Option<&str>,
draft: TypeDraft<'_>,
) -> ElementTypeResult<EntityId> {
let request = Request {
kind,
global_id,
predefined_type,
draft,
};
author(tx, Layout::catalogue()?, request, None)
}
pub fn create_type_in(
tx: &mut Transaction,
model: &Model,
kind: ElementType,
global_id: &str,
predefined_type: Option<&str>,
draft: TypeDraft<'_>,
) -> ElementTypeResult<EntityId> {
let request = Request {
kind,
global_id,
predefined_type,
draft,
};
author(tx, bind(model)?, request, None)
}
pub fn create_type_with_owner_history(
tx: &mut Transaction,
model: &Model,
kind: ElementType,
global_id: &str,
predefined_type: Option<&str>,
draft: TypeDraft<'_>,
owner_history: EntityId,
) -> ElementTypeResult<EntityId> {
let layout = bind(model)?;
require_owner_history(tx, model, kind.type_name, owner_history)?;
let request = Request {
kind,
global_id,
predefined_type,
draft,
};
author(tx, layout, request, Some(owner_history))
}
struct Request<'a> {
kind: ElementType,
global_id: &'a str,
predefined_type: Option<&'a str>,
draft: TypeDraft<'a>,
}
fn author(
tx: &mut Transaction,
layout: Layout,
request: Request<'_>,
owner_history: Option<EntityId>,
) -> ElementTypeResult<EntityId> {
let Request {
kind,
global_id,
predefined_type,
draft,
} = request;
let entity = kind.type_name;
if Guid::parse(global_id).is_none() {
return Err(invalid(entity, "GlobalId", global_id));
}
if blank(draft.name) {
return Err(invalid(entity, "Name", "NameRequired"));
}
layout.require_entity(entity)?;
match layout.attribute(entity, "PredefinedType") {
None if predefined_type.is_some() => {
return Err(ElementTypeError::AuthoringNotInSchema {
entity,
attribute: "PredefinedType",
schema: layout.version(),
});
}
None => {}
Some(declared) => {
let members = layout.members(entity, "PredefinedType").unwrap_or_default();
match predefined_type {
None if !declared.optional => {
return Err(invalid(entity, "PredefinedType", "required"));
}
Some(token) if !members.contains(&token) => {
return Err(invalid(entity, "PredefinedType", token));
}
_ => {}
}
}
}
if predefined_type == Some("USERDEFINED") && blank(draft.fallback) {
return Err(invalid(
entity,
kind.fallback_attr,
"required by USERDEFINED",
));
}
let (slot6_name, slot6) = match (draft.maps_or_identification, kind.family) {
(Some(Slot6::RepresentationMaps(maps)), Family::Element) => {
if maps.is_empty() {
return Err(invalid(entity, "RepresentationMaps", "empty"));
}
(
"RepresentationMaps",
Value::List(maps.iter().copied().map(Value::Ref).collect()),
)
}
(Some(Slot6::Identification(id)), Family::ResourceOrProcess) => {
("Identification", Value::Text(id.into()))
}
(Some(Slot6::RepresentationMaps(_)), Family::ResourceOrProcess) => {
return Err(invalid(entity, "Identification", "expected text, got maps"));
}
(Some(Slot6::Identification(_)), Family::Element) => {
return Err(invalid(
entity,
"RepresentationMaps",
"expected maps, got text",
));
}
(None, _) => ("RepresentationMaps", Value::Null),
};
let slot7_name = match kind.family {
Family::Element => "Tag",
Family::ResourceOrProcess => "LongDescription",
};
let specific = specific_values(layout, entity, &draft)?;
let mut values = vec![
("GlobalId", Value::Text(global_id.into())),
(
"OwnerHistory",
owner_history.map_or(Value::Null, Value::Ref),
),
("Name", text(draft.name)),
("Description", text(draft.description)),
("ApplicableOccurrence", text(draft.applicable_occurrence)),
(slot6_name, slot6),
(slot7_name, text(draft.tag_or_long_description)),
(kind.fallback_attr, text(draft.fallback)),
(
"PredefinedType",
predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
),
];
values.extend(specific);
let record = layout.named_record(entity, values)?;
Ok(tx.create(record))
}
fn specific_values(
layout: Layout,
entity: &'static str,
draft: &TypeDraft<'_>,
) -> ElementTypeResult<Vec<(&'static str, Value)>> {
let enums = [
("OperationType", draft.operation_type),
("PartitioningType", draft.partitioning_type),
("EventTriggerType", draft.event_trigger_type),
("AssemblyPlace", draft.assembly_place),
];
let mut values = Vec::new();
for (attribute, token) in enums {
let Some(token) = token else {
continue;
};
let Some(members) = layout.members(entity, attribute) else {
return Err(ElementTypeError::AuthoringNotInSchema {
entity,
attribute,
schema: layout.version(),
});
};
if !members.contains(&token) {
return Err(invalid(entity, attribute, token));
}
values.push((attribute, Value::Enum(token.into())));
}
if draft.event_trigger_type == Some("USERDEFINED")
&& blank(draft.user_defined_event_trigger_type)
{
return Err(invalid(
entity,
"UserDefinedEventTriggerType",
"required by USERDEFINED",
));
}
values.extend([
(
"UserDefinedOperationType",
text(draft.user_defined_operation_type),
),
(
"UserDefinedPartitioningType",
text(draft.user_defined_partitioning_type),
),
(
"UserDefinedEventTriggerType",
text(draft.user_defined_event_trigger_type),
),
(
"ParameterTakesPrecedence",
draft
.parameter_takes_precedence
.map_or(Value::Null, Value::Bool),
),
]);
Ok(values)
}
fn text(value: Option<&str>) -> Value {
value.map_or(Value::Null, |v| Value::Text(v.into()))
}
fn blank(value: Option<&str>) -> bool {
value.is_none_or(|v| v.trim().is_empty())
}