Skip to content

Instantly share code, notes, and snippets.

@joshmoore
Last active November 11, 2021 15:09
Show Gist options
  • Save joshmoore/60be4cff1f9a6e173be68ed2f232d3f4 to your computer and use it in GitHub Desktop.
Save joshmoore/60be4cff1f9a6e173be68ed2f232d3f4 to your computer and use it in GitHub Desktop.
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