use crate::error::{GeometryError, GeometryResult};
use ifc_model::{Entity, EntityId, Model, Value};
#[derive(Debug, Clone, Copy)]
pub struct Slots<'m> {
id: EntityId,
entity: &'m Entity,
}
impl<'m> Slots<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self { id, entity }
}
pub fn id(&self) -> EntityId {
self.id
}
pub fn entity(&self) -> &'m Entity {
self.entity
}
pub fn type_name(&self) -> &'m str {
&self.entity.type_name
}
pub fn opt(&self, index: usize) -> Option<&'m Value> {
match self.entity.attribute(index) {
None | Some(Value::Null) => None,
other => other,
}
}
pub fn req(&self, index: usize, name: &'static str) -> GeometryResult<&'m Value> {
self.opt(index)
.ok_or_else(|| GeometryError::MissingAttribute {
entity: self.id,
type_name: self.type_name().to_string(),
attribute: name,
})
}
pub fn req_f64(&self, index: usize, name: &'static str) -> GeometryResult<f64> {
let value = self.req(index, name)?;
value
.unwrap_typed()
.as_f64()
.ok_or_else(|| self.kind_error(name, "a number", value))
}
pub fn opt_f64(&self, index: usize) -> Option<f64> {
self.opt(index)?.unwrap_typed().as_f64()
}
pub fn req_i64(&self, index: usize, name: &'static str) -> GeometryResult<i64> {
let value = self.req(index, name)?;
match value.unwrap_typed() {
Value::Integer(i) => Ok(*i),
other => Err(self.kind_error(name, "an integer", other)),
}
}
pub fn req_ref(&self, index: usize, name: &'static str) -> GeometryResult<EntityId> {
let value = self.req(index, name)?;
value
.as_ref_id()
.ok_or_else(|| self.kind_error(name, "an entity reference", value))
}
pub fn opt_ref(&self, index: usize) -> Option<EntityId> {
self.opt(index)?.as_ref_id()
}
pub fn req_f64_list(&self, index: usize, name: &'static str) -> GeometryResult<Vec<f64>> {
let value = self.req(index, name)?;
let items = value
.as_list()
.ok_or_else(|| self.kind_error(name, "a list", value))?;
items
.iter()
.map(|v| {
v.unwrap_typed()
.as_f64()
.ok_or_else(|| self.kind_error(name, "a list of numbers", v))
})
.collect()
}
pub fn req_ref_list(&self, index: usize, name: &'static str) -> GeometryResult<Vec<EntityId>> {
let value = self.req(index, name)?;
let items = value
.as_list()
.ok_or_else(|| self.kind_error(name, "a list", value))?;
items
.iter()
.map(|v| {
v.as_ref_id()
.ok_or_else(|| self.kind_error(name, "a list of references", v))
})
.collect()
}
pub fn opt_ref_list(&self, index: usize) -> Vec<EntityId> {
self.opt(index)
.and_then(|v| v.as_list())
.map(|items| items.iter().filter_map(|v| v.as_ref_id()).collect())
.unwrap_or_default()
}
pub fn opt_text(&self, index: usize) -> Option<String> {
match self.opt(index)?.unwrap_typed() {
Value::Text(text) => Some(text.to_string()),
_ => None,
}
}
pub fn opt_enum(&self, index: usize) -> Option<&'m str> {
match self.opt(index)? {
Value::Enum(e) => Some(e),
_ => None,
}
}
pub fn opt_bool(&self, index: usize) -> Option<bool> {
match self.opt(index)? {
Value::Bool(b) => Some(*b),
_ => None,
}
}
pub fn req_bool(&self, index: usize, name: &'static str) -> GeometryResult<bool> {
let value = self.req(index, name)?;
match value {
Value::Bool(b) => Ok(*b),
other => Err(self.kind_error(name, "a boolean", other)),
}
}
pub fn resolve(&self, model: &'m Model, id: EntityId) -> GeometryResult<&'m Entity> {
model.get(id).ok_or(GeometryError::MissingEntity {
referrer: self.id,
missing: id,
})
}
fn kind_error(
&self,
attribute: &'static str,
expected: &'static str,
found: &Value,
) -> GeometryError {
GeometryError::WrongValueKind {
entity: self.id,
type_name: self.type_name().to_string(),
attribute,
expected,
found: describe(found),
}
}
pub fn unsupported(&self, detail: &'static str) -> GeometryError {
GeometryError::Unsupported {
entity: self.id,
type_name: self.type_name().to_string(),
detail,
}
}
pub fn degenerate(&self, detail: impl Into<String>) -> GeometryError {
GeometryError::Degenerate {
entity: self.id,
type_name: self.type_name().to_string(),
detail: detail.into(),
}
}
}
fn describe(value: &Value) -> String {
match value {
Value::Null => "$".into(),
Value::Derived => "*".into(),
Value::Bool(b) => format!(".{}.", if *b { "T" } else { "F" }),
Value::LogicalUnknown => ".U.".into(),
Value::Integer(i) => format!("integer {i}"),
Value::Real(r) => format!("real {r}"),
Value::Text(t) => format!("text {t:?}"),
Value::Binary(_) => "binary".into(),
Value::Enum(e) => format!(".{e}."),
Value::Ref(id) => format!("reference {id}"),
Value::List(items) => format!("list of {}", items.len()),
Value::Typed { type_name, .. } => format!("{type_name}(...)"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn point() -> Entity {
Entity::new(
"IFCCARTESIANPOINT",
vec![Value::List(vec![
Value::Real(1.0),
Value::Real(2.0),
Value::Real(3.0),
])],
)
}
#[test]
fn reads_a_coordinate_list() {
let e = point();
let s = Slots::new(EntityId(1), &e);
assert_eq!(
s.req_f64_list(0, "Coordinates").unwrap(),
vec![1.0, 2.0, 3.0]
);
}
#[test]
fn missing_required_attribute_names_the_entity_and_slot() {
let e = point();
let s = Slots::new(EntityId(7), &e);
let err = s.req(3, "Missing").unwrap_err();
assert!(err.to_string().contains("#7"));
assert!(err.to_string().contains("Missing"));
}
#[test]
fn explicit_null_reads_as_absent() {
let e = Entity::new("IFCTEST", vec![Value::Null, Value::Real(5.0)]);
let s = Slots::new(EntityId(1), &e);
assert!(s.opt(0).is_none(), "$ is absent");
assert!(s.opt(99).is_none(), "past the end is absent");
assert_eq!(s.opt_f64(1), Some(5.0));
}
#[test]
fn unwraps_typed_measures() {
let e = Entity::new(
"IFCCIRCLE",
vec![Value::Typed {
type_name: "IFCPOSITIVELENGTHMEASURE".into(),
value: Box::new(Value::Real(2.5)),
}],
);
let s = Slots::new(EntityId(1), &e);
assert_eq!(s.req_f64(0, "Radius").unwrap(), 2.5);
}
#[test]
fn integer_literal_is_accepted_where_a_real_is_declared() {
let e = Entity::new("IFCTEST", vec![Value::Integer(0)]);
let s = Slots::new(EntityId(1), &e);
assert_eq!(s.req_f64(0, "Depth").unwrap(), 0.0);
}
#[test]
fn logical_unknown_is_not_false() {
let e = Entity::new("IFCTEST", vec![Value::LogicalUnknown]);
let s = Slots::new(EntityId(1), &e);
assert_eq!(s.opt_bool(0), None);
}
#[test]
fn wrong_kind_reports_what_was_actually_found() {
let e = Entity::new("IFCTEST", vec![Value::Text("nope".into())]);
let s = Slots::new(EntityId(1), &e);
let err = s.req_f64(0, "Radius").unwrap_err();
assert!(err.to_string().contains("text"), "got: {err}");
}
}
pub mod profile_slot {
pub const PROFILE_TYPE: usize = 0;
pub const PROFILE_NAME: usize = 1;
pub const POSITION: usize = 2;
pub const X_DIM: usize = 3;
pub const Y_DIM: usize = 4;
pub const RADIUS: usize = 3;
pub const OUTER_CURVE: usize = 2;
pub const INNER_CURVES: usize = 3;
pub const CIRCLE_WALL_THICKNESS: usize = 4;
pub const RECT_WALL_THICKNESS: usize = 5;
pub const RECT_INNER_RADIUS: usize = 6;
pub const RECT_OUTER_RADIUS: usize = 7;
pub const ROUNDED_RECT_RADIUS: usize = 5;
}
pub mod section_slot {
pub const I_WIDTH: usize = 3;
pub const I_DEPTH: usize = 4;
pub const I_WEB: usize = 5;
pub const I_FLANGE: usize = 6;
pub const I_FILLET: usize = 7;
pub const I_EDGE: usize = 8;
pub const I_SLOPE: usize = 9;
pub const AI_BOTTOM_WIDTH: usize = 3;
pub const AI_DEPTH: usize = 4;
pub const AI_WEB: usize = 5;
pub const AI_BOTTOM_FLANGE: usize = 6;
pub const AI_BOTTOM_FILLET: usize = 7;
pub const AI_TOP_WIDTH: usize = 8;
pub const AI_TOP_FLANGE: usize = 9;
pub const AI_TOP_FILLET: usize = 10;
pub const AI_BOTTOM_EDGE: usize = 11;
pub const AI_BOTTOM_SLOPE: usize = 12;
pub const AI_TOP_EDGE: usize = 13;
pub const AI_TOP_SLOPE: usize = 14;
pub const L_DEPTH: usize = 3;
pub const L_WIDTH: usize = 4;
pub const L_THICKNESS: usize = 5;
pub const L_FILLET: usize = 6;
pub const L_EDGE: usize = 7;
pub const L_SLOPE: usize = 8;
pub const T_DEPTH: usize = 3;
pub const T_WIDTH: usize = 4;
pub const T_WEB: usize = 5;
pub const T_FLANGE: usize = 6;
pub const T_FILLET: usize = 7;
pub const T_FLANGE_EDGE: usize = 8;
pub const T_WEB_EDGE: usize = 9;
pub const T_WEB_SLOPE: usize = 10;
pub const T_FLANGE_SLOPE: usize = 11;
pub const U_DEPTH: usize = 3;
pub const U_WIDTH: usize = 4;
pub const U_WEB: usize = 5;
pub const U_FLANGE: usize = 6;
pub const U_FILLET: usize = 7;
pub const U_EDGE: usize = 8;
pub const U_SLOPE: usize = 9;
pub const C_DEPTH: usize = 3;
pub const C_WIDTH: usize = 4;
pub const C_WALL: usize = 5;
pub const C_GIRTH: usize = 6;
pub const C_FILLET: usize = 7;
pub const Z_DEPTH: usize = 3;
pub const Z_FLANGE_WIDTH: usize = 4;
pub const Z_WEB: usize = 5;
pub const Z_FLANGE: usize = 6;
pub const Z_FILLET: usize = 7;
pub const Z_EDGE: usize = 8;
pub const E_SEMI1: usize = 3;
pub const E_SEMI2: usize = 4;
pub const TZ_BOTTOM: usize = 3;
pub const TZ_TOP: usize = 4;
pub const TZ_Y: usize = 5;
pub const CL_CURVE: usize = 2;
pub const CL_THICKNESS: usize = 3;
pub const TZ_OFFSET: usize = 6;
pub const COMPOSITE_PROFILES: usize = 2;
pub const DERIVED_PARENT: usize = 2;
pub const DERIVED_OPERATOR: usize = 3;
pub const COMPOSITE_LABEL: usize = 3;
pub const DERIVED_LABEL: usize = 4;
}