Skip to main content

strixonomy_owl/
bridge.rs

1use crate::manchester::class_expression_to_manchester;
2use crate::span::{annotate_spans, find_entity_block, short_name_from_iri};
3use horned_owl::model::{
4    AnnotatedComponent, AnnotationSubject, AnnotationValue, ClassExpression, Component,
5    ComponentKind, Individual, ObjectPropertyExpression, PropertyExpression, RcAnnotatedComponent,
6    RcStr, SubObjectPropertyExpression,
7};
8use horned_owl::ontology::component_mapped::{ComponentMappedIndex, ComponentMappedOntology};
9use std::collections::BTreeMap;
10use strixonomy_core::{
11    Annotation, Axiom, AxiomAnnotation, Entity, EntityKind, Import, Namespace, SourceLocation,
12    AXIOM_KIND_CLASS_ASSERTION, AXIOM_KIND_DATATYPE_DEFINITION, AXIOM_KIND_DATA_PROPERTY_ASSERTION,
13    AXIOM_KIND_DIFFERENT_INDIVIDUALS, AXIOM_KIND_DISJOINT_CLASS,
14    AXIOM_KIND_DISJOINT_DATA_PROPERTIES, AXIOM_KIND_DISJOINT_OBJECT_PROPERTIES,
15    AXIOM_KIND_DISJOINT_UNION, AXIOM_KIND_DOMAIN, AXIOM_KIND_EQUIVALENT_CLASS,
16    AXIOM_KIND_EQUIVALENT_DATA_PROPERTIES, AXIOM_KIND_EQUIVALENT_OBJECT_PROPERTIES,
17    AXIOM_KIND_HAS_KEY, AXIOM_KIND_INVERSE_OBJECT_PROPERTIES,
18    AXIOM_KIND_NEGATIVE_DATA_PROPERTY_ASSERTION, AXIOM_KIND_NEGATIVE_OBJECT_PROPERTY_ASSERTION,
19    AXIOM_KIND_OBJECT_PROPERTY_ASSERTION, AXIOM_KIND_PROPERTY_CHAIN, AXIOM_KIND_RANGE,
20    AXIOM_KIND_SAME_INDIVIDUAL, AXIOM_KIND_SUB_CLASS_OF, AXIOM_KIND_SUB_DATA_PROPERTY_OF,
21    AXIOM_KIND_SUB_OBJECT_PROPERTY_OF,
22};
23
24const RDFS_LABEL: &str = "http://www.w3.org/2000/01/rdf-schema#label";
25const RDFS_COMMENT: &str = "http://www.w3.org/2000/01/rdf-schema#comment";
26const RDFS_SUB_CLASS_OF: &str = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
27const RDFS_SUB_PROPERTY_OF: &str = "http://www.w3.org/2000/01/rdf-schema#subPropertyOf";
28const OWL_DEPRECATED: &str = "http://www.w3.org/2002/07/owl#deprecated";
29const OWL_SAME_AS: &str = "http://www.w3.org/2002/07/owl#sameAs";
30const OWL_DIFFERENT_FROM: &str = "http://www.w3.org/2002/07/owl#differentFrom";
31const OWL_HAS_KEY: &str = "http://www.w3.org/2002/07/owl#hasKey";
32const OWL_DISJOINT_UNION_OF: &str = "http://www.w3.org/2002/07/owl#disjointUnionOf";
33const OWL_INVERSE_OF: &str = "http://www.w3.org/2002/07/owl#inverseOf";
34const OWL_EQUIVALENT_PROPERTY: &str = "http://www.w3.org/2002/07/owl#equivalentProperty";
35const OWL_PROPERTY_DISJOINT_WITH: &str = "http://www.w3.org/2002/07/owl#propertyDisjointWith";
36const OWL_EQUIVALENT_CLASS: &str = "http://www.w3.org/2002/07/owl#equivalentClass";
37const MEMBER_SEP: &str = " | ";
38
39/// Catalog-shaped extraction from a Horned-OWL ontology.
40#[derive(Debug, Clone, Default)]
41pub struct OwlBridgeResult {
42    pub entities: Vec<Entity>,
43    pub annotations: Vec<Annotation>,
44    pub axioms: Vec<Axiom>,
45    pub imports: Vec<Import>,
46    pub namespace_rows: Vec<Namespace>,
47    pub base_iri: Option<String>,
48}
49
50pub fn bridge_ontology(
51    ontology: impl Into<ComponentMappedOntology<RcStr, RcAnnotatedComponent>>,
52    ontology_id: &str,
53    source_text: &str,
54    namespaces: &BTreeMap<String, String>,
55) -> OwlBridgeResult {
56    let mapped = ontology.into();
57    let idx: &ComponentMappedIndex<RcStr, RcAnnotatedComponent> = mapped.i();
58
59    let mut result = OwlBridgeResult::default();
60    let ont_id = idx
61        .the_ontology_id()
62        .and_then(|id| id.iri.as_ref().map(|iri| iri.to_string()))
63        .unwrap_or_else(|| ontology_id.to_string());
64    result.base_iri = Some(ont_id.clone());
65
66    let mut entity_map: BTreeMap<String, Entity> = BTreeMap::new();
67
68    for decl in idx.declare_class() {
69        insert_entity(&mut entity_map, decl.0.to_string(), EntityKind::Class, &ont_id);
70    }
71    for decl in idx.declare_object_property() {
72        insert_entity(&mut entity_map, decl.0.to_string(), EntityKind::ObjectProperty, &ont_id);
73    }
74    for decl in idx.declare_data_property() {
75        insert_entity(&mut entity_map, decl.0.to_string(), EntityKind::DataProperty, &ont_id);
76    }
77    for decl in idx.declare_annotation_property() {
78        insert_entity(&mut entity_map, decl.0.to_string(), EntityKind::AnnotationProperty, &ont_id);
79    }
80    for decl in idx.declare_named_individual() {
81        insert_entity(&mut entity_map, decl.0.to_string(), EntityKind::Individual, &ont_id);
82    }
83    for decl in idx.declare_datatype() {
84        insert_entity(&mut entity_map, decl.0.to_string(), EntityKind::Datatype, &ont_id);
85    }
86
87    for ann_ax in idx.annotation_assertion() {
88        let subject = annotation_subject_iri(&ann_ax.subject);
89        let predicate = ann_ax.ann.ap.to_string();
90        let object = annotation_value_string(&ann_ax.ann.av);
91        if predicate == RDFS_LABEL {
92            entity_map.entry(subject.clone()).and_modify(|e| e.labels.push(object.clone()));
93        } else if predicate == RDFS_COMMENT {
94            entity_map.entry(subject.clone()).and_modify(|e| e.comments.push(object.clone()));
95        } else if predicate == OWL_DEPRECATED
96            && strixonomy_core::parse_boolean_literal(&object) == Some(true)
97        {
98            entity_map.entry(subject.clone()).and_modify(|e| e.deprecated = true);
99        }
100        result.annotations.push(Annotation {
101            subject,
102            predicate,
103            object,
104            ontology_id: ont_id.clone(),
105            source_location: SourceLocation::default(),
106        });
107    }
108
109    // Project SameIndividual as owl:sameAs annotations so semantic-diff rename detection works (#312).
110    for si in idx.same_individual() {
111        for a in &si.0 {
112            let subject = individual_to_iri(a);
113            for b in &si.0 {
114                if a == b {
115                    continue;
116                }
117                result.annotations.push(Annotation {
118                    subject: subject.clone(),
119                    predicate: OWL_SAME_AS.to_string(),
120                    object: individual_to_iri(b),
121                    ontology_id: ont_id.clone(),
122                    source_location: SourceLocation::default(),
123                });
124            }
125        }
126    }
127
128    let mut axiom_counter = 0usize;
129
130    for ac in idx.component_for_kind(ComponentKind::SubClassOf) {
131        let Component::SubClassOf(ax) = &ac.component else {
132            continue;
133        };
134        if let ClassExpression::Class(sub) = &ax.sub {
135            if let Some(sup) = class_expr_display(&ax.sup, namespaces) {
136                push_axiom(
137                    &mut result.axioms,
138                    &mut axiom_counter,
139                    ontology_id,
140                    &ont_id,
141                    sub.to_string(),
142                    RDFS_SUB_CLASS_OF.to_string(),
143                    sup,
144                    AXIOM_KIND_SUB_CLASS_OF,
145                    ann_bag(ac),
146                );
147            }
148        }
149    }
150
151    for ac in idx.component_for_kind(ComponentKind::EquivalentClasses) {
152        let Component::EquivalentClasses(eq) = &ac.component else {
153            continue;
154        };
155        let anns = ann_bag(ac);
156        for ce in &eq.0 {
157            if let ClassExpression::Class(sub) = ce {
158                for other in &eq.0 {
159                    if other == ce {
160                        continue;
161                    }
162                    if let Some(obj) = class_expr_display(other, namespaces) {
163                        push_axiom(
164                            &mut result.axioms,
165                            &mut axiom_counter,
166                            ontology_id,
167                            &ont_id,
168                            sub.to_string(),
169                            OWL_EQUIVALENT_CLASS.to_string(),
170                            obj,
171                            AXIOM_KIND_EQUIVALENT_CLASS,
172                            anns.clone(),
173                        );
174                    }
175                }
176            }
177        }
178    }
179
180    for ac in idx.component_for_kind(ComponentKind::DisjointClasses) {
181        let Component::DisjointClasses(disj) = &ac.component else {
182            continue;
183        };
184        let anns = ann_bag(ac);
185        for ce in &disj.0 {
186            if let ClassExpression::Class(sub) = ce {
187                for other in &disj.0 {
188                    if other == ce {
189                        continue;
190                    }
191                    if let Some(obj) = class_expr_display(other, namespaces) {
192                        push_axiom(
193                            &mut result.axioms,
194                            &mut axiom_counter,
195                            ontology_id,
196                            &ont_id,
197                            sub.to_string(),
198                            "http://www.w3.org/2002/07/owl#disjointWith".to_string(),
199                            obj,
200                            AXIOM_KIND_DISJOINT_CLASS,
201                            anns.clone(),
202                        );
203                    }
204                }
205            }
206        }
207    }
208
209    for ac in idx.component_for_kind(ComponentKind::DisjointUnion) {
210        let Component::DisjointUnion(du) = &ac.component else {
211            continue;
212        };
213        let members: Vec<String> =
214            du.1.iter().filter_map(|ce| class_expr_display(ce, namespaces)).collect();
215        push_axiom(
216            &mut result.axioms,
217            &mut axiom_counter,
218            ontology_id,
219            &ont_id,
220            du.0.to_string(),
221            OWL_DISJOINT_UNION_OF.to_string(),
222            members.join(MEMBER_SEP),
223            AXIOM_KIND_DISJOINT_UNION,
224            ann_bag(ac),
225        );
226    }
227
228    for ac in idx.component_for_kind(ComponentKind::HasKey) {
229        let Component::HasKey(hk) = &ac.component else {
230            continue;
231        };
232        let ClassExpression::Class(cls) = &hk.ce else {
233            continue;
234        };
235        let props: Vec<String> = hk.vpe.iter().map(property_expr_to_iri).collect();
236        push_axiom(
237            &mut result.axioms,
238            &mut axiom_counter,
239            ontology_id,
240            &ont_id,
241            cls.to_string(),
242            OWL_HAS_KEY.to_string(),
243            props.join(MEMBER_SEP),
244            AXIOM_KIND_HAS_KEY,
245            ann_bag(ac),
246        );
247    }
248
249    for ac in idx.component_for_kind(ComponentKind::InverseObjectProperties) {
250        let Component::InverseObjectProperties(inv) = &ac.component else {
251            continue;
252        };
253        let anns = ann_bag(ac);
254        let a = inv.0.to_string();
255        let b = inv.1.to_string();
256        push_axiom(
257            &mut result.axioms,
258            &mut axiom_counter,
259            ontology_id,
260            &ont_id,
261            a.clone(),
262            OWL_INVERSE_OF.to_string(),
263            b.clone(),
264            AXIOM_KIND_INVERSE_OBJECT_PROPERTIES,
265            anns.clone(),
266        );
267        push_axiom(
268            &mut result.axioms,
269            &mut axiom_counter,
270            ontology_id,
271            &ont_id,
272            b,
273            OWL_INVERSE_OF.to_string(),
274            a,
275            AXIOM_KIND_INVERSE_OBJECT_PROPERTIES,
276            anns,
277        );
278    }
279
280    for ac in idx.component_for_kind(ComponentKind::EquivalentObjectProperties) {
281        let Component::EquivalentObjectProperties(eq) = &ac.component else {
282            continue;
283        };
284        let anns = ann_bag(ac);
285        let iris: Vec<String> = eq.0.iter().map(ope_to_iri).collect();
286        push_pairwise_property_axioms(
287            &mut result.axioms,
288            &mut axiom_counter,
289            ontology_id,
290            &ont_id,
291            &iris,
292            OWL_EQUIVALENT_PROPERTY,
293            AXIOM_KIND_EQUIVALENT_OBJECT_PROPERTIES,
294            anns,
295        );
296    }
297
298    for ac in idx.component_for_kind(ComponentKind::DisjointObjectProperties) {
299        let Component::DisjointObjectProperties(dj) = &ac.component else {
300            continue;
301        };
302        let anns = ann_bag(ac);
303        let iris: Vec<String> = dj.0.iter().map(ope_to_iri).collect();
304        push_pairwise_property_axioms(
305            &mut result.axioms,
306            &mut axiom_counter,
307            ontology_id,
308            &ont_id,
309            &iris,
310            OWL_PROPERTY_DISJOINT_WITH,
311            AXIOM_KIND_DISJOINT_OBJECT_PROPERTIES,
312            anns,
313        );
314    }
315
316    for ac in idx.component_for_kind(ComponentKind::EquivalentDataProperties) {
317        let Component::EquivalentDataProperties(eq) = &ac.component else {
318            continue;
319        };
320        let anns = ann_bag(ac);
321        let iris: Vec<String> = eq.0.iter().map(|dp| dp.to_string()).collect();
322        push_pairwise_property_axioms(
323            &mut result.axioms,
324            &mut axiom_counter,
325            ontology_id,
326            &ont_id,
327            &iris,
328            OWL_EQUIVALENT_PROPERTY,
329            AXIOM_KIND_EQUIVALENT_DATA_PROPERTIES,
330            anns,
331        );
332    }
333
334    for ac in idx.component_for_kind(ComponentKind::DisjointDataProperties) {
335        let Component::DisjointDataProperties(dj) = &ac.component else {
336            continue;
337        };
338        let anns = ann_bag(ac);
339        let iris: Vec<String> = dj.0.iter().map(|dp| dp.to_string()).collect();
340        push_pairwise_property_axioms(
341            &mut result.axioms,
342            &mut axiom_counter,
343            ontology_id,
344            &ont_id,
345            &iris,
346            OWL_PROPERTY_DISJOINT_WITH,
347            AXIOM_KIND_DISJOINT_DATA_PROPERTIES,
348            anns,
349        );
350    }
351
352    for ac in idx.component_for_kind(ComponentKind::ObjectPropertyDomain) {
353        let Component::ObjectPropertyDomain(dom) = &ac.component else {
354            continue;
355        };
356        let prop = ope_to_iri(&dom.ope);
357        if let ClassExpression::Class(cls) = &dom.ce {
358            push_axiom(
359                &mut result.axioms,
360                &mut axiom_counter,
361                ontology_id,
362                &ont_id,
363                prop,
364                "http://www.w3.org/2000/01/rdf-schema#domain".to_string(),
365                cls.to_string(),
366                AXIOM_KIND_DOMAIN,
367                ann_bag(ac),
368            );
369        }
370    }
371    for ac in idx.component_for_kind(ComponentKind::ObjectPropertyRange) {
372        let Component::ObjectPropertyRange(rng) = &ac.component else {
373            continue;
374        };
375        let prop = ope_to_iri(&rng.ope);
376        if let ClassExpression::Class(cls) = &rng.ce {
377            push_axiom(
378                &mut result.axioms,
379                &mut axiom_counter,
380                ontology_id,
381                &ont_id,
382                prop,
383                "http://www.w3.org/2000/01/rdf-schema#range".to_string(),
384                cls.to_string(),
385                AXIOM_KIND_RANGE,
386                ann_bag(ac),
387            );
388        }
389    }
390    for ac in idx.component_for_kind(ComponentKind::DataPropertyDomain) {
391        let Component::DataPropertyDomain(dom) = &ac.component else {
392            continue;
393        };
394        let prop = dom.dp.to_string();
395        if let ClassExpression::Class(cls) = &dom.ce {
396            push_axiom(
397                &mut result.axioms,
398                &mut axiom_counter,
399                ontology_id,
400                &ont_id,
401                prop,
402                "http://www.w3.org/2000/01/rdf-schema#domain".to_string(),
403                cls.to_string(),
404                AXIOM_KIND_DOMAIN,
405                ann_bag(ac),
406            );
407        }
408    }
409    for ac in idx.component_for_kind(ComponentKind::DataPropertyRange) {
410        let Component::DataPropertyRange(rng) = &ac.component else {
411            continue;
412        };
413        push_axiom(
414            &mut result.axioms,
415            &mut axiom_counter,
416            ontology_id,
417            &ont_id,
418            rng.dp.to_string(),
419            "http://www.w3.org/2000/01/rdf-schema#range".to_string(),
420            data_range_display(&rng.dr),
421            AXIOM_KIND_RANGE,
422            ann_bag(ac),
423        );
424    }
425
426    for ac in idx.component_for_kind(ComponentKind::SubObjectPropertyOf) {
427        let Component::SubObjectPropertyOf(sub_prop) = &ac.component else {
428            continue;
429        };
430        match &sub_prop.sub {
431            SubObjectPropertyExpression::ObjectPropertyChain(chain) => {
432                let chain_display = chain.iter().map(ope_to_iri).collect::<Vec<_>>().join(" o ");
433                push_axiom(
434                    &mut result.axioms,
435                    &mut axiom_counter,
436                    ontology_id,
437                    &ont_id,
438                    ope_to_iri(&sub_prop.sup),
439                    "http://www.w3.org/2002/07/owl#propertyChainAxiom".to_string(),
440                    chain_display,
441                    AXIOM_KIND_PROPERTY_CHAIN,
442                    ann_bag(ac),
443                );
444            }
445            SubObjectPropertyExpression::ObjectPropertyExpression(sub) => {
446                push_axiom(
447                    &mut result.axioms,
448                    &mut axiom_counter,
449                    ontology_id,
450                    &ont_id,
451                    ope_to_iri(sub),
452                    RDFS_SUB_PROPERTY_OF.to_string(),
453                    ope_to_iri(&sub_prop.sup),
454                    AXIOM_KIND_SUB_OBJECT_PROPERTY_OF,
455                    ann_bag(ac),
456                );
457            }
458        }
459    }
460
461    for ac in idx.component_for_kind(ComponentKind::SubDataPropertyOf) {
462        let Component::SubDataPropertyOf(sub_prop) = &ac.component else {
463            continue;
464        };
465        push_axiom(
466            &mut result.axioms,
467            &mut axiom_counter,
468            ontology_id,
469            &ont_id,
470            sub_prop.sub.to_string(),
471            RDFS_SUB_PROPERTY_OF.to_string(),
472            sub_prop.sup.to_string(),
473            AXIOM_KIND_SUB_DATA_PROPERTY_OF,
474            ann_bag(ac),
475        );
476    }
477
478    for ac in idx.component_for_kind(ComponentKind::ClassAssertion) {
479        let Component::ClassAssertion(ca) = &ac.component else {
480            continue;
481        };
482        if let ClassExpression::Class(cls) = &ca.ce {
483            push_axiom(
484                &mut result.axioms,
485                &mut axiom_counter,
486                ontology_id,
487                &ont_id,
488                individual_to_iri(&ca.i),
489                "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
490                cls.to_string(),
491                AXIOM_KIND_CLASS_ASSERTION,
492                ann_bag(ac),
493            );
494        }
495    }
496
497    for ac in idx.component_for_kind(ComponentKind::ObjectPropertyAssertion) {
498        let Component::ObjectPropertyAssertion(opa) = &ac.component else {
499            continue;
500        };
501        push_axiom(
502            &mut result.axioms,
503            &mut axiom_counter,
504            ontology_id,
505            &ont_id,
506            individual_to_iri(&opa.from),
507            ope_to_iri(&opa.ope),
508            individual_to_iri(&opa.to),
509            AXIOM_KIND_OBJECT_PROPERTY_ASSERTION,
510            ann_bag(ac),
511        );
512    }
513
514    for ac in idx.component_for_kind(ComponentKind::DataPropertyAssertion) {
515        let Component::DataPropertyAssertion(dpa) = &ac.component else {
516            continue;
517        };
518        push_axiom(
519            &mut result.axioms,
520            &mut axiom_counter,
521            ontology_id,
522            &ont_id,
523            individual_to_iri(&dpa.from),
524            dpa.dp.to_string(),
525            dpa.to.literal().clone(),
526            AXIOM_KIND_DATA_PROPERTY_ASSERTION,
527            ann_bag(ac),
528        );
529    }
530
531    for ac in idx.component_for_kind(ComponentKind::NegativeObjectPropertyAssertion) {
532        let Component::NegativeObjectPropertyAssertion(nopa) = &ac.component else {
533            continue;
534        };
535        push_axiom(
536            &mut result.axioms,
537            &mut axiom_counter,
538            ontology_id,
539            &ont_id,
540            individual_to_iri(&nopa.from),
541            ope_to_iri(&nopa.ope),
542            individual_to_iri(&nopa.to),
543            AXIOM_KIND_NEGATIVE_OBJECT_PROPERTY_ASSERTION,
544            ann_bag(ac),
545        );
546    }
547
548    for ac in idx.component_for_kind(ComponentKind::NegativeDataPropertyAssertion) {
549        let Component::NegativeDataPropertyAssertion(ndpa) = &ac.component else {
550            continue;
551        };
552        push_axiom(
553            &mut result.axioms,
554            &mut axiom_counter,
555            ontology_id,
556            &ont_id,
557            individual_to_iri(&ndpa.from),
558            ndpa.dp.to_string(),
559            ndpa.to.literal().clone(),
560            AXIOM_KIND_NEGATIVE_DATA_PROPERTY_ASSERTION,
561            ann_bag(ac),
562        );
563    }
564
565    for ac in idx.component_for_kind(ComponentKind::SameIndividual) {
566        let Component::SameIndividual(si) = &ac.component else {
567            continue;
568        };
569        let anns = ann_bag(ac);
570        let iris: Vec<String> = si.0.iter().map(individual_to_iri).collect();
571        for (i, a) in iris.iter().enumerate() {
572            for (j, b) in iris.iter().enumerate() {
573                if i == j {
574                    continue;
575                }
576                push_axiom(
577                    &mut result.axioms,
578                    &mut axiom_counter,
579                    ontology_id,
580                    &ont_id,
581                    a.clone(),
582                    OWL_SAME_AS.to_string(),
583                    b.clone(),
584                    AXIOM_KIND_SAME_INDIVIDUAL,
585                    anns.clone(),
586                );
587            }
588        }
589    }
590
591    for ac in idx.component_for_kind(ComponentKind::DifferentIndividuals) {
592        let Component::DifferentIndividuals(di) = &ac.component else {
593            continue;
594        };
595        let anns = ann_bag(ac);
596        let iris: Vec<String> = di.0.iter().map(individual_to_iri).collect();
597        for (i, a) in iris.iter().enumerate() {
598            for (j, b) in iris.iter().enumerate() {
599                if i >= j {
600                    continue;
601                }
602                push_axiom(
603                    &mut result.axioms,
604                    &mut axiom_counter,
605                    ontology_id,
606                    &ont_id,
607                    a.clone(),
608                    OWL_DIFFERENT_FROM.to_string(),
609                    b.clone(),
610                    AXIOM_KIND_DIFFERENT_INDIVIDUALS,
611                    anns.clone(),
612                );
613                push_axiom(
614                    &mut result.axioms,
615                    &mut axiom_counter,
616                    ontology_id,
617                    &ont_id,
618                    b.clone(),
619                    OWL_DIFFERENT_FROM.to_string(),
620                    a.clone(),
621                    AXIOM_KIND_DIFFERENT_INDIVIDUALS,
622                    anns.clone(),
623                );
624            }
625        }
626    }
627
628    for ac in idx.component_for_kind(ComponentKind::DatatypeDefinition) {
629        let Component::DatatypeDefinition(dd) = &ac.component else {
630            continue;
631        };
632        insert_entity(&mut entity_map, dd.kind.to_string(), EntityKind::Datatype, &ont_id);
633        push_axiom(
634            &mut result.axioms,
635            &mut axiom_counter,
636            ontology_id,
637            &ont_id,
638            dd.kind.to_string(),
639            OWL_EQUIVALENT_CLASS.to_string(),
640            data_range_display(&dd.range),
641            AXIOM_KIND_DATATYPE_DEFINITION,
642            ann_bag(ac),
643        );
644    }
645
646    mark_functional_properties(&mut entity_map, idx);
647
648    for imp in idx.import() {
649        result.imports.push(Import { ontology_id: ont_id.clone(), import_iri: imp.0.to_string() });
650    }
651
652    for (prefix, iri) in namespaces {
653        result.namespace_rows.push(Namespace {
654            prefix: prefix.clone(),
655            iri: iri.clone(),
656            ontology_id: ont_id.clone(),
657        });
658    }
659
660    result.entities = entity_map.into_values().collect();
661    for entity in &mut result.entities {
662        entity.source_location =
663            find_entity_block(source_text, &entity.iri, &entity.short_name, namespaces);
664    }
665    annotate_spans(source_text, &mut result.entities, &mut result.annotations, &mut result.axioms);
666    result
667}
668
669fn ann_bag(ac: &AnnotatedComponent<RcStr>) -> Vec<AxiomAnnotation> {
670    ac.ann
671        .iter()
672        .map(|a| AxiomAnnotation {
673            predicate: a.ap.to_string(),
674            value: annotation_value_string(&a.av),
675        })
676        .collect()
677}
678
679#[allow(clippy::too_many_arguments)]
680fn push_axiom(
681    axioms: &mut Vec<Axiom>,
682    counter: &mut usize,
683    doc_id: &str,
684    ontology_id: &str,
685    subject: String,
686    predicate: String,
687    object: String,
688    kind: &str,
689    annotations: Vec<AxiomAnnotation>,
690) {
691    *counter += 1;
692    axioms.push(Axiom {
693        id: format!("{doc_id}#axiom-{counter}"),
694        ontology_id: ontology_id.to_string(),
695        subject,
696        predicate,
697        object,
698        axiom_kind: kind.to_string(),
699        source_location: SourceLocation::default(),
700        annotations,
701    });
702}
703
704#[allow(clippy::too_many_arguments)]
705fn push_pairwise_property_axioms(
706    axioms: &mut Vec<Axiom>,
707    counter: &mut usize,
708    doc_id: &str,
709    ontology_id: &str,
710    iris: &[String],
711    predicate: &str,
712    kind: &str,
713    annotations: Vec<AxiomAnnotation>,
714) {
715    for (i, a) in iris.iter().enumerate() {
716        for (j, b) in iris.iter().enumerate() {
717            if i == j {
718                continue;
719            }
720            push_axiom(
721                axioms,
722                counter,
723                doc_id,
724                ontology_id,
725                a.clone(),
726                predicate.to_string(),
727                b.clone(),
728                kind,
729                annotations.clone(),
730            );
731        }
732    }
733}
734
735fn insert_entity(
736    map: &mut BTreeMap<String, Entity>,
737    iri: String,
738    kind: EntityKind,
739    ontology_id: &str,
740) {
741    let short_name = short_name_from_iri(&iri);
742    map.entry(iri.clone()).or_insert_with(|| Entity {
743        iri,
744        short_name,
745        kind,
746        ontology_id: ontology_id.to_string(),
747        source_location: SourceLocation::default(),
748        labels: Vec::new(),
749        comments: Vec::new(),
750        deprecated: false,
751        obo_id: None,
752        characteristics: strixonomy_core::PropertyCharacteristics::default(),
753    });
754}
755
756fn individual_to_iri(individual: &Individual<RcStr>) -> String {
757    individual.to_string()
758}
759
760fn mark_functional_properties(
761    entity_map: &mut BTreeMap<String, Entity>,
762    idx: &ComponentMappedIndex<RcStr, RcAnnotatedComponent>,
763) {
764    for f in idx.functional_object_property() {
765        let iri = ope_to_iri(&f.0);
766        if let Some(e) = entity_map.get_mut(&iri) {
767            e.characteristics.functional = true;
768        }
769    }
770    for f in idx.inverse_functional_object_property() {
771        let iri = ope_to_iri(&f.0);
772        if let Some(e) = entity_map.get_mut(&iri) {
773            e.characteristics.inverse_functional = true;
774        }
775    }
776    for f in idx.transitive_object_property() {
777        let iri = ope_to_iri(&f.0);
778        if let Some(e) = entity_map.get_mut(&iri) {
779            e.characteristics.transitive = true;
780        }
781    }
782    for f in idx.symmetric_object_property() {
783        let iri = ope_to_iri(&f.0);
784        if let Some(e) = entity_map.get_mut(&iri) {
785            e.characteristics.symmetric = true;
786        }
787    }
788    for f in idx.asymmetric_object_property() {
789        let iri = ope_to_iri(&f.0);
790        if let Some(e) = entity_map.get_mut(&iri) {
791            e.characteristics.asymmetric = true;
792        }
793    }
794    for f in idx.reflexive_object_property() {
795        let iri = ope_to_iri(&f.0);
796        if let Some(e) = entity_map.get_mut(&iri) {
797            e.characteristics.reflexive = true;
798        }
799    }
800    for f in idx.irreflexive_object_property() {
801        let iri = ope_to_iri(&f.0);
802        if let Some(e) = entity_map.get_mut(&iri) {
803            e.characteristics.irreflexive = true;
804        }
805    }
806    for f in idx.functional_data_property() {
807        let iri = f.0.to_string();
808        if let Some(e) = entity_map.get_mut(&iri) {
809            e.characteristics.functional = true;
810        }
811    }
812}
813
814fn class_expr_display(
815    expr: &ClassExpression<RcStr>,
816    namespaces: &BTreeMap<String, String>,
817) -> Option<String> {
818    match expr {
819        ClassExpression::Class(c) => Some(c.to_string()),
820        _ => Some(class_expression_to_manchester(expr, namespaces)),
821    }
822}
823
824fn ope_to_iri(ope: &ObjectPropertyExpression<RcStr>) -> String {
825    match ope {
826        ObjectPropertyExpression::ObjectProperty(p) => p.to_string(),
827        ObjectPropertyExpression::InverseObjectProperty(p) => {
828            format!("inverse({})", p.0)
829        }
830    }
831}
832
833fn property_expr_to_iri(pe: &PropertyExpression<RcStr>) -> String {
834    match pe {
835        PropertyExpression::ObjectPropertyExpression(ope) => ope_to_iri(ope),
836        PropertyExpression::DataProperty(dp) => dp.to_string(),
837        PropertyExpression::AnnotationProperty(ap) => ap.to_string(),
838    }
839}
840
841fn data_range_display(dr: &horned_owl::model::DataRange<RcStr>) -> String {
842    crate::manchester::data_range_to_manchester(dr, &BTreeMap::new())
843}
844
845fn annotation_subject_iri(subject: &AnnotationSubject<RcStr>) -> String {
846    match subject {
847        AnnotationSubject::IRI(iri) => iri.to_string(),
848        AnnotationSubject::AnonymousIndividual(a) => format!("_:{}", a.as_ref()),
849    }
850}
851
852fn annotation_value_string(value: &AnnotationValue<RcStr>) -> String {
853    match value {
854        AnnotationValue::Literal(l) => l.literal().clone(),
855        AnnotationValue::IRI(iri) => iri.to_string(),
856        AnnotationValue::AnonymousIndividual(a) => format!("_:{}", a.as_ref()),
857    }
858}
859
860#[cfg(test)]
861mod tests {
862    use super::*;
863    use crate::load::load_turtle_text;
864    use oxigraph::io::RdfParser;
865    use oxigraph::model::Quad;
866    use std::path::Path;
867
868    #[test]
869    fn bridge_extracts_subclass_axioms() {
870        let ttl = include_str!("../../../fixtures/example.ttl");
871        let parser = RdfParser::from_format(oxigraph::io::RdfFormat::Turtle);
872        let quads: Vec<Quad> =
873            parser.for_reader(ttl.as_bytes()).collect::<std::result::Result<Vec<_>, _>>().unwrap();
874        let namespaces =
875            BTreeMap::from([("ex".to_string(), "http://example.org/people#".to_string())]);
876        let loaded = load_turtle_text(Path::new("example.ttl"), "doc-1", ttl, &quads, &namespaces)
877            .expect("load");
878        assert!(loaded.bridge.axioms.iter().any(|a| a.axiom_kind == AXIOM_KIND_SUB_CLASS_OF));
879    }
880
881    #[test]
882    fn bridge_extracts_disjoint_and_property_chain() {
883        let ttl = include_str!("../../../fixtures/disjoint-classes.ttl");
884        let parser = RdfParser::from_format(oxigraph::io::RdfFormat::Turtle);
885        let quads: Vec<Quad> =
886            parser.for_reader(ttl.as_bytes()).collect::<std::result::Result<Vec<_>, _>>().unwrap();
887        let namespaces =
888            BTreeMap::from([("ex".to_string(), "http://example.org/org#".to_string())]);
889        let loaded =
890            load_turtle_text(Path::new("disjoint-classes.ttl"), "doc-1", ttl, &quads, &namespaces)
891                .expect("load");
892        assert!(loaded.bridge.axioms.iter().any(|a| a.axiom_kind == AXIOM_KIND_DISJOINT_CLASS));
893        assert!(loaded.bridge.axioms.iter().any(|a| a.axiom_kind == AXIOM_KIND_PROPERTY_CHAIN));
894    }
895
896    #[test]
897    fn bridge_extracts_owl2_keys_and_inverse() {
898        let ttl = include_str!("../../../examples/protege-roundtrip/owl2-keys.ttl");
899        let parser = RdfParser::from_format(oxigraph::io::RdfFormat::Turtle);
900        let quads: Vec<Quad> =
901            parser.for_reader(ttl.as_bytes()).collect::<std::result::Result<Vec<_>, _>>().unwrap();
902        let namespaces =
903            BTreeMap::from([("ex".to_string(), "http://example.org/keys#".to_string())]);
904        let loaded =
905            load_turtle_text(Path::new("owl2-keys.ttl"), "doc-keys", ttl, &quads, &namespaces)
906                .expect("load");
907        assert!(loaded
908            .bridge
909            .axioms
910            .iter()
911            .any(|a| a.axiom_kind == AXIOM_KIND_HAS_KEY
912                && a.subject == "http://example.org/keys#Person"));
913        assert!(loaded.bridge.axioms.iter().any(|a| a.axiom_kind == AXIOM_KIND_DISJOINT_UNION
914            && a.subject == "http://example.org/keys#Sex"));
915        assert!(loaded.bridge.axioms.iter().any(|a| {
916            a.axiom_kind == AXIOM_KIND_INVERSE_OBJECT_PROPERTIES
917                && a.subject == "http://example.org/keys#hasParent"
918                && a.object == "http://example.org/keys#hasChild"
919        }));
920    }
921
922    #[test]
923    fn bridge_extracts_owl2_abox() {
924        let ttl = include_str!("../../../examples/protege-roundtrip/owl2-abox.ttl");
925        let parser = RdfParser::from_format(oxigraph::io::RdfFormat::Turtle);
926        let quads: Vec<Quad> =
927            parser.for_reader(ttl.as_bytes()).collect::<std::result::Result<Vec<_>, _>>().unwrap();
928        let namespaces =
929            BTreeMap::from([("ex".to_string(), "http://example.org/abox#".to_string())]);
930        let loaded =
931            load_turtle_text(Path::new("owl2-abox.ttl"), "doc-abox", ttl, &quads, &namespaces)
932                .expect("load");
933        assert!(loaded.bridge.axioms.iter().any(|a| {
934            a.axiom_kind == AXIOM_KIND_SAME_INDIVIDUAL
935                && a.subject == "http://example.org/abox#alice"
936        }));
937        assert!(loaded.bridge.axioms.iter().any(|a| {
938            a.axiom_kind == AXIOM_KIND_DIFFERENT_INDIVIDUALS
939                && a.subject == "http://example.org/abox#alice"
940        }));
941        assert!(loaded.bridge.axioms.iter().any(|a| {
942            a.axiom_kind == AXIOM_KIND_NEGATIVE_OBJECT_PROPERTY_ASSERTION
943                && a.subject == "http://example.org/abox#alice"
944                && a.object == "http://example.org/abox#bob"
945        }));
946    }
947}