Last active
November 11, 2021 15:09
-
-
Save joshmoore/60be4cff1f9a6e173be68ed2f232d3f4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from pyshacl import validate | |
from rdflib import Graph | |
import pytest | |
import json | |
def assert_graph(shacl, graph, valid=True): | |
s = Graph().parse(data=shacl, format="turtle") | |
d = Graph().parse(data=graph, format="json-ld") | |
conforms, report, message = validate( | |
d, | |
shacl_graph=s, | |
advanced=True, | |
debug=False, | |
allow_warnings=True, | |
) | |
if not conforms: | |
if valid: | |
raise Exception(message) | |
else: | |
if not valid: | |
raise Exception(f"expected failure: {report}") | |
shacl = """ | |
@prefix sh: <http://www.w3.org/ns/shacl#> . | |
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . | |
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . | |
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . | |
@prefix ngff: <http://example.com/ns#> . | |
@prefix shapes: <http://example.com/schema#> . | |
shapes:ItemShape | |
a sh:NodeShape ; | |
sh:closed true ; | |
sh:ignoredProperties ( rdf:type ) ; | |
sh:targetClass ngff:ListItem ; | |
sh:property [ | |
sh:path ngff:item ; | |
# `collectionType` applied by sparql | |
] ; | |
sh:property [ | |
sh:path ngff:position; | |
sh:datatype xsd:integer ; | |
] . | |
shapes:BaseCollectionShape | |
a sh:NodeShape ; | |
sh:closed true ; | |
sh:ignoredProperties ( rdf:type ) ; | |
sh:targetClass ngff:ItemList ; | |
sh:property [ | |
sh:path ngff:itemListElement ; | |
sh:sparql [ | |
sh:message "Positions MUST be unique." ; | |
sh:prefixes ngff: ; | |
sh:select \"\"\" | |
SELECT $this ?position | |
WHERE { $this $PATH ?item . ?item ngff:position ?position } | |
GROUP BY ?position | |
HAVING (COUNT(*) > 1) | |
\"\"\" | |
] ; | |
] ; | |
sh:property [ | |
sh:path ngff:collectionType ; | |
sh:sparql [ | |
sh:message "Apply collection type: $this" ; | |
sh:prefixes ngff: ; | |
sh:prefixes rdfs: ; | |
sh:prefixes rdf: ; | |
sh:select \"\"\" | |
SELECT $this ?type | |
WHERE { | |
$this $PATH ?typeObj . | |
?typeObj rdf:type ?type . | |
$this ngff:itemListElement ?item . | |
?item ngff:item ?obj . | |
FILTER NOT EXISTS { ?obj rdf:type/rdfs:subClassOf* ?type } | |
} | |
\"\"\" | |
] ; | |
] ; | |
sh:property [ | |
sh:path ngff:itemListElement; | |
sh:node shapes:ItemShape; | |
] . """ | |
coll_valid = """ | |
{ | |
"@context" : { | |
"ngff" : "http://example.com/ns#" | |
}, | |
"@graph": [{ | |
"@type": "ngff:ItemList", | |
"ngff:itemListElement": [ | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 1, | |
"ngff:item": { | |
"@type": "ngff:Image", | |
"path": "image1", | |
"name": "Image 1" | |
} | |
}, | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 2, | |
"ngff:item": { | |
"@type": "%(type)s", | |
"path": "image2", | |
"name": "Image 2" | |
} | |
} | |
] | |
}] | |
} | |
""" | |
coll_bad_position = """ | |
{ | |
"@context" : { | |
"ngff" : "http://example.com/ns#" | |
}, | |
"@graph": [{ | |
"@type": "ngff:ItemList", | |
"ngff:itemListElement": [ | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 1, | |
"ngff:item": { | |
"@type": "ngff:Image", | |
"path": "image1", | |
"name": "Image 1" | |
} | |
}, | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 1, | |
"ngff:item": { | |
"@type": "ngff:Image", | |
"path": "image2", | |
"name": "Image 2" | |
} | |
} | |
] | |
}] | |
} | |
""" | |
images_valid_subclass = """ | |
{ | |
"@context" : { | |
"ngff" : "http://example.com/ns#", | |
"schema" : "http://schema.org/", | |
"rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", | |
"rdfs": "http://www.w3.org/2000/01/rdf-schema#" , | |
"xsd" : "http://www.w3.org/2001/XMLSchema#" | |
}, | |
"@graph": [{ | |
"@id": "ngff:NewImage", | |
"@type": "rdfs:Class", | |
"rdfs:subClassOf": { | |
"@id": "ngff:Image" | |
} | |
}, { | |
"@type": "ngff:ItemList", | |
"ngff:collectionType": {"@type": "ngff:Image"}, | |
"ngff:itemListElement": [ | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 1, | |
"ngff:item": { | |
"@type": "ngff:Image", | |
"path": "image1", | |
"name": "Image 1" | |
} | |
}, | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 2, | |
"ngff:item": { | |
"@type": "ngff:NewImage", | |
"path": "image2", | |
"name": "Image 2" | |
} | |
} | |
] | |
}] | |
} | |
""" | |
images_non_image = """ | |
{ | |
"@context" : { | |
"ngff" : "http://example.com/ns#", | |
"schema" : "http://schema.org/", | |
"rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", | |
"rdfs": "http://www.w3.org/2000/01/rdf-schema#" , | |
"xsd" : "http://www.w3.org/2001/XMLSchema#" | |
}, | |
"@graph": [{ | |
"@id": "ngff:NonImage", | |
"@type": "rdfs:Class", | |
"rdfs:subClassOf": { | |
"@id": "ngff:Foo" | |
} | |
}, { | |
"@type": "ngff:ItemList", | |
"ngff:collectionType": {"@type": "ngff:Image"}, | |
"ngff:itemListElement": [ | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 1, | |
"ngff:item": { | |
"@type": "ngff:Image", | |
"path": "image1", | |
"name": "Image 1" | |
} | |
}, | |
{ | |
"@type": "ngff:ListItem", | |
"ngff:position": 2, | |
"ngff:item": { | |
"@type": "ngff:NonImage", | |
"path": "something-else", | |
"name": "bob" | |
} | |
} | |
] | |
}] | |
} | |
""" | |
@pytest.mark.parametrize("data,valid", ( | |
pytest.param(coll_valid % { "type": "ngff:Image"}, True, id="coll_valid"), | |
pytest.param(coll_valid % { "type": "ngff:NonImage"}, True, id="coll_invalid"), | |
pytest.param(coll_bad_position, False, id="coll_bad_position"), | |
pytest.param(images_valid_subclass, True, id="images_valid_subclass"), | |
pytest.param(images_non_image, False, id="images_non_image"), | |
)) | |
def test_simplified(data, valid): | |
assert_graph(shacl, data, valid) | |
def test_wrap(): | |
""" | |
Example of simplifying the _data_ graph while still using schema.org-style | |
lists in the shacl graph. | |
""" | |
shacl = """ | |
@prefix sh: <http://www.w3.org/ns/shacl#> . | |
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . | |
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . | |
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . | |
@prefix ngff: <http://example.com/ns#> . | |
@prefix shapes: <http://example.com/schema#> . | |
shapes:ItemShape | |
a sh:NodeShape ; | |
sh:closed true ; | |
sh:ignoredProperties ( rdf:type ) ; | |
sh:targetClass ngff:ListItemWrapper ; | |
sh:property [ | |
sh:path ngff:item ; | |
# `collectionType` applied by sparql | |
] ; | |
sh:property [ | |
sh:path ngff:position; | |
sh:datatype xsd:integer ; | |
] . | |
shapes:BaseCollectionShape | |
a sh:NodeShape ; | |
sh:closed true ; | |
sh:ignoredProperties ( rdf:type ) ; | |
sh:targetClass ngff:ItemList ; | |
sh:property [ | |
sh:path ngff:itemListElement ; | |
sh:sparql [ | |
sh:message "Positions MUST be unique." ; | |
sh:prefixes ngff: ; | |
sh:select \"\"\" | |
SELECT $this ?position | |
WHERE { $this $PATH ?item . ?item ngff:position ?position } | |
GROUP BY ?position | |
HAVING (COUNT(*) > 1) | |
\"\"\" | |
] ; | |
] ; | |
sh:property [ | |
sh:path ngff:collectionType ; | |
sh:sparql [ | |
sh:message "Apply collection type: $this" ; | |
sh:prefixes ngff: ; | |
sh:prefixes rdfs: ; | |
sh:prefixes rdf: ; | |
sh:select \"\"\" | |
SELECT $this ?type | |
WHERE { | |
$this $PATH ?typeObj . | |
?typeObj rdf:type ?type . | |
$this ngff:itemListElement ?item . | |
?item ngff:item ?obj . | |
FILTER NOT EXISTS { ?obj rdf:type/rdfs:subClassOf* ?type } | |
} | |
\"\"\" | |
] ; | |
] ; | |
sh:property [ | |
sh:path ngff:itemListElement; | |
sh:node shapes:ItemShape; | |
] . """ | |
data = """ | |
{ | |
"@context" : { | |
"ngff" : "http://example.com/ns#" | |
}, | |
"@graph": [{ | |
"@type": "ngff:ItemList", | |
"ngff:collectionType": {"@type": "ngff:Image"}, | |
"ngff:itemListElement": [ | |
{ | |
"@type": "ngff:Image", | |
"path": "image1", | |
"name": "Image 1" | |
}, | |
{ | |
"@type": "ngff:Image", | |
"path": "something-else", | |
"name": "bob" | |
} | |
] | |
}] | |
} | |
""" | |
data = json.loads(data) | |
data = walk(data) | |
data = json.dumps(data) ## TODO: Seems wasteful | |
assert_graph(shacl, data, True) | |
def walk(data, path=None): | |
if path is None: | |
path = [] | |
if isinstance(data, dict): | |
for k, v in data.items(): | |
data[k] = walk(v, path + [k]) | |
elif isinstance(data, list): | |
replacement = list() | |
for idx, item in enumerate(data): | |
if path[-1] == "@graph": | |
replacement.append(walk(item, path)) | |
else: | |
wrapper = { | |
"@type": "ListItemWrapper", | |
"ngff:position": idx | |
} | |
wrapper["ngff:item"] = walk(item, path + [idx]) | |
replacement.append(wrapper) | |
data = replacement | |
return data | |
def test_rdf_list(): | |
data = """ | |
{ | |
"@context" : { | |
"example" : { | |
"@id": "http://example.com/example", | |
"@container": "@list" | |
} | |
}, | |
"@graph": [{ | |
"@id": "my-list", | |
"@type": "http://example.com/type", | |
"example": ["a","b","c"] | |
}] | |
} | |
""" | |
shacl = """ | |
@prefix sh: <http://www.w3.org/ns/shacl#> . | |
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . | |
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . | |
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . | |
@prefix ex: <http://example.com/> . | |
@prefix owl: <http://www.w3.org/2002/07/owl#> . | |
@prefix dash: <http://datashapes.org/dash> . | |
# <http://example.org/myShapesGraph> owl:imports <http://datashapes.org/dash> . | |
ex:MyNodeShape | |
a sh:NodeShape ; | |
sh:targetClass ex:type ; | |
sh:property [ | |
sh:path ex:example; | |
sh:node dash:ListShape ; | |
sh:property [ | |
sh:path ( [ sh:zeroOrMorePath rdf:rest ] rdf:first ) ; | |
sh:datatype xsd:string; | |
# sh:minLength 3 ; | |
sh:minCount 1; | |
sh:maxCount 3; | |
] | |
] . | |
# sh:property [ | |
# sh:path ex:example; | |
# sh:sparql [ | |
# sh:message "working with lists" ; | |
# sh:prefixes ex: ; | |
# sh:prefixes rdfs: ; | |
# sh:prefixes rdf: ; | |
# sh:select \"\"\" | |
# SELECT $this ?value | |
# WHERE { | |
# $this $PATH ?bnode . | |
# ?bnode rdf:rest*/rdf:first ?value . | |
# ?value in ("a") . | |
# } | |
# \"\"\" | |
# ] ; | |
# ] . | |
""" | |
d = Graph().parse(data=data, format="json-ld") | |
for stmt in d: | |
import pprint | |
pprint.pprint(stmt) | |
assert_graph(shacl, data, True) | |
def test_rdf_alt(): | |
# https://stackoverflow.com/questions/44959817/how-to-represent-collection-of-alternatives-in-json-ld | |
data = """{ | |
"@context": { | |
"ex": "http://example.com/example/", | |
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |
}, | |
"@graph": [ | |
{ | |
"@id": "ex:object1", | |
"@type": "ex:toplevel", | |
"ex:availableOptions": { | |
"@id": "ex:optionsFor1" | |
} | |
}, | |
{ | |
"@id": "ex:optionsFor1", | |
"@type": "rdf:Seq", | |
"rdf:_1": 100, | |
"rdf:_2": 120, | |
"rdf:_3": 130 | |
} | |
] | |
}""" | |
shacl = """ | |
@prefix sh: <http://www.w3.org/ns/shacl#> . | |
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . | |
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . | |
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . | |
@prefix ex: <http://example.com/example/> . | |
ex:MyNodeShape | |
a sh:NodeShape ; | |
sh:targetClass ex:toplevel ; | |
sh:property [ | |
sh:path ex:availableOptions ; | |
sh:property [ | |
sh:path rdf:_1 ; | |
sh:datatype xsd:integer ; | |
] | |
] . | |
""" | |
d = Graph().parse(data=data, format="json-ld") | |
for stmt in d: | |
import pprint | |
pprint.pprint(stmt) | |
assert_graph(shacl, data, True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment