#![cfg(feature = "schema")]
use ifc_model::{Codec, Entity, EntityId, Model, Value};
use ifc_xml::{SchemaReading, XmlCodec, XmlError};
use std::sync::Arc;
const GUID: &str = "0YvctVUKr0kugbFTf53O9L";
fn strict() -> XmlCodec {
XmlCodec::with_schema(Arc::new(ifc_schema::ifc4().clone()))
}
fn lenient() -> XmlCodec {
strict().with_reading(SchemaReading::Lenient)
}
fn read(codec: &XmlCodec, body: &str) -> Result<Model, XmlError> {
let xml =
format!("<ifcXML xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">{body}</ifcXML>");
ifc_xml::reader::read(codec, xml.as_bytes())
}
fn slot(model: &Model, id: u64, name: &str) -> Value {
let entity = model.get(EntityId(id)).expect("entity");
let names = ifc_schema::ifc4().attribute_names(&entity.type_name);
let index = names
.iter()
.position(|candidate| *candidate == name)
.expect("slot");
entity.attributes[index].clone()
}
#[test]
fn strict_is_the_default_with_a_schema() {
assert_eq!(strict().reading(), SchemaReading::Strict);
assert_eq!(XmlCodec::default().reading(), SchemaReading::Strict);
}
#[test]
fn a_numeric_looking_label_reads_as_a_label() {
let body = format!(r#"<IFCWALL id="i1" GlobalId="{GUID}" Name="1" Tag="i7"/>"#);
let model = read(&strict(), &body).unwrap();
assert_eq!(slot(&model, 1, "Name"), Value::Text("1".into()));
assert_eq!(slot(&model, 1, "Tag"), Value::Text("i7".into()));
let model = read(&lenient(), &body).unwrap();
assert_eq!(slot(&model, 1, "Name"), Value::Integer(1));
assert_eq!(slot(&model, 1, "Tag"), Value::Ref(EntityId(7)));
}
#[test]
fn enumeration_and_number_text_is_typed_from_the_declaration() {
let model = read(
&strict(),
&format!(
r#"<IFCWALLTYPE id="i1" GlobalId="{GUID}" PredefinedType="NOTDEFINED"/>
<IFCCARTESIANPOINT id="i2"><Coordinates kind="list"><item kind="real">1.5</item></Coordinates></IFCCARTESIANPOINT>
<IFCMATERIALLAYER id="i3" LayerThickness="5"/>"#
),
)
.unwrap();
assert_eq!(
slot(&model, 1, "PredefinedType"),
Value::Enum("NOTDEFINED".into())
);
assert_eq!(slot(&model, 3, "LayerThickness"), Value::Real(5.0));
let error = read(
&strict(),
&format!(r#"<IFCWALLTYPE id="i1" GlobalId="{GUID}" PredefinedType="DOOR"/>"#),
)
.unwrap_err();
assert!(
matches!(error.root_cause(), XmlError::InvalidScalar { .. }),
"{error:?}"
);
}
#[test]
fn an_undeclared_name_is_refused_with_entity_element_and_attribute() {
for body in [
format!(r#"<IFCWALL id="i1" GlobalId="{GUID}" Colour="red"/>"#),
format!(
r#"<IFCWALL id="i1" GlobalId="{GUID}"><Colour kind="string">red</Colour></IFCWALL>"#
),
] {
let error = read(&strict(), &body).unwrap_err();
assert!(
matches!(
error.root_cause(),
XmlError::UnknownAttribute { entity, element, attribute }
if entity == "IfcWall" && element == "IFCWALL" && attribute == "Colour"
),
"{error:?}"
);
assert_eq!(
error.path().map(ToString::to_string).as_deref(),
Some("/ifcXML/IFCWALL[@id='i1']")
);
let model = read(&lenient(), &body).unwrap();
assert_eq!(
model.get(EntityId(1)).unwrap().attributes.last(),
Some(&Value::Text("red".into()))
);
}
let error = read(&strict(), r#"<IFCWALL id="i1" a0="x"/>"#).unwrap_err();
assert!(
matches!(error.root_cause(), XmlError::UnknownAttribute { attribute, .. } if attribute == "a0")
);
}
#[test]
fn an_undeclared_or_abstract_entity_is_refused() {
let error = read(&strict(), r#"<IFCFUTUREWALL id="i1"/>"#).unwrap_err();
assert!(
matches!(error.root_cause(), XmlError::UnknownEntity { name } if name == "IFCFUTUREWALL")
);
let error = read(&strict(), r#"<IFCROOT id="i1"/>"#).unwrap_err();
assert!(matches!(
error.root_cause(),
XmlError::AbstractEntity { .. }
));
assert_eq!(
read(&lenient(), r#"<IFCFUTUREWALL id="i1" a0="x"/>"#)
.unwrap()
.len(),
1
);
}
#[test]
fn an_explicit_kind_the_declaration_does_not_admit_is_refused() {
for (body, why) in [
(
format!(r#"<IFCWALL id="i1" GlobalId="{GUID}"><Name kind="integer">1</Name></IFCWALL>"#),
"an integer in a label",
),
(
r#"<IFCPROPERTYSINGLEVALUE id="i1" Name="w"><NominalValue kind="real">1.0</NominalValue></IFCPROPERTYSINGLEVALUE>"#.into(),
"a bare value in a SELECT",
),
(
r#"<IFCPROPERTYSINGLEVALUE id="i1" Name="w"><NominalValue kind="typed" type="IFCWALLTYPEENUM"><value kind="enum">NOTDEFINED</value></NominalValue></IFCPROPERTYSINGLEVALUE>"#.into(),
"a typed value outside the SELECT",
),
(
r#"<IFCPROPERTYSINGLEVALUE id="i1" Name="w"><NominalValue kind="typed" type="IFCLENGTHMEASURE"><value kind="string">1</value></NominalValue></IFCPROPERTYSINGLEVALUE>"#.into(),
"a string in a typed measure",
),
(
format!(r#"<IFCWALL id="i1" GlobalId="{GUID}" ObjectPlacement="i2"/><IFCPERSON id="i2"/>"#),
"a reference to an entity of the wrong type",
),
(
format!(r#"<IFCWALL id="i1" GlobalId="{GUID}" ObjectPlacement="i9"/>"#),
"a reference to no entity",
),
] {
let error = read(&strict(), &body).unwrap_err();
assert!(
matches!(
error.root_cause(),
XmlError::TypeMismatch { .. } | XmlError::UnresolvedReference { .. }
),
"{why}: {error:?}"
);
}
let model = read(
&strict(),
r#"<IFCPROPERTYSINGLEVALUE id="i1" Name="w"><NominalValue kind="typed" type="IFCLABEL"><value kind="string">1</value></NominalValue></IFCPROPERTYSINGLEVALUE>"#,
)
.unwrap();
assert_eq!(
slot(&model, 1, "NominalValue"),
Value::Typed {
type_name: "IFCLABEL".into(),
value: Box::new(Value::Text("1".into())),
}
);
}
#[test]
fn a_non_conformant_model_is_refused_not_changed() {
let mut model = Model::new();
let mut wall = vec![Value::Null; ifc_schema::ifc4().attribute_names("IfcWall").len()];
wall[0] = Value::Text(GUID.into());
wall[2] = Value::Integer(1); model.insert(EntityId(1), Entity::new("IFCWALL", wall.clone()));
let xml = strict().write_bytes(&model).unwrap();
let error = ifc_xml::reader::read(&strict(), &xml).unwrap_err();
assert!(
matches!(error.root_cause(), XmlError::TypeMismatch { .. }),
"{error:?}"
);
let back = lenient().read_bytes(&xml).unwrap();
assert_eq!(back.get(EntityId(1)).unwrap().attributes, wall);
wall[2] = Value::Text("1".into());
model.insert(EntityId(1), Entity::new("IFCWALL", wall.clone()));
let xml = strict().write_bytes(&model).unwrap();
for codec in [strict(), lenient()] {
let back = codec.read_bytes(&xml).unwrap();
assert_eq!(back.get(EntityId(1)).unwrap().attributes, wall);
}
}