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)]
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>,
}
#[derive(Debug, Clone, Copy)]
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 record = layout.named_record(
entity,
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())),
),
],
)?;
Ok(tx.create(record))
}
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())
}