diff --git a/shacl/BrickEntityShapeBase.ttl b/shacl/BrickEntityShapeBase.ttl new file mode 100644 index 00000000..b49c21f3 --- /dev/null +++ b/shacl/BrickEntityShapeBase.ttl @@ -0,0 +1,113 @@ +@prefix brick: . +@prefix rdf: . +@prefix bsh: . +@prefix owl: . +@prefix sh: . +@prefix unit: . +@prefix vcard: . +@prefix xsd: . + + +bsh:LocationShape a sh:NodeShape ; + sh:targetClass brick:Location ; + sh:not [ + sh:or ( + [sh:class brick:Point;] + [sh:class brick:Equipment;] + [sh:class brick:Substance;] + [sh:class brick:Quantity;] + ); + sh:message "Location is an exclusive top class." + ]; + sh:property [ + sh:path brick:hasPart; + sh:class brick:Location; + sh:message "A Location's parts should be always Locations." + ]; + sh:property [ + sh:path brick:isPartOf; + sh:class brick:Location; + sh:message "A Location's parts should be always Locations." + ]; + sh:property [ + sh:path brick:isFedBy; + sh:class brick:Equipment; + sh:message "Locations can be fed only by other Equipment." + ]; + sh:property [ + sh:path brick:hasPoint; + sh:class brick:Point; + sh:message "A Location may have associated Points" + ]; + . + +bsh:EquipmentShape a sh:NodeShape ; + sh:targetClass brick:Equipment ; + sh:not [ + sh:or ( + [sh:class brick:Location;] + [sh:class brick:Point;] + [sh:class brick:Substance;] + [sh:class brick:Quantity;] + ); + sh:message "Equipment is an exclusive top class." + ]; + sh:property [ + sh:path brick:hasPart; + sh:class brick:Equipment; + sh:message "A piece of Equipment's parts should be always other Equipment." + ]; + sh:property [ + sh:path brick:isPartOf; + sh:class brick:Equipment; + sh:message "A piece of Equipment's parts should be always other Equipment." + ]; + sh:property [ + sh:path brick:hasLocation; + sh:class brick:Location; + sh:message "A piece of Equipment can be located only at a Location" + ]; + sh:property [ + sh:path brick:feeds; + sh:or ( + [ sh:class brick:Equipment ] + [ sh:class brick:Location ] + ) ; + sh:message "A piece of Equipment can be located at a Location" + ]; + sh:property [ + sh:path brick:hasPoint; + sh:class brick:Point; + sh:message "A piece of Equipment may have associated Points" + ]; + . + +bsh:PointShape a sh:NodeShape; + sh:targetClass brick:Point; + sh:not [ + sh:or ( + [sh:class brick:Location;] + [sh:class brick:Equipment;] + [sh:class brick:Substance;] + [sh:class brick:Quantity;] + ); + sh:message "Point is an exclusive top class." + ]; + sh:property [ + sh:path brick:isPointOf; + sh:or ( + [ sh:class brick:Location ] + [ sh:class brick:Equipment ] + ); + sh:message "A Point can be associated with Locations or Equipment." + ]; + . + +bsh:hasLocationShape a sh:Nodeshape; + sh:targetSubjectsOf brick:hasLocation; + sh:not [ + sh:class brick:Point; + ]; + sh:message "Points are a virtual concept and always belonging to a physical device, represented by Equipment. Thus, it cannot have a Location alone." + . + diff --git a/shacl/BrickShape.ttl b/shacl/BrickShape.ttl index ea4a203f..5a456870 100644 --- a/shacl/BrickShape.ttl +++ b/shacl/BrickShape.ttl @@ -1,11 +1,44 @@ @prefix brick: . @prefix bsh: . @prefix owl: . +@prefix rdf: . @prefix sh: . @prefix unit: . @prefix vcard: . @prefix xsd: . +bsh:EquipmentShape a sh:NodeShape ; + sh:not [ sh:or ( [ sh:class brick:Location ] [ sh:class brick:Point ] [ sh:class brick:Substance ] [ sh:class brick:Quantity ] ) ] ; + sh:property [ sh:class brick:Equipment ; + sh:path brick:hasPart ], + [ sh:class brick:Equipment ; + sh:path brick:isPartOf ], + [ sh:class brick:Location ; + sh:path brick:hasLocation ], + [ sh:or ( [ sh:class brick:Equipment ] [ sh:class brick:Location ] ) ; + sh:path brick:feeds ], + [ sh:class brick:Point ; + sh:path brick:hasPoint ] ; + sh:targetClass brick:Equipment . + +bsh:LocationShape a sh:NodeShape ; + sh:not [ sh:or ( [ sh:class brick:Point ] [ sh:class brick:Equipment ] [ sh:class brick:Substance ] [ sh:class brick:Quantity ] ) ] ; + sh:property [ sh:class brick:Location ; + sh:path brick:hasPart ], + [ sh:class brick:Location ; + sh:path brick:isPartOf ], + [ sh:class brick:Equipment ; + sh:path brick:isFedBy ], + [ sh:class brick:Point ; + sh:path brick:hasPoint ] ; + sh:targetClass brick:Location . + +bsh:PointShape a sh:NodeShape ; + sh:not [ sh:or ( [ sh:class brick:Location ] [ sh:class brick:Equipment ] [ sh:class brick:Substance ] [ sh:class brick:Quantity ] ) ] ; + sh:property [ sh:or ( [ sh:class brick:Location ] [ sh:class brick:Equipment ] ) ; + sh:path brick:isPointOf ] ; + sh:targetClass brick:Point . + bsh:hasAddressDomainShape a sh:NodeShape ; sh:class brick:Building ; sh:message "Property hasAddress has subject with incorrect type" ; @@ -40,6 +73,10 @@ bsh:hasLocationRangeShape a sh:NodeShape ; sh:path brick:hasLocation ] ; sh:targetSubjectsOf brick:hasLocation . +bsh:hasLocationShape a sh:Nodeshape ; + sh:not [ sh:class brick:Point ] ; + sh:targetSubjectsOf brick:hasLocation . + bsh:hasOutputSubstanceRangeShape a sh:NodeShape ; sh:property [ sh:class brick:Substance ; sh:message "Property hasOutputSubstance has object with incorrect type" ; diff --git a/shacl/generate_shacl.py b/shacl/generate_shacl.py index dd380633..7192224d 100755 --- a/shacl/generate_shacl.py +++ b/shacl/generate_shacl.py @@ -14,6 +14,8 @@ rangeShapeDict = {} subpropertyDict = {} +# Add base Entity shapes +G.parse('BrickEntityShapeBase.ttl', format='turtle') # Make shape for expectedDomain property def addDomainShape(propertyName, expectedType): diff --git a/tests/test_generate_shacl.py b/tests/test_generate_shacl.py deleted file mode 100644 index 6cdb59ef..00000000 --- a/tests/test_generate_shacl.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys -import brickschema -from bricksrc.namespaces import A, OWL, RDFS, SKOS, BRICK, SH, BSH, bind_prefixes -from .util import make_readable - -sys.path.append("..") -from bricksrc.properties import properties # noqa: E402 - -g = brickschema.Graph() -g.load_file("shacl/BrickShape.ttl") -bind_prefixes(g) - - -def test_domainProperties(): - for (name, props) in properties.items(): - if RDFS.domain in props: - q = f"""SELECT ?shape WHERE {{ - ?shape a sh:NodeShape . - ?shape sh:targetSubjectsOf brick:{name} . - ?shape sh:class <{props[RDFS.domain]}> . }} - """ - res = make_readable(g.query(q)) - assert len(res) == 1, "unexpected # of query results" - return - - -def test_rangeProperties(): - for (name, props) in properties.items(): - if RDFS.range in props: - q = f"""SELECT ?shape WHERE {{ - ?shape a sh:NodeShape . - ?shape sh:property [ - sh:class <{props[RDFS.range]}> ; - sh:path brick:{name} ; - ] }} - """ - res = make_readable(g.query(q)) - assert len(res) == 1, "unexpected # of query results" - return diff --git a/tests/test_shapes.py b/tests/test_shapes.py new file mode 100644 index 00000000..6205902a --- /dev/null +++ b/tests/test_shapes.py @@ -0,0 +1,61 @@ +import sys +from bricksrc.namespaces import A, OWL, RDFS, SKOS, BRICK, SH, BSH, bind_prefixes +import brickschema + +schema_g = brickschema.Graph().load_file('shacl/BrickShape.ttl') +bind_prefixes(schema_g) + +prefixes = """ +@prefix brick: . +@prefix : . +""" + +base_data = prefixes + """ +:equip a brick:Equipment. +:point a brick:Point. +:loc a brick:Location. +""" + +def test_no_relations(): + data = base_data + data_g = brickschema.Graph().parse(data=data, format='turtle') + conforms, r1, r2 = data_g.validate([schema_g]) + assert conforms + +def test_equip(): + valid_data = base_data + """ +:equip brick:hasLocation :loc. +""" + valid_g = brickschema.Graph().parse(data=valid_data, format='turtle') + conforms, _, _ = valid_g.validate([schema_g]) + assert conforms + + invalid_data = base_data + """ +:equip brick:hasLocation :point. + +""" + invalid_g = brickschema.Graph().parse(data=invalid_data, format='turtle') + conforms, _, _= invalid_g.validate([schema_g]) + assert not conforms + + +def test_type(): + invalid_data = base_data + """ +:loc a brick:Point. +""" + invalid_g = brickschema.Graph().parse(data=invalid_data, format='turtle') + conforms, _, _= invalid_g.validate([schema_g]) + assert not conforms + + +def test_point(): + invalid_data = base_data + """ +:point brick:hasLocation :loc. +""" + invalid_g = brickschema.Graph().parse(data=invalid_data, format='turtle') + conforms, _, _= invalid_g.validate([schema_g]) + assert not conforms + + + +