use serde::{Deserialize, Serialize};
use crate::axiom::Axiom;
use crate::entity::{EntityId, EntityKind};
use crate::error::{Error, Result};
use crate::limits::Limits;
use crate::ontology::Ontology;
const FORMAT_VERSION: u32 = 2;
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct OntologySnapshot {
format_version: u32,
entities: Vec<SnapshotEntity>,
axioms: Vec<SnapshotAxiom>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SnapshotEntity {
iri: String,
kind: EntityKind,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
#[serde(deny_unknown_fields)]
enum SnapshotAxiom {
SubClassOf {
subclass: String,
superclass: String,
},
EquivalentClasses(Vec<String>),
DisjointClasses(Vec<String>),
ObjectPropertyDomain {
property: String,
domain: String,
},
ObjectPropertyRange {
property: String,
range: String,
},
SubObjectPropertyOf {
sub_property: String,
super_property: String,
},
InverseObjectProperties {
left: String,
right: String,
},
TransitiveObjectProperty(String),
SubClassOfExistential {
subclass: String,
property: String,
filler: String,
},
SymmetricObjectProperty(String),
ReflexiveObjectProperty(String),
FunctionalObjectProperty(String),
AsymmetricObjectProperty(String),
EquivalentObjectProperties(Vec<String>),
ClassAssertion {
individual: String,
class: String,
},
ObjectPropertyAssertion {
subject: String,
property: String,
object: String,
},
SameIndividual(Vec<String>),
DifferentIndividuals(Vec<String>),
}
impl Ontology {
pub fn to_json(&self) -> Result<String> {
let snapshot = self.to_snapshot()?;
serde_json::to_string_pretty(&snapshot).map_err(|e| Error::Serialization(e.to_string()))
}
pub fn from_json(json: &str) -> Result<Self> {
Self::from_json_with_limits(json, Limits::default())
}
pub fn from_json_with_limits(json: &str, limits: Limits) -> Result<Self> {
if json.len() > limits.max_json_bytes {
return Err(Error::Serialization(format!(
"JSON input exceeds maximum size of {} bytes",
limits.max_json_bytes
)));
}
let value: serde_json::Value =
serde_json::from_str(json).map_err(|e| Error::Serialization(e.to_string()))?;
let format_version = value
.get("format_version")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| Error::Serialization("missing format_version".into()))?;
if format_version == 1 {
return Err(Error::Serialization(
"format_version 1 is not supported for untrusted input; use format_version 2"
.into(),
));
}
if format_version != u64::from(FORMAT_VERSION) {
return Err(Error::Serialization(format!(
"unsupported format_version: {format_version}"
)));
}
let snapshot: OntologySnapshot =
serde_json::from_value(value).map_err(|e| Error::Serialization(e.to_string()))?;
Self::from_snapshot(snapshot, limits)
}
fn to_snapshot(&self) -> Result<OntologySnapshot> {
let entities = self
.entities
.iter()
.map(|(_, record)| -> Result<SnapshotEntity> {
Ok(SnapshotEntity {
iri: self.iris.resolve(record.iri)?.to_owned(),
kind: record.kind,
})
})
.collect::<Result<Vec<_>>>()?;
let axioms = self
.axioms
.iter()
.map(|(_, axiom)| axiom_to_snapshot(axiom, self))
.collect::<Result<Vec<_>>>()?;
Ok(OntologySnapshot {
format_version: FORMAT_VERSION,
entities,
axioms,
})
}
fn from_snapshot(snapshot: OntologySnapshot, limits: Limits) -> Result<Self> {
if snapshot.format_version != FORMAT_VERSION {
return Err(Error::Serialization(format!(
"unsupported format_version: {}",
snapshot.format_version
)));
}
if snapshot.entities.len() > limits.max_entities {
return Err(Error::Serialization(format!(
"entity count exceeds maximum of {}",
limits.max_entities
)));
}
if snapshot.axioms.len() > limits.max_axioms {
return Err(Error::Serialization(format!(
"axiom count exceeds maximum of {}",
limits.max_axioms
)));
}
let mut ontology = Self::new();
let mut seen_iris = std::collections::HashSet::new();
for entity in snapshot.entities {
if !seen_iris.insert(entity.iri.clone()) {
return Err(Error::Serialization(format!(
"duplicate entity IRI in snapshot: {}",
entity.iri
)));
}
let iri_id = ontology
.iris
.intern_with_limit(&entity.iri, limits.max_iri_len)?;
let iri_str = ontology.iris.resolve(iri_id)?;
ontology
.entities
.get_or_register(iri_id, iri_str, entity.kind)?;
}
for axiom in snapshot.axioms {
let axiom = snapshot_axiom_to_axiom(&axiom, &ontology)?;
axiom.validate_with_limits(&ontology.entities, limits)?;
ontology.add_axiom(axiom)?;
}
Ok(ontology)
}
}
fn entity_iri(ontology: &Ontology, id: EntityId) -> Result<String> {
let record = ontology.entity(id)?;
Ok(ontology.iris.resolve(record.iri)?.to_owned())
}
fn resolve_entity(ontology: &Ontology, iri: &str) -> Result<EntityId> {
ontology
.try_lookup_entity(iri)?
.ok_or_else(|| Error::InvalidAxiom(format!("unknown entity IRI in axiom: {iri}")))
}
fn axiom_to_snapshot(axiom: &Axiom, ontology: &Ontology) -> Result<SnapshotAxiom> {
Ok(match axiom {
Axiom::SubClassOf {
subclass,
superclass,
} => SnapshotAxiom::SubClassOf {
subclass: entity_iri(ontology, *subclass)?,
superclass: entity_iri(ontology, *superclass)?,
},
Axiom::EquivalentClasses(classes) => SnapshotAxiom::EquivalentClasses(
classes
.iter()
.map(|id| entity_iri(ontology, *id))
.collect::<Result<Vec<_>>>()?,
),
Axiom::DisjointClasses(classes) => SnapshotAxiom::DisjointClasses(
classes
.iter()
.map(|id| entity_iri(ontology, *id))
.collect::<Result<Vec<_>>>()?,
),
Axiom::ObjectPropertyDomain { property, domain } => SnapshotAxiom::ObjectPropertyDomain {
property: entity_iri(ontology, *property)?,
domain: entity_iri(ontology, *domain)?,
},
Axiom::ObjectPropertyRange { property, range } => SnapshotAxiom::ObjectPropertyRange {
property: entity_iri(ontology, *property)?,
range: entity_iri(ontology, *range)?,
},
Axiom::SubObjectPropertyOf {
sub_property,
super_property,
} => SnapshotAxiom::SubObjectPropertyOf {
sub_property: entity_iri(ontology, *sub_property)?,
super_property: entity_iri(ontology, *super_property)?,
},
Axiom::InverseObjectProperties { left, right } => SnapshotAxiom::InverseObjectProperties {
left: entity_iri(ontology, *left)?,
right: entity_iri(ontology, *right)?,
},
Axiom::TransitiveObjectProperty(property) => {
SnapshotAxiom::TransitiveObjectProperty(entity_iri(ontology, *property)?)
}
Axiom::SubClassOfExistential {
subclass,
property,
filler,
} => SnapshotAxiom::SubClassOfExistential {
subclass: entity_iri(ontology, *subclass)?,
property: entity_iri(ontology, *property)?,
filler: entity_iri(ontology, *filler)?,
},
Axiom::SymmetricObjectProperty(property) => {
SnapshotAxiom::SymmetricObjectProperty(entity_iri(ontology, *property)?)
}
Axiom::ReflexiveObjectProperty(property) => {
SnapshotAxiom::ReflexiveObjectProperty(entity_iri(ontology, *property)?)
}
Axiom::FunctionalObjectProperty(property) => {
SnapshotAxiom::FunctionalObjectProperty(entity_iri(ontology, *property)?)
}
Axiom::AsymmetricObjectProperty(property) => {
SnapshotAxiom::AsymmetricObjectProperty(entity_iri(ontology, *property)?)
}
Axiom::EquivalentObjectProperties(properties) => SnapshotAxiom::EquivalentObjectProperties(
properties
.iter()
.map(|id| entity_iri(ontology, *id))
.collect::<Result<Vec<_>>>()?,
),
Axiom::ClassAssertion { individual, class } => SnapshotAxiom::ClassAssertion {
individual: entity_iri(ontology, *individual)?,
class: entity_iri(ontology, *class)?,
},
Axiom::ObjectPropertyAssertion {
subject,
property,
object,
} => SnapshotAxiom::ObjectPropertyAssertion {
subject: entity_iri(ontology, *subject)?,
property: entity_iri(ontology, *property)?,
object: entity_iri(ontology, *object)?,
},
Axiom::SameIndividual(individuals) => SnapshotAxiom::SameIndividual(
individuals
.iter()
.map(|id| entity_iri(ontology, *id))
.collect::<Result<Vec<_>>>()?,
),
Axiom::DifferentIndividuals(individuals) => SnapshotAxiom::DifferentIndividuals(
individuals
.iter()
.map(|id| entity_iri(ontology, *id))
.collect::<Result<Vec<_>>>()?,
),
})
}
fn snapshot_axiom_to_axiom(snapshot: &SnapshotAxiom, ontology: &Ontology) -> Result<Axiom> {
Ok(match snapshot {
SnapshotAxiom::SubClassOf {
subclass,
superclass,
} => Axiom::SubClassOf {
subclass: resolve_entity(ontology, subclass)?,
superclass: resolve_entity(ontology, superclass)?,
},
SnapshotAxiom::EquivalentClasses(classes) => Axiom::EquivalentClasses(
classes
.iter()
.map(|iri| resolve_entity(ontology, iri))
.collect::<Result<Vec<_>>>()?,
),
SnapshotAxiom::DisjointClasses(classes) => Axiom::DisjointClasses(
classes
.iter()
.map(|iri| resolve_entity(ontology, iri))
.collect::<Result<Vec<_>>>()?,
),
SnapshotAxiom::ObjectPropertyDomain { property, domain } => Axiom::ObjectPropertyDomain {
property: resolve_entity(ontology, property)?,
domain: resolve_entity(ontology, domain)?,
},
SnapshotAxiom::ObjectPropertyRange { property, range } => Axiom::ObjectPropertyRange {
property: resolve_entity(ontology, property)?,
range: resolve_entity(ontology, range)?,
},
SnapshotAxiom::SubObjectPropertyOf {
sub_property,
super_property,
} => Axiom::SubObjectPropertyOf {
sub_property: resolve_entity(ontology, sub_property)?,
super_property: resolve_entity(ontology, super_property)?,
},
SnapshotAxiom::InverseObjectProperties { left, right } => Axiom::InverseObjectProperties {
left: resolve_entity(ontology, left)?,
right: resolve_entity(ontology, right)?,
},
SnapshotAxiom::TransitiveObjectProperty(property) => {
Axiom::TransitiveObjectProperty(resolve_entity(ontology, property)?)
}
SnapshotAxiom::SubClassOfExistential {
subclass,
property,
filler,
} => Axiom::SubClassOfExistential {
subclass: resolve_entity(ontology, subclass)?,
property: resolve_entity(ontology, property)?,
filler: resolve_entity(ontology, filler)?,
},
SnapshotAxiom::SymmetricObjectProperty(property) => {
Axiom::SymmetricObjectProperty(resolve_entity(ontology, property)?)
}
SnapshotAxiom::ReflexiveObjectProperty(property) => {
Axiom::ReflexiveObjectProperty(resolve_entity(ontology, property)?)
}
SnapshotAxiom::FunctionalObjectProperty(property) => {
Axiom::FunctionalObjectProperty(resolve_entity(ontology, property)?)
}
SnapshotAxiom::AsymmetricObjectProperty(property) => {
Axiom::AsymmetricObjectProperty(resolve_entity(ontology, property)?)
}
SnapshotAxiom::EquivalentObjectProperties(properties) => Axiom::EquivalentObjectProperties(
properties
.iter()
.map(|iri| resolve_entity(ontology, iri))
.collect::<Result<Vec<_>>>()?,
),
SnapshotAxiom::ClassAssertion { individual, class } => Axiom::ClassAssertion {
individual: resolve_entity(ontology, individual)?,
class: resolve_entity(ontology, class)?,
},
SnapshotAxiom::ObjectPropertyAssertion {
subject,
property,
object,
} => Axiom::ObjectPropertyAssertion {
subject: resolve_entity(ontology, subject)?,
property: resolve_entity(ontology, property)?,
object: resolve_entity(ontology, object)?,
},
SnapshotAxiom::SameIndividual(individuals) => Axiom::SameIndividual(
individuals
.iter()
.map(|iri| resolve_entity(ontology, iri))
.collect::<Result<Vec<_>>>()?,
),
SnapshotAxiom::DifferentIndividuals(individuals) => Axiom::DifferentIndividuals(
individuals
.iter()
.map(|iri| resolve_entity(ontology, iri))
.collect::<Result<Vec<_>>>()?,
),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_json_preserves_semantics() {
let ontology = Ontology::builder()
.class("http://example.org/Pizza")
.expect("class")
.class("http://example.org/Food")
.expect("class")
.object_property("http://example.org/hasTopping")
.expect("property")
.class("http://example.org/Topping")
.expect("class")
.subclass_of("http://example.org/Pizza", "http://example.org/Food")
.expect("subclass")
.build()
.expect("build");
let json = ontology.to_json().expect("to_json");
assert!(json.contains("\"format_version\": 2"));
let restored = Ontology::from_json(&json).expect("from_json");
assert_eq!(restored, ontology);
let pizza = restored
.lookup_entity("http://example.org/Pizza")
.expect("pizza");
let food = restored
.lookup_entity("http://example.org/Food")
.expect("food");
assert_eq!(restored.direct_superclasses(pizza), &[food]);
}
#[test]
fn rejects_format_version_1() {
let json = r#"{
"format_version": 1,
"iris": ["http://example.org/A"],
"entities": [{"iri_index": 1, "kind": "Class"}],
"axioms": []
}"#;
let err = Ontology::from_json(json).expect_err("v1");
assert!(matches!(err, Error::Serialization(_)));
}
#[test]
fn rejects_unsupported_format_version() {
let json = r#"{"format_version":99,"entities":[],"axioms":[]}"#;
let err = Ontology::from_json(json).expect_err("version");
assert!(matches!(err, Error::Serialization(_)));
}
#[test]
fn rejects_invalid_json_syntax() {
let err = Ontology::from_json("{not json").expect_err("json");
assert!(matches!(err, Error::Serialization(_)));
}
#[test]
fn rejects_unknown_entity_iri_in_axiom() {
let json = r#"{
"format_version": 2,
"entities": [
{"iri": "http://example.org/A", "kind": "Class"},
{"iri": "http://example.org/B", "kind": "Class"}
],
"axioms": [
{"SubClassOf": {"subclass": "http://example.org/A", "superclass": "http://example.org/Missing"}}
]
}"#;
let err = Ontology::from_json(json).expect_err("entity");
assert!(matches!(err, Error::InvalidAxiom(_)));
}
#[test]
fn rejects_duplicate_entity_iris() {
let json = r#"{
"format_version": 2,
"entities": [
{"iri": "http://example.org/A", "kind": "Class"},
{"iri": "http://example.org/A", "kind": "Class"}
],
"axioms": []
}"#;
let err = Ontology::from_json(json).expect_err("dup");
assert!(matches!(err, Error::Serialization(_)));
}
#[test]
fn rejects_unknown_snapshot_fields() {
let json = r#"{
"format_version": 2,
"entitys": [],
"entities": [],
"axioms": []
}"#;
let err = Ontology::from_json(json).expect_err("typo");
assert!(matches!(err, Error::Serialization(_)));
}
#[test]
fn rejects_oversized_json() {
let limits = Limits {
max_json_bytes: 10,
..Limits::default()
};
let json = r#"{"format_version":2,"entities":[],"axioms":[]}"#;
let err = Ontology::from_json_with_limits(json, limits).expect_err("size");
assert!(matches!(err, Error::Serialization(_)));
}
#[test]
fn rejects_unknown_axiom_fields() {
let json = r#"{
"format_version": 2,
"entities": [
{"iri": "http://example.org/A", "kind": "Class"},
{"iri": "http://example.org/B", "kind": "Class"}
],
"axioms": [
{"SubClassOf": {"subclass": "http://example.org/A", "superclass": "http://example.org/B", "extra": true}}
]
}"#;
let err = Ontology::from_json(json).expect_err("unknown axiom field");
assert!(matches!(err, Error::Serialization(_)));
}
#[test]
fn round_trip_subclass_of_existential() {
let ontology = Ontology::builder()
.class("http://example.org/C")
.expect("class")
.class("http://example.org/B")
.expect("class")
.object_property("http://example.org/hasPart")
.expect("property")
.build()
.expect("build");
let mut ontology = ontology;
let c = ontology.lookup_entity("http://example.org/C").expect("C");
let b = ontology.lookup_entity("http://example.org/B").expect("B");
let has_part = ontology
.lookup_entity("http://example.org/hasPart")
.expect("hasPart");
ontology
.add_axiom(crate::axiom::Axiom::SubClassOfExistential {
subclass: c,
property: has_part,
filler: b,
})
.expect("axiom");
let json = ontology.to_json().expect("to_json");
let restored = Ontology::from_json(&json).expect("from_json");
assert_eq!(restored.axiom_count(), 1);
assert!(restored.direct_superclasses(c).is_empty());
assert_eq!(restored.existentials_of(c), &[(has_part, b)]);
}
#[test]
fn round_trip_abox_axiom_variants() {
let ontology = Ontology::builder()
.individual("http://example.org/alice")
.expect("alice")
.individual("http://example.org/bob")
.expect("bob")
.class("http://example.org/Person")
.expect("Person")
.object_property("http://example.org/knows")
.expect("knows")
.class_assertion("http://example.org/alice", "http://example.org/Person")
.expect("type")
.object_property_assertion(
"http://example.org/alice",
"http://example.org/knows",
"http://example.org/bob",
)
.expect("assertion")
.same_individual(&["http://example.org/alice", "http://example.org/bob"])
.expect("same")
.build()
.expect("build");
let json = ontology.to_json().expect("to_json");
let restored = Ontology::from_json(&json).expect("from_json");
assert_eq!(restored.axiom_count(), 3);
let alice = restored
.lookup_entity("http://example.org/alice")
.expect("alice");
let person = restored
.lookup_entity("http://example.org/Person")
.expect("Person");
assert_eq!(restored.classes_of(alice), &[person]);
}
#[test]
fn round_trip_rl_property_axiom_variants() {
let mut ontology = Ontology::builder()
.object_property("http://example.org/symmetric")
.expect("symmetric")
.object_property("http://example.org/reflexive")
.expect("reflexive")
.object_property("http://example.org/functional")
.expect("functional")
.build()
.expect("build");
let symmetric = ontology
.lookup_entity("http://example.org/symmetric")
.expect("symmetric");
let reflexive = ontology
.lookup_entity("http://example.org/reflexive")
.expect("reflexive");
let functional = ontology
.lookup_entity("http://example.org/functional")
.expect("functional");
ontology
.add_axiom(crate::axiom::Axiom::SymmetricObjectProperty(symmetric))
.expect("symmetric");
ontology
.add_axiom(crate::axiom::Axiom::ReflexiveObjectProperty(reflexive))
.expect("reflexive");
ontology
.add_axiom(crate::axiom::Axiom::FunctionalObjectProperty(functional))
.expect("functional");
let json = ontology.to_json().expect("to_json");
let restored = Ontology::from_json(&json).expect("from_json");
assert_eq!(restored.axiom_count(), 3);
assert_eq!(restored.index().by_kind("SymmetricObjectProperty").len(), 1);
assert_eq!(restored.index().by_kind("ReflexiveObjectProperty").len(), 1);
assert_eq!(
restored.index().by_kind("FunctionalObjectProperty").len(),
1
);
}
}