mod support;
use iri_rfc3987::Iri;
use owl_structural_model::{KeyProperties, ObjectPropertyExpression};
type ClassExpression = owl_structural_model::ClassExpression;
type Annotation = owl_structural_model::Annotation;
type AnnotationValue = owl_structural_model::AnnotationValue;
type Axiom = owl_structural_model::Axiom;
type AxiomAnnotations = owl_structural_model::AxiomAnnotations;
type DataRange = owl_structural_model::DataRange;
fn iri(value: &str) -> Iri<String> {
Iri::parse(value).unwrap().into()
}
fn property(value: &str) -> ObjectPropertyExpression {
ObjectPropertyExpression::named(iri(value))
}
#[test]
fn object_property_relationships_preserve_operands_and_annotations() {
let p = property("urn:parent");
let q = ObjectPropertyExpression::inverse_of(iri("urn:child"));
let equivalent = Axiom::equivalent_object_properties(vec![p.clone(), q.clone()]).unwrap();
assert!(
matches!(&equivalent, Axiom::EquivalentObjectProperties { properties, .. }
if properties.as_slice() == [p.clone(), q.clone()])
);
let disjoint = Axiom::disjoint_object_properties(vec![p.clone(), q.clone()]).unwrap();
assert!(
matches!(&disjoint, Axiom::DisjointObjectProperties { properties, .. }
if properties.as_slice() == [p.clone(), q.clone()])
);
let inverse = Axiom::inverse_object_properties(p.clone(), q.clone());
assert!(
matches!(&inverse, Axiom::InverseObjectProperties { properties, .. }
if properties.as_slice() == [p.clone(), q.clone()])
);
let range = Axiom::object_property_range(p.clone(), ClassExpression::thing());
assert!(
matches!(&range, Axiom::ObjectPropertyRange { property, range: actual_range, .. }
if property == &p && actual_range == &ClassExpression::thing())
);
let domain = Axiom::object_property_domain(p.clone(), ClassExpression::nothing());
assert!(
matches!(&domain, Axiom::ObjectPropertyDomain { property, domain: actual_domain, .. }
if property == &p && actual_domain == &ClassExpression::nothing())
);
for axiom in [equivalent, disjoint, inverse, range, domain] {
assert_annotated_round_trip(axiom);
}
}
#[test]
fn data_property_relationships_and_datatype_definitions_preserve_ranges() {
let p = iri("urn:age");
let q = iri("urn:years");
let sub = Axiom::sub_data_property_of(p.clone(), q.clone());
assert!(
matches!(&sub, Axiom::SubDataPropertyOf { sub_property, super_property, .. }
if sub_property == &p && super_property == &q)
);
let equivalent = Axiom::equivalent_data_properties(vec![p.clone(), q.clone()]).unwrap();
assert!(
matches!(&equivalent, Axiom::EquivalentDataProperties { properties, .. }
if properties.as_slice() == [p.clone(), q.clone()])
);
let disjoint = Axiom::disjoint_data_properties(vec![p.clone(), q.clone()]).unwrap();
assert!(
matches!(&disjoint, Axiom::DisjointDataProperties { properties, .. }
if properties.as_slice() == [p.clone(), q])
);
let domain = Axiom::data_property_domain(p.clone(), ClassExpression::thing());
assert!(
matches!(&domain, Axiom::DataPropertyDomain { property, domain: actual_domain, .. }
if property == &p && actual_domain == &ClassExpression::thing())
);
let range = DataRange::data_complement_of(DataRange::datatype(iri("urn:negative")));
let range_axiom = Axiom::data_property_range(p.clone(), range.clone());
assert!(
matches!(&range_axiom, Axiom::DataPropertyRange { property, range: actual, .. }
if property == &p && actual == &range)
);
let definition = Axiom::datatype_definition(iri("urn:positive"), range.clone());
assert!(
matches!(&definition, Axiom::DatatypeDefinition { datatype, range: actual, .. }
if datatype == &iri("urn:positive") && actual == &range)
);
for axiom in [sub, equivalent, disjoint, domain, range_axiom, definition] {
assert_annotated_round_trip(axiom);
}
}
#[test]
fn equivalent_and_disjoint_properties_require_two_operands() {
for objects in [vec![], vec![property("urn:p")]] {
assert!(Axiom::equivalent_object_properties(objects.clone()).is_err());
assert!(Axiom::disjoint_object_properties(objects.clone()).is_err());
for kind in ["EquivalentObjectProperties", "DisjointObjectProperties"] {
assert_bad_properties(kind, &objects);
}
}
for data in [vec![], vec![iri("urn:p")]] {
assert!(Axiom::equivalent_data_properties(data.clone()).is_err());
assert!(Axiom::disjoint_data_properties(data.clone()).is_err());
for kind in ["EquivalentDataProperties", "DisjointDataProperties"] {
assert_bad_properties(kind, &data);
}
}
}
fn assert_bad_properties<T: serde::Serialize>(kind: &str, properties: &[T]) {
let properties = serde_json::to_string(properties).unwrap();
let encoded = format!(
r#"{{"{kind}":{{"properties":{properties},"annotations":{{"annotations":[]}}}}}}"#,
);
let error = serde_json::from_str::<Axiom>(&encoded).unwrap_err();
assert!(error.to_string().contains("requires at least two"));
}
#[test]
fn keys_keep_object_and_data_properties_separate() {
let p = ObjectPropertyExpression::inverse_of(iri("urn:parent"));
let d = iri("urn:identifier");
for (objects, data) in [
(vec![p.clone()], vec![]),
(vec![], vec![d.clone()]),
(vec![p.clone()], vec![d.clone()]),
] {
let properties = KeyProperties::try_new(objects.clone(), data.clone()).unwrap();
assert_eq!(properties.object_properties(), objects);
assert_eq!(properties.data_properties(), data);
let class = ClassExpression::object_complement_of(ClassExpression::nothing());
let axiom = Axiom::has_key(class.clone(), properties.clone());
assert!(
matches!(&axiom, Axiom::HasKey { class: actual, properties: actual_properties, .. }
if actual == &class && actual_properties == &properties)
);
assert_annotated_round_trip(axiom);
}
}
#[test]
fn empty_keys_fail_at_construction_and_deserialization() {
assert!(KeyProperties::try_new(vec![], vec![]).is_err());
let error =
serde_json::from_str::<KeyProperties>(r#"{"object_properties":[],"data_properties":[]}"#)
.unwrap_err();
assert!(
error
.to_string()
.contains("requires at least one key property")
);
let error = serde_json::from_str::<Axiom>(
r#"{"HasKey":{"class":{"Named":"http://www.w3.org/2002/07/owl#Thing"},"properties":{"object_properties":[],"data_properties":[]},"annotations":{"annotations":[]}}}"#,
).unwrap_err();
assert!(
error
.to_string()
.contains("requires at least one key property")
);
}
fn assert_annotated_round_trip(axiom: Axiom) {
assert!(axiom.annotations().is_empty());
let annotations = AxiomAnnotations::new(vec![Annotation::new(
iri("urn:source"),
AnnotationValue::Iri(iri("urn:fixture")),
)]);
let annotated = axiom.with_annotations(annotations.clone());
assert_eq!(annotated.annotations(), &annotations);
support::assert_round_trip(&annotated);
}