use ifc_model::guid::Guid;
use ifc_model::{EntityId, Model, Transaction, Value};
use ifc_schema::SchemaVersion;
use crate::release::{bind, require_owner_history};
use crate::table::Occurrence;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum OccurrenceError {
MalformedGuid {
value: String,
},
UnknownPredefinedType {
entity: &'static str,
token: String,
},
NoPredefinedType {
entity: &'static str,
},
UserDefinedWithoutObjectType {
entity: &'static str,
},
WrongTypeClass {
entity: &'static str,
expected: &'static str,
found: String,
},
TypingNotPermitted {
entity: &'static str,
},
UnresolvedType {
id: EntityId,
},
MultipleSchemas {
schemas: usize,
},
UnsupportedSchema {
schema: String,
},
EntityNotInSchema {
entity: &'static str,
schema: SchemaVersion,
},
AuthoringNotInSchema {
entity: &'static str,
attribute: &'static str,
schema: SchemaVersion,
},
AuthoringRequired {
entity: &'static str,
attribute: &'static str,
schema: SchemaVersion,
},
UnresolvedOwnerHistory {
id: EntityId,
},
NotAnOwnerHistory {
id: EntityId,
found: String,
},
}
pub type OccurrenceResult<T> = Result<T, OccurrenceError>;
#[derive(Debug, Clone, Copy, Default)]
pub struct OccurrenceDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub object_type: Option<&'a str>,
pub placement: Option<EntityId>,
pub representation: Option<EntityId>,
pub tag: Option<&'a str>,
}
fn text(v: Option<&str>) -> Value {
v.map_or(Value::Null, |s| Value::Text(s.into()))
}
fn slot_ref(v: Option<EntityId>) -> Value {
v.map_or(Value::Null, Value::Ref)
}
pub fn create(
tx: &mut Transaction,
model: &Model,
kind: Occurrence,
global_id: &str,
predefined_type: Option<&str>,
typed_by: Option<EntityId>,
draft: OccurrenceDraft<'_>,
) -> OccurrenceResult<EntityId> {
let request = Request {
kind,
global_id,
predefined_type,
typed_by,
draft,
};
author(tx, model, request, None)
}
#[allow(clippy::too_many_arguments)]
pub fn create_with_owner_history(
tx: &mut Transaction,
model: &Model,
kind: Occurrence,
global_id: &str,
predefined_type: Option<&str>,
typed_by: Option<EntityId>,
draft: OccurrenceDraft<'_>,
owner_history: EntityId,
) -> OccurrenceResult<EntityId> {
let request = Request {
kind,
global_id,
predefined_type,
typed_by,
draft,
};
author(tx, model, request, Some(owner_history))
}
struct Request<'a> {
kind: Occurrence,
global_id: &'a str,
predefined_type: Option<&'a str>,
typed_by: Option<EntityId>,
draft: OccurrenceDraft<'a>,
}
fn author(
tx: &mut Transaction,
model: &Model,
request: Request<'_>,
owner_history: Option<EntityId>,
) -> OccurrenceResult<EntityId> {
let Request {
kind,
global_id,
predefined_type,
typed_by,
draft,
} = request;
let entity = kind.type_name;
if Guid::parse(global_id).is_none() {
return Err(OccurrenceError::MalformedGuid {
value: global_id.into(),
});
}
let layout = bind(model)?;
layout.require_entity(entity)?;
if let Some(token) = predefined_type {
let Some(members) = layout.predefined_members(entity) else {
return Err(OccurrenceError::NoPredefinedType { entity });
};
if !members.contains(&token) {
return Err(OccurrenceError::UnknownPredefinedType {
entity,
token: token.into(),
});
}
if token == "USERDEFINED" && draft.object_type.is_none_or(|s| s.trim().is_empty()) {
return Err(OccurrenceError::UserDefinedWithoutObjectType { entity });
}
}
if let Some(id) = typed_by {
let Some(expected) = kind.type_class else {
return Err(OccurrenceError::TypingNotPermitted { entity });
};
let found = model
.get(id)
.ok_or(OccurrenceError::UnresolvedType { id })?
.type_name
.to_ascii_uppercase();
if found != expected {
return Err(OccurrenceError::WrongTypeClass {
entity,
expected,
found,
});
}
}
if let Some(owner_history) = owner_history {
require_owner_history(tx, model, owner_history)?;
}
let record = layout.named_record(
entity,
vec![
("GlobalId", Value::Text(global_id.into())),
("OwnerHistory", slot_ref(owner_history)),
("Name", text(draft.name)),
("Description", text(draft.description)),
("ObjectType", text(draft.object_type)),
("ObjectPlacement", slot_ref(draft.placement)),
("Representation", slot_ref(draft.representation)),
("Tag", text(draft.tag)),
(
"PredefinedType",
predefined_type.map_or(Value::Null, |token| Value::Enum(token.into())),
),
],
)?;
Ok(tx.create(record))
}