Skip to content

Commit

Permalink
fix(xmlvalidate): duplicate cardinalities of LinkValues (DEV-4203) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Nora-Olivia-Ammann authored Oct 4, 2024
1 parent 6b386e9 commit a8a67ab
Show file tree
Hide file tree
Showing 12 changed files with 464 additions and 291 deletions.
5 changes: 5 additions & 0 deletions src/dsp_tools/commands/xml_validate/api_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def _get(self, url: str, headers: dict[str, Any] | None = None) -> Response:
raise UserError(msg)
return response

def get_knora_api(self) -> str:
url = f"{self.api_url}/ontology/knora-api/v2#"
onto = self._get(url, headers={"Accept": "text/turtle"})
return onto.text

def get_ontologies(self) -> list[str]:
"""
Returns a list of project ontologies as a string in turtle format.
Expand Down
3 changes: 2 additions & 1 deletion src/dsp_tools/commands/xml_validate/deserialise_input.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Callable
from typing import Sequence
from typing import cast

from lxml import etree

Expand Down Expand Up @@ -50,7 +51,7 @@ def _deserialise_all_resources(root: etree._Element) -> DataDeserialised:
all_res: list[AbstractResource] = []
for res in root.iterchildren():
res_id = res.attrib["id"]
lbl = res.attrib["label"]
lbl = cast(str, res.attrib.get("label"))
match res.tag:
case "resource":
all_res.append(_deserialise_one_resource(res))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ def _reformat_onto_iri(prop: str) -> str:


def _reformat_data_iri(iri: str) -> str:
return iri.lstrip("http://data/")
return iri.replace("http://data/", "")
11 changes: 10 additions & 1 deletion src/dsp_tools/commands/xml_validate/sparql/resource_shacl.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ def _construct_resource_nodeshape(onto_graph: Graph) -> Graph:
} WHERE {
?class a owl:Class ;
knora-api:isResourceClass true .
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true .
BIND(IRI(CONCAT(str(?class), "_Shape")) AS ?shapesIRI)
}
Expand Down Expand Up @@ -79,11 +80,13 @@ def _construct_1_cardinality(onto_graph: Graph) -> Graph:
?class a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf ?restriction .
?restriction a owl:Restriction ;
owl:onProperty ?propRestriction ;
salsah-gui:guiOrder ?order ;
owl:cardinality 1 .
FILTER NOT EXISTS { ?propRestriction knora-api:isLinkValueProperty true }
BIND(IRI(CONCAT(str(?class), "_Shape")) AS ?shapesIRI)
}
Expand Down Expand Up @@ -117,11 +120,13 @@ def _construct_0_1_cardinality(onto_graph: Graph) -> Graph:
?class a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf ?restriction .
?restriction a owl:Restriction ;
owl:onProperty ?propRestriction ;
salsah-gui:guiOrder ?order ;
owl:maxCardinality 1 .
FILTER NOT EXISTS { ?propRestriction knora-api:isLinkValueProperty true }
BIND(IRI(CONCAT(str(?class), "_Shape")) AS ?shapesIRI)
}
Expand Down Expand Up @@ -154,11 +159,13 @@ def _construct_1_n_cardinality(onto_graph: Graph) -> Graph:
?class a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf ?restriction .
?restriction a owl:Restriction ;
owl:onProperty ?propRestriction ;
salsah-gui:guiOrder ?order ;
owl:minCardinality 1 .
FILTER NOT EXISTS { ?propRestriction knora-api:isLinkValueProperty true }
BIND(IRI(CONCAT(str(?class), "_Shape")) AS ?shapesIRI)
}
Expand Down Expand Up @@ -188,11 +195,13 @@ def _construct_0_n_cardinality(onto_graph: Graph) -> Graph:
?class a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf ?restriction .
?restriction a owl:Restriction ;
owl:onProperty ?propRestriction ;
salsah-gui:guiOrder ?order ;
owl:minCardinality 0 .
FILTER NOT EXISTS { ?propRestriction knora-api:isLinkValueProperty true }
BIND(IRI(CONCAT(str(?class), "_Shape")) AS ?shapesIRI)
}
Expand Down
32 changes: 22 additions & 10 deletions src/dsp_tools/commands/xml_validate/xml_validate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import warnings
from copy import deepcopy
from pathlib import Path

from lxml import etree
Expand Down Expand Up @@ -39,10 +40,11 @@ def xml_validate(filepath: Path, api_url: str, dev_route: bool) -> bool: # noqa
_inform_about_experimental_feature()
data_rdf, shortcode = _get_data_info_from_file(filepath, api_url)
onto_con = OntologyConnection(api_url, shortcode)
ontologies = _get_project_ontos(onto_con)
data_graph = data_rdf.make_graph() + ontologies
ontologies, shapes = _get_shacl(onto_con)
data_graph = data_rdf.make_graph()
# data_graph += ontologies
val = ShaclValidator(api_url)
report = _validate(val, ontologies, data_graph)
report = _validate(val, shapes, data_graph)
if report.conforms:
cprint("\n Validation passed! ", color="green", attrs=["bold", "reverse"])
else:
Expand All @@ -68,28 +70,38 @@ def _inform_about_experimental_feature() -> None:
warnings.warn(DspToolsUserWarning(LIST_SEPARATOR.join(what_is_validated)))


def _validate(validator: ShaclValidator, onto_graph: Graph, data_graph: Graph) -> ValidationReport:
shapes = construct_shapes_graph(onto_graph)
shape_str = shapes.serialize(format="ttl")
def _validate(validator: ShaclValidator, shapes_graph: Graph, data_graph: Graph) -> ValidationReport:
shape_str = shapes_graph.serialize(format="ttl")
data_str = data_graph.serialize(format="ttl")
results = validator.validate(data_str, shape_str)
conforms = bool(next(results.objects(None, SH.conforms)))
return ValidationReport(
conforms=conforms,
validation_graph=results,
shacl_graph=shapes,
shacl_graph=shapes_graph,
data_graph=data_graph,
)


def _get_shacl(onto_con: OntologyConnection) -> tuple[Graph, Graph]:
ontologies = _get_project_ontos(onto_con)
knora_ttl = onto_con.get_knora_api()
kag = Graph()
kag.parse(data=knora_ttl, format="ttl")
onto_for_construction = deepcopy(ontologies) + kag
shapes = construct_shapes_graph(onto_for_construction)
shapes += ontologies
return ontologies, shapes


def _get_project_ontos(onto_con: OntologyConnection) -> Graph:
all_ontos = onto_con.get_ontologies()
g = Graph()
onto_g = Graph()
for onto in all_ontos:
og = Graph()
og.parse(data=onto, format="ttl")
g += og
return g
onto_g += og
return onto_g


def _get_data_info_from_file(file: Path, api_url: str) -> tuple[DataRDF, str]:
Expand Down
1 change: 1 addition & 0 deletions test/unittests/commands/xml_validate/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from test.unittests.commands.xml_validate.fixtures_data_deserialised import * # noqa: F403
from test.unittests.commands.xml_validate.fixtures_data_rdf import * # noqa: F403
from test.unittests.commands.xml_validate.fixtures_onto import * # noqa: F403
from test.unittests.commands.xml_validate.fixtures_xml_data import * # noqa: F403
96 changes: 96 additions & 0 deletions test/unittests/commands/xml_validate/fixtures_onto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import pytest
from rdflib import Graph
from rdflib import Namespace

ONTO = Namespace("http://0.0.0.0:3333/ontology/9999/onto/v2#")
API_SHAPES = Namespace("http://api.knora.org/ontology/knora-api/shapes/v2#")
PREFIXES = """
@prefix knora-api: <http://api.knora.org/ontology/knora-api/v2#> .
@prefix onto: <http://0.0.0.0:3333/ontology/9999/onto/v2#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix salsah-gui: <http://api.knora.org/ontology/salsah-gui/v2#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
"""


@pytest.fixture
def link_prop_card_1() -> Graph:
ttl = f"""{PREFIXES}
onto:ClassMixedCard a owl:Class ;
rdfs:label "Resource with all cardinality options" ;
knora-api:canBeInstantiated true ;
knora-api:isResourceClass true ;
rdfs:subClassOf [
a owl:Restriction ;
salsah-gui:guiOrder 1 ;
owl:cardinality 1 ;
owl:onProperty onto:testHasLinkToCardOneResourceValue
] ,
[ a owl:Restriction ;
salsah-gui:guiOrder 1 ;
owl:cardinality 1 ;
owl:onProperty onto:testHasLinkToCardOneResource ] .
onto:testHasLinkToCardOneResource a owl:ObjectProperty ;
rdfs:label "Super-class" ;
knora-api:isEditable true ;
knora-api:isLinkProperty true ;
knora-api:isResourceProperty true ;
knora-api:objectType onto:CardOneResource ;
salsah-gui:guiElement salsah-gui:Searchbox ;
rdfs:subPropertyOf knora-api:hasLinkTo .
onto:testHasLinkToCardOneResourceValue a owl:ObjectProperty ;
rdfs:label "Super-class" ;
knora-api:isEditable true ;
knora-api:isLinkValueProperty true ;
knora-api:isResourceProperty true ;
knora-api:objectType knora-api:LinkValue ;
salsah-gui:guiElement salsah-gui:Searchbox ;
rdfs:subPropertyOf knora-api:hasLinkToValue .
"""
g = Graph()
g.parse(data=ttl, format="ttl")
return g


@pytest.fixture
def link_prop_card_01() -> Graph:
ttl = f"""{PREFIXES}
onto:ClassMixedCard a owl:Class ;
rdfs:label "Resource with all cardinality options" ;
knora-api:canBeInstantiated true ;
knora-api:isResourceClass true ;
rdfs:subClassOf [
a owl:Restriction ;
salsah-gui:guiOrder 1 ;
owl:maxCardinality 1 ;
owl:onProperty onto:testHasLinkToCardOneResourceValue
] ,
[ a owl:Restriction ;
salsah-gui:guiOrder 1 ;
owl:maxCardinality 1 ;
owl:onProperty onto:testHasLinkToCardOneResource ] .
onto:testHasLinkToCardOneResource a owl:ObjectProperty ;
rdfs:label "Super-class" ;
knora-api:isEditable true ;
knora-api:isLinkProperty true ;
knora-api:isResourceProperty true ;
knora-api:objectType onto:CardOneResource ;
salsah-gui:guiElement salsah-gui:Searchbox ;
rdfs:subPropertyOf knora-api:hasLinkTo .
onto:testHasLinkToCardOneResourceValue a owl:ObjectProperty ;
rdfs:label "Super-class" ;
knora-api:isEditable true ;
knora-api:isLinkValueProperty true ;
knora-api:isResourceProperty true ;
knora-api:objectType knora-api:LinkValue ;
salsah-gui:guiElement salsah-gui:Searchbox ;
rdfs:subPropertyOf knora-api:hasLinkToValue .
"""
g = Graph()
g.parse(data=ttl, format="ttl")
return g
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def card_1() -> Graph:
ttl = f"""{PREFIXES}
onto:ClassMixedCard a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf [
a owl:Restriction ;
salsah-gui:guiOrder 0 ;
Expand All @@ -103,6 +104,7 @@ def card_0_1() -> Graph:
ttl = f"""{PREFIXES}
onto:ClassMixedCard a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf [
a owl:Restriction ;
salsah-gui:guiOrder 1 ;
Expand All @@ -120,6 +122,7 @@ def card_1_n() -> Graph:
ttl = f"""{PREFIXES}
onto:ClassMixedCard a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf [
a owl:Restriction ;
salsah-gui:guiOrder 2 ;
Expand All @@ -137,6 +140,7 @@ def card_0_n() -> Graph:
ttl = f"""{PREFIXES}
onto:ClassMixedCard a owl:Class ;
knora-api:isResourceClass true ;
knora-api:canBeInstantiated true ;
rdfs:subClassOf [
a owl:Restriction ;
salsah-gui:guiOrder 3 ;
Expand Down Expand Up @@ -172,7 +176,7 @@ def test_cardinality_0_1(self, onto_graph: Graph) -> None:

def test_cardinality_0_n(self, onto_graph: Graph) -> None:
result = _construct_0_n_cardinality(onto_graph)
number_of_occurrences_in_onto = 24
number_of_occurrences_in_onto = 21 # Inheritance included
triples_card_0_n = 3 * number_of_occurrences_in_onto
assert len(result) == triples_card_0_n

Expand Down Expand Up @@ -231,6 +235,18 @@ def test_good(self, card_1: Graph) -> None:
assert next(result.objects(bn, SH.severity)) == SH.Violation
assert str(next(result.objects(bn, SH.message))) == "1"

def test_good_link_value(self, link_prop_card_1: Graph) -> None:
result = _construct_1_cardinality(link_prop_card_1)
assert len(result) == 7
bn = next(result.subjects(RDF.type, SH.PropertyShape))
shape_iri = next(result.subjects(SH.property, bn))
assert shape_iri == ONTO.ClassMixedCard_Shape
assert str(next(result.objects(bn, SH.minCount))) == "1"
assert str(next(result.objects(bn, SH.maxCount))) == "1"
assert next(result.objects(bn, SH.path)) == ONTO.testHasLinkToCardOneResource
assert next(result.objects(bn, SH.severity)) == SH.Violation
assert str(next(result.objects(bn, SH.message))) == "1"

def test_empty_0_1(self, card_0_1: Graph) -> None:
result = _construct_1_cardinality(card_0_1)
assert len(result) == 0
Expand All @@ -257,6 +273,18 @@ def test_good(self, card_0_1: Graph) -> None:
assert next(result.objects(bn, SH.severity)) == SH.Violation
assert str(next(result.objects(bn, SH.message))) == "0-1"

def test_good_link_value(self, link_prop_card_01: Graph) -> None:
result = _construct_0_1_cardinality(link_prop_card_01)
assert len(result) == 7
bn = next(result.subjects(RDF.type, SH.PropertyShape))
shape_iri = next(result.subjects(SH.property, bn))
assert shape_iri == ONTO.ClassMixedCard_Shape
assert str(next(result.objects(bn, SH.minCount))) == "0"
assert str(next(result.objects(bn, SH.maxCount))) == "1"
assert next(result.objects(bn, SH.path)) == ONTO.testHasLinkToCardOneResource
assert next(result.objects(bn, SH.severity)) == SH.Violation
assert str(next(result.objects(bn, SH.message))) == "0-1"

def test_empty_1(self, card_1: Graph) -> None:
result = _construct_0_1_cardinality(card_1)
assert len(result) == 0
Expand Down
14 changes: 10 additions & 4 deletions testdata/xml-validate/data/cardinality_correct.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@
</boolean-prop>
</resource>

<resource label="Card Mixed" restype=":ClassMixedCard" id="id_3">
<resource label="Card 1" restype=":ClassInheritedCardinality" id="id_3">
<text-prop name=":testSimpleText">
<text encoding="utf8">Text</text>
</text-prop>
</resource>

<resource label="Card Mixed" restype=":ClassMixedCard" id="id_4">
<boolean-prop name=":testBoolean">
<boolean>true</boolean>
</boolean-prop>
<decimal-prop name=":testDecimalSimpleText">
<decimal>2.71</decimal>
</decimal-prop>
<resptr-prop name=":testHasLinkToCardOneResource">
<resptr>id_2</resptr>
</resptr-prop>
<geoname-prop name=":testGeoname">
<geoname>1111111</geoname>
<geoname>2222222</geoname>
Expand Down
14 changes: 7 additions & 7 deletions testdata/xml-validate/data/cardinality_violation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@
<boolean-prop name=":testBoolean">
<boolean>true</boolean>
</boolean-prop>
<decimal-prop name=":testDecimalSimpleText">
<decimal>2.71</decimal>
<decimal>2.00</decimal>
</decimal-prop>
<resptr-prop name=":testHasLinkToCardOneResource">
<resptr>id_closed_constraint</resptr>
<resptr>id_card_one</resptr>
</resptr-prop>
<geoname-prop name=":testGeoname">
<geoname>111111</geoname>
</geoname-prop>
</resource>

<!-- 'testGeoname' cardinality 1-n -->
<resource label="Geoname Card 1-n" restype=":ClassMixedCard" id="id_min_card">
<boolean-prop name=":testBoolean">
<boolean>true</boolean>
</boolean-prop>
<resptr-prop name=":testHasLinkToCardOneResource">
<resptr>id_closed_constraint</resptr>
</resptr-prop>
</resource>

</knora>
Loading

0 comments on commit a8a67ab

Please sign in to comment.