diff --git a/ontology/uco/observable/observable.ttl b/ontology/uco/observable/observable.ttl index 9d141ecd..7ed93d93 100644 --- a/ontology/uco/observable/observable.ttl +++ b/ontology/uco/observable/observable.ttl @@ -6,6 +6,7 @@ # imports: https://ontology.unifiedcyberontology.org/uco/vocabulary @prefix action: . +@prefix co: . @prefix core: . @prefix identity: . @prefix location: . @@ -3863,9 +3864,20 @@ observable:MessageThread owl:Class , sh:NodeShape ; - rdfs:subClassOf observable:ObservableObject ; + rdfs:subClassOf + observable:ObservableObject , + types:Thread + ; rdfs:label "MessageTread"@en ; rdfs:comment "A message thread is a running commentary of electronic messages pertaining to one topic or question."@en ; + sh:property [ + sh:class observable:Message ; + sh:description "A MessageThread's items' contents must be Message objects."@en ; + sh:path ( + co:item + co:itemContent + ) ; + ] ; sh:targetClass observable:MessageThread ; . diff --git a/ontology/uco/types/types.ttl b/ontology/uco/types/types.ttl index ce5877b7..aa9574f6 100644 --- a/ontology/uco/types/types.ttl +++ b/ontology/uco/types/types.ttl @@ -1,6 +1,9 @@ +# imports: https://ontology.unifiedcyberontology.org/co # imports: https://ontology.unifiedcyberontology.org/uco/core # imports: https://ontology.unifiedcyberontology.org/uco/vocabulary +@prefix co: . +@prefix core: . @prefix owl: . @prefix rdf: . @prefix rdfs: . @@ -13,6 +16,7 @@ a owl:Ontology ; rdfs:label "uco-types"@en ; owl:imports + , , ; @@ -169,6 +173,42 @@ types:StructuredText rdfs:comment "Expresses string-based data in some information structuring format (e.g., HTML5)."@en ; . +types:Thread + a + owl:Class , + sh:NodeShape + ; + rdfs:subClassOf + co:Bag , + core:UcoObject + ; + rdfs:label "Thread"@en ; + rdfs:comment "A semi-ordered array of items, that can be present in multiple copies. Implemetation of a UCO Thread is similar to a Collections Ontology List, except a Thread may fork and merge - that is, one of its members may have two or more direct successors, and two or more direct predecessors."@en ; + owl:disjointWith co:List ; + sh:property [ + sh:class types:ThreadItem ; + sh:path co:item ; + ] ; + . + +types:ThreadItem + a + owl:Class , + sh:NodeShape + ; + rdfs:subClassOf + co:Item , + core:UcoObject + ; + rdfs:label "ThreadItem"@en ; + rdfs:comment "A ThreadItem is a member of a thread."@en ; + owl:disjointWith co:ListItem ; + sh:property [ + sh:class core:UcoObject ; + sh:path co:itemContent ; + ] ; + . + types:entry a owl:ObjectProperty ; rdfs:label "entry"@en ; @@ -209,6 +249,178 @@ types:key rdfs:range xsd:string ; . +types:threadNextItem + a owl:ObjectProperty ; + rdfs:subPropertyOf types:threadSuccessor ; + rdfs:label "threadNextItem"@en ; + rdfs:comment "The link to a next item in a thread."@en ; + rdfs:seeAlso co:nextItem ; + . + +types:threadNextItem-subjects-shape + a sh:PropertyShape ; + sh:class types:ThreadItem ; + sh:nodeKind sh:BlankNodeOrIRI ; + sh:path types:threadNextItem ; + sh:targetSubjectsOf types:threadNextItem ; + . + +types:threadOriginItem + a owl:ObjectProperty ; + rdfs:subPropertyOf co:item ; + rdfs:label "threadOriginItem"@en ; + rdfs:comment "A link to an item of the thread known to have no predecessor."@en ; + rdfs:domain types:Thread ; + rdfs:range [ + a owl:Class ; + owl:intersectionOf ( + types:ThreadItem + [ + a owl:Restriction ; + owl:onProperty types:threadPreviousItem ; + owl:cardinality "0"^^xsd:nonNegativeInteger ; + ] + ) ; + ] ; + rdfs:seeAlso co:firstItem ; + . + +types:threadOriginItem-subjects-shape + a sh:PropertyShape ; + sh:class types:ThreadItem ; + sh:path types:threadOriginItem ; + sh:targetSubjectsOf types:threadOriginItem ; + . + +types:threadOriginItem-subjects-threadPredecessor-shape + a sh:PropertyShape ; + sh:description "An origin item in a thread must not have a predecessor."@en ; + sh:maxCount "0"^^xsd:integer ; + sh:path ( + types:threadOriginItem + types:threadPredecessor + ) ; + sh:targetSubjectsOf types:threadOriginItem ; + . + +types:threadOriginItem-subjects-threadPreviousItem-shape + a sh:PropertyShape ; + sh:description "An origin item in a thread must not have a previous item."@en ; + sh:maxCount "0"^^xsd:integer ; + sh:path ( + types:threadOriginItem + types:threadPreviousItem + ) ; + sh:targetSubjectsOf types:threadOriginItem ; + . + +types:threadPredecessor + a + owl:ObjectProperty , + owl:TransitiveProperty + ; + rdfs:label "threadPredecessor"@en ; + rdfs:comment "The link to the preceding item in a thread."@en ; + rdfs:domain types:ThreadItem ; + rdfs:range types:ThreadItem ; + rdfs:seeAlso co:precededBy ; + owl:inverseOf types:threadSuccessor ; + . + +types:threadPredecessor-subjects-shape + a sh:PropertyShape ; + sh:class types:ThreadItem ; + sh:nodeKind sh:BlankNodeOrIRI ; + sh:path types:threadPredecessor ; + sh:targetSubjectsOf types:threadPredecessor ; + . + +types:threadPreviousItem + a owl:ObjectProperty ; + rdfs:subPropertyOf types:threadPredecessor ; + rdfs:label "threadPreviousItem"@en ; + rdfs:comment "A direct link to a previous item in a thread."@en ; + rdfs:seeAlso co:previousItem ; + owl:inverseOf types:threadNextItem ; + . + +types:threadPreviousItem-subjects-shape + a sh:PropertyShape ; + sh:class types:ThreadItem ; + sh:nodeKind sh:BlankNodeOrIRI ; + sh:path types:threadPreviousItem ; + sh:targetSubjectsOf types:threadPreviousItem ; + . + +types:threadSuccessor + a + owl:ObjectProperty , + owl:TransitiveProperty + ; + rdfs:label "threadSuccessor"@en ; + rdfs:comment "A link to a following item in a thread."@en ; + rdfs:domain types:ThreadItem ; + rdfs:range types:ThreadItem ; + rdfs:seeAlso co:followedBy ; + . + +types:threadSuccessor-subjects-shape + a sh:PropertyShape ; + sh:class types:ThreadItem ; + sh:nodeKind sh:BlankNodeOrIRI ; + sh:path types:threadSuccessor ; + sh:targetSubjectsOf types:threadSuccessor ; + . + +types:threadTerminalItem + a owl:ObjectProperty ; + rdfs:subPropertyOf co:item ; + rdfs:label "threadTerminalItem"@en ; + rdfs:comment "A link to an item of the thread known to have no successor."@en ; + rdfs:domain types:Thread ; + rdfs:range [ + a owl:Class ; + owl:intersectionOf ( + types:ThreadItem + [ + a owl:Restriction ; + owl:onProperty types:threadNextItem ; + owl:cardinality "0"^^xsd:nonNegativeInteger ; + ] + ) ; + ] ; + rdfs:seeAlso co:lastItem ; + . + +types:threadTerminalItem-subjects-shape + a sh:PropertyShape ; + sh:class types:ThreadItem ; + sh:path types:threadTerminalItem ; + sh:targetSubjectsOf types:threadTerminalItem ; + . + +types:threadTerminalItem-subjects-threadNextItem-shape + a sh:PropertyShape ; + sh:description "A terminal item in a thread must not have a next item."@en ; + sh:maxCount "0"^^xsd:integer ; + sh:path ( + types:threadTerminalItem + types:threadNextItem + ) ; + sh:targetSubjectsOf types:threadTerminalItem ; + . + +types:threadTerminalItem-subjects-threadSuccessor-shape + a sh:PropertyShape ; + sh:description "A terminal item in a thread must not have a successor."@en ; + sh:maxCount "0"^^xsd:integer ; + sh:path ( + types:threadTerminalItem + types:threadSuccessor + ) ; + sh:targetSubjectsOf types:threadTerminalItem ; + . + types:value a owl:DatatypeProperty ; rdfs:label "value"@en ; diff --git a/tests/examples/Makefile b/tests/examples/Makefile index 260e1e18..8db1bb8f 100644 --- a/tests/examples/Makefile +++ b/tests/examples/Makefile @@ -27,8 +27,11 @@ all: \ hash_XFAIL_validation.ttl \ location_PASS_validation.ttl \ location_XFAIL_validation.ttl \ + message_thread_PASS_validation.ttl \ relationship_PASS_validation.ttl \ - relationship_XFAIL_validation.ttl + relationship_XFAIL_validation.ttl \ + thread_PASS_validation.ttl \ + thread_XFAIL_validation.ttl .PRECIOUS: \ %_validation.ttl @@ -78,8 +81,11 @@ check: \ hash_XFAIL_validation.ttl \ location_PASS_validation.ttl \ location_XFAIL_validation.ttl \ + message_thread_PASS_validation.ttl \ relationship_PASS_validation.ttl \ - relationship_XFAIL_validation.ttl + relationship_XFAIL_validation.ttl \ + thread_PASS_validation.ttl \ + thread_XFAIL_validation.ttl source $(tests_srcdir)/venv/bin/activate \ && pytest \ --log-level=DEBUG diff --git a/tests/examples/message_thread_PASS.json b/tests/examples/message_thread_PASS.json new file mode 100644 index 00000000..947acb4e --- /dev/null +++ b/tests/examples/message_thread_PASS.json @@ -0,0 +1,165 @@ +{ + "@context": { + "co": "http://purl.org/co/", + "kb": "http://example.org/kb/", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "observable": "https://ontology.unifiedcyberontology.org/uco/observable/", + "types": "https://ontology.unifiedcyberontology.org/uco/types/", + "xsd": "http://www.w3.org/2001/XMLSchema#" + }, + "@graph": [ + { + "@id": "kb:message-1", + "@type": "observable:Message" + }, + { + "@id": "kb:message-2", + "@type": "observable:Message" + }, + { + "@id": "kb:message-3", + "@type": "observable:Message" + }, + { + "@id": "kb:message-4", + "@type": "observable:Message" + }, + { + "@id": "kb:message-5", + "@type": "observable:Message" + }, + { + "@id": "kb:message-6", + "@type": "observable:Message" + }, + { + "@id": "kb:message-7", + "@type": "observable:Message" + }, + { + "@id": "kb:message-thread-1", + "@type": "observable:MessageThread", + "co:size": { + "@type": "xsd:nonNegativeInteger", + "@value": "6" + }, + "co:item": [ + { + "@id": "kb:message-thread-1-item-1" + }, + { + "@id": "kb:message-thread-1-item-2" + }, + { + "@id": "kb:message-thread-1-item-3" + }, + { + "@id": "kb:message-thread-1-item-4" + }, + { + "@id": "kb:message-thread-1-item-5" + }, + { + "@id": "kb:message-thread-1-item-6" + } + ], + "types:threadOriginItem": [ + { + "@id": "kb:message-thread-1-item-1" + }, + { + "@id": "kb:message-thread-1-item-5" + } + ], + "types:threadTerminalItem": [ + { + "@id": "kb:message-thread-1-item-3" + }, + { + "@id": "kb:message-thread-1-item-4" + }, + { + "@id": "kb:message-thread-1-item-6" + } + ] + }, + { + "@id": "kb:message-thread-1-item-1", + "@type": "types:ThreadItem", + "co:itemContent": { + "@id": "kb:message-1" + }, + "types:threadNextItem": [ + { + "@id": "kb:message-thread-1-item-2" + }, + { + "@id": "kb:message-thread-1-item-6" + } + ] + }, + { + "@id": "kb:message-thread-1-item-2", + "@type": "types:ThreadItem", + "co:itemContent": { + "@id": "kb:message-2" + }, + "types:threadNextItem": [ + { + "@id": "kb:message-thread-1-item-3" + }, + { + "@id": "kb:message-thread-1-item-4" + } + ], + "types:threadPreviousItem": { + "@id": "kb:message-thread-1-item-1" + } + }, + { + "@id": "kb:message-thread-1-item-3", + "@type": "types:ThreadItem", + "co:itemContent": { + "@id": "kb:message-3" + }, + "types:threadPreviousItem": { + "@id": "kb:message-thread-1-item-2" + } + }, + { + "@id": "kb:message-thread-1-item-4", + "@type": "types:ThreadItem", + "co:itemContent": { + "@id": "kb:message-4" + }, + "types:threadPreviousItem": { + "@id": "kb:message-thread-1-item-2" + } + }, + { + "@id": "kb:message-thread-1-item-5", + "@type": "types:ThreadItem", + "co:itemContent": { + "@id": "kb:message-5" + }, + "types:threadNextItem": { + "@id": "kb:message-thread-1-item-6" + } + }, + { + "@id": "kb:message-thread-1-item-6", + "@type": "types:ThreadItem", + "co:itemContent": { + "@id": "kb:message-6" + }, + "types:threadPreviousItem": [ + { + "@id": "kb:message-thread-1-item-1" + }, + { + "@id": "kb:message-thread-1-item-5" + } + ] + } + ] +} diff --git a/tests/examples/test_validation.py b/tests/examples/test_validation.py index 44b30335..b48cc765 100644 --- a/tests/examples/test_validation.py +++ b/tests/examples/test_validation.py @@ -21,6 +21,7 @@ the only functions to be called to be named "test_*". """ +import pathlib import logging import typing @@ -33,9 +34,17 @@ NS_UCO_CO = rdflib.Namespace("https://ontology.unifiedcyberontology.org/co/") NS_UCO_CORE = rdflib.Namespace("https://ontology.unifiedcyberontology.org/uco/core/") NS_UCO_LOCATION = rdflib.Namespace("https://ontology.unifiedcyberontology.org/uco/location/") +NS_UCO_TYPES = rdflib.Namespace("https://ontology.unifiedcyberontology.org/uco/types/") NSDICT = {"sh": NS_SH} +@pytest.fixture(scope="session") +def monolithic_ontology_graph() -> rdflib.Graph: + graph = rdflib.Graph() + monolithic_ttl_path = pathlib.Path(__file__).parent.parent / "uco_monolithic.ttl" + graph.parse(str(monolithic_ttl_path), format="turtle") + return graph + def load_validation_graph( filename : str, expected_conformance : bool @@ -250,6 +259,64 @@ def test_location_XFAIL_validation_XPASS_wrong_concept_name(): } ) +def test_message_thread(monolithic_ontology_graph: rdflib.Graph) -> None: + r""" + Confirm the answer to this question: + What are all of the messages that followed the first in the thread kb:message-thread-1? + + message-thread-1 forked, and has these reply paths: + + 1 2 3 + * --- * --- * + \ \ + \ \ 4 + \ * + 5 \ 6 + * --- * + + 7 + * + + (Message 7 is outside the thread.) + """ + + expected: typing.Set[str] = { + "http://example.org/kb/message-2", + "http://example.org/kb/message-3", + "http://example.org/kb/message-4", + "http://example.org/kb/message-6", + } + computed: typing.Set[str] = set() + + data_graph = rdflib.Graph() + data_filepath = pathlib.Path(__file__).parent / "message_thread_PASS.json" + data_graph.parse(str(data_filepath), format="json-ld") + + analysis_graph = data_graph + monolithic_ontology_graph + + query_str = """\ +PREFIX co: +PREFIX kb: +PREFIX rdfs: +PREFIX types: + +SELECT ?nLaterMessage +WHERE { + ?nFirstMessageItem + co:itemContent kb:message-1 ; + (types:threadNextItem|types:threadSuccessor)+ / co:itemContent ?nLaterMessage ; + . +} +""" + + for result in analysis_graph.query(query_str): + computed.add(str(result[0])) + + assert expected == computed + +def test_message_thread_PASS_validation(): + confirm_validation_results("message_thread_PASS_validation.ttl", True) + def test_relationship_PASS_partial() -> None: """ This test should be replaced with test_relationship_XFAIL_full when the semi-open vocabulary design current as of UCO 0.8.0 is re-done. @@ -318,3 +385,17 @@ def test_relationship_XFAIL_full() -> None: ("http://example.org/kb/relationship-2-3-2", str(NS_SH.Violation)), } ) + +def test_thread_PASS_validation(): + confirm_validation_results("thread_PASS_validation.ttl", True) + +def test_thread_XFAIL_validation(): + confirm_validation_results( + "thread_XFAIL_validation.ttl", + False, + expected_result_paths={ + str(NS_CO.item), + str(NS_CO.itemContent), + str(NS_UCO_TYPES.threadOriginItem), + } + ) diff --git a/tests/examples/thread_PASS.json b/tests/examples/thread_PASS.json new file mode 100644 index 00000000..a13a296c --- /dev/null +++ b/tests/examples/thread_PASS.json @@ -0,0 +1,52 @@ +{ + "@context": { + "co": "http://purl.org/co/", + "kb": "http://example.org/kb/", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "types": "https://ontology.unifiedcyberontology.org/uco/types/", + "xsd": "http://www.w3.org/2001/XMLSchema#" + }, + "@graph": [ + { + "@id": "kb:thread-1", + "@type": "types:Thread", + "co:item": [ + { + "@id": "kb:thread-1-item-1" + }, + { + "@id": "kb:thread-1-item-2" + }, + { + "@id": "kb:thread-1-item-3" + } + ] + }, + { + "@id": "kb:thread-1-item-1", + "@type": "types:ThreadItem", + "types:threadNextItem": [ + { + "@id": "kb:thread-1-item-2" + }, + { + "@id": "kb:thread-1-item-3" + } + ] + }, + { + "@id": "kb:thread-1-item-2", + "@type": "types:ThreadItem", + "types:threadPreviousItem": { + "@id": "kb:thread-1-item-1" + } + }, + { + "@id": "kb:thread-1-item-3", + "@type": "types:ThreadItem", + "types:threadPreviousItem": { + "@id": "kb:thread-1-item-1" + } + } + ] +} diff --git a/tests/examples/thread_XFAIL.json b/tests/examples/thread_XFAIL.json new file mode 100644 index 00000000..7a844ad3 --- /dev/null +++ b/tests/examples/thread_XFAIL.json @@ -0,0 +1,113 @@ +{ + "@context": { + "co": "http://purl.org/co/", + "core": "https://ontology.unifiedcyberontology.org/uco/core/", + "kb": "http://example.org/kb/", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "types": "https://ontology.unifiedcyberontology.org/uco/types/", + "xsd": "http://www.w3.org/2001/XMLSchema#" + }, + "@graph": [ + { + "@id": "kb:uco-object-4", + "@type": "core:UcoObject" + }, + { + "@id": "kb:uco-object-5", + "@type": "core:UcoObject" + }, + { + "@id": "kb:thread-2", + "@type": "types:Thread", + "types:threadOriginItem": { + "@id": "kb:uco-object-4" + }, + "co:item": { + "@id": "kb:uco-object-4" + }, + "rdfs:comment": [ + "Error - types:threadOriginItem is not a types:ThreadItem.", + "Error - co:item is not a co:Item." + ] + }, + { + "@id": "kb:thread-3", + "@type": "types:Thread", + "co:item": [ + { + "@id": "kb:thread-3-item-1" + }, + { + "@id": "kb:thread-3-item-2" + } + ] + }, + { + "@id": "kb:thread-3-item-1", + "@type": "types:ThreadItem", + "co:itemContent": { + "@id": "kb:thread-3-item-2" + }, + "rdfs:comment": [ + "Error - co:itemContent must not be a co:Item (and types:ThreadItem is a subclass of co:Item)." + ] + }, + { + "@id": "kb:thread-3-item-2", + "@type": "types:ThreadItem", + "co:itemContent": [ + { + "@id": "kb:uco-object-4" + }, + { + "@id": "kb:uco-object-5" + } + ], + "rdfs:comment": [ + "Error - 2 values of co:itemContent." + ] + }, + { + "@id": "kb:thread-4", + "@type": "types:Thread", + "co:item": [ + { + "@id": "kb:thread-4-item-1" + }, + { + "@id": "kb:thread-4-item-2" + }, + { + "@id": "kb:thread-4-item-3" + } + ], + "rdfs:comment": "Error - list forks in opposite direction of what types:Thread supports, with item 3 directly preceeded by 1 and 2." + }, + { + "@id": "kb:thread-4-item-1", + "@type": "types:ThreadItem", + "types:threadNextItem": { + "@id": "kb:thread-4-item-3" + } + }, + { + "@id": "kb:thread-4-item-2", + "@type": "types:ThreadItem", + "types:threadNextItem": { + "@id": "kb:thread-4-item-3" + } + }, + { + "@id": "kb:thread-4-item-3", + "@type": "types:ThreadItem", + "types:threadPreviousItem": [ + { + "@id": "kb:thread-4-item-1" + }, + { + "@id": "kb:thread-4-item-2" + } + ] + } + ] +}