zazuko/rdf-validate-shacl

Inheritance works only up to one level

BenediktHeinrichs opened this issue · 6 comments

Recently, I have been trying to create shapes which inherit properties from other shapes.

Using this library, the validation went fairly well until I hit the second inherited level.

This means that having a construct of Shape C > Shape B and Shape B > Shape A, the properties of Shape B will be validated, the properties of Shape A however won't.

I tried this construct in the https://shacl.org/playground/ and this supports the structure, so I assume there is something not working correctly.

In the following I provide an example shape graph and data graph which validates correctly that the field "schema:name" is missing in the playground, however does not report that issue using this library.

Shapes Graph:
@prefix dash: <http://datashapes.org/dash#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix schema: <http://schema.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix dcterms: <http://purl.org/dc/terms/> .

schema:ToolShape
    a sh:NodeShape , rdfs:Class ;
    sh:targetClass schema:ToolShape ;
    sh:property [
        sh:path schema:name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Name"@en, "Name"@de ;
    ] .

schema:SoftwareShape
    a sh:NodeShape , rdfs:Class ;
    rdfs:subClassOf schema:ToolShape ;
    sh:targetClass schema:SoftwareShape ;
    sh:property [
        sh:path schema:version ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Version"@en, "Version"@de ;
    ] ;
    sh:property [
        sh:path schema:creator ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:name "Creator"@en, "Ersteller"@de ;
    ] .

schema:AnalysisSoftwareShape
    a sh:NodeShape , rdfs:Class ;
    rdfs:subClassOf schema:SoftwareShape ;
    sh:targetClass schema:AnalysisSoftwareShape .

Data Graph:
[
   {
      "@type":"http://schema.org/AnalysisSoftwareShape",
      "http://schema.org/version":{
         "@value":"ü+",
         "@type":"http://www.w3.org/2001/XMLSchema#string"
      },
      "http://schema.org/creator":{
         "@value":"pop",
         "@type":"http://www.w3.org/2001/XMLSchema#string"
      }
   },
   {
      "@id":"http://schema.org/SoftwareShape",
      "http://www.w3.org/2000/01/rdf-schema#subClassOf":[
         {
            "@id":"http://schema.org/ToolShape"
         }
      ]
   },
   {
      "@id":"http://schema.org/AnalysisSoftwareShape",
      "http://www.w3.org/2000/01/rdf-schema#subClassOf":[
         {
            "@id":"http://schema.org/SoftwareShape"
         }
      ]
   },
   {
      "@id":"http://schema.org/ToolShape"
   }
]

I created a minimal code example for this issue, which can test this problem:

index.js
async function main() {
    const fs = require('fs');
    const factory = require('rdf-ext');
    const ParserN3 = require('@rdfjs/parser-n3');
    const SHACLValidator = require('rdf-validate-shacl');

    async function loadDataset (filePath) {
    const stream = fs.createReadStream(filePath);
    const parser = new ParserN3({ factory });
    return factory.dataset().import(parser.import(stream));
    }

    const shapes = await loadDataset('my-shapes.ttl');
    const data = await loadDataset('my-data.ttl');

    const validator = new SHACLValidator(shapes, { factory });
    const report = await validator.validate(data);

    // Check conformance: `true` or `false`
    console.log(report.conforms);

    for (const result of report.results) {
        // See https://www.w3.org/TR/shacl/#results-validation-result for details
        // about each property
        console.log(result.message);
        console.log(result.path);
        console.log(result.focusNode);
        console.log(result.severity);
        console.log(result.sourceConstraintComponent);
        console.log(result.sourceShape);
    }

    // Validation report as RDF dataset
    console.log(report.dataset);

}

main();
my-shapes.ttl
@prefix dash: <http://datashapes.org/dash#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix schema: <http://schema.org/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix dcterms: <http://purl.org/dc/terms/> .

schema:ToolShape
    a sh:NodeShape , rdfs:Class ;
    sh:targetClass schema:ToolShape ;
    sh:property [
        sh:path schema:name ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:name "Name"@en, "Name"@de ;
    ] .

schema:SoftwareShape
    a sh:NodeShape , rdfs:Class ;
    rdfs:subClassOf schema:ToolShape ;
    sh:targetClass schema:SoftwareShape ;
    sh:property [
        sh:path schema:version ;
        sh:datatype xsd:string ;
        sh:maxCount 1 ;
        sh:name "Version"@en, "Version"@de ;
    ] ;
    sh:property [
        sh:path schema:creator ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:name "Creator"@en, "Ersteller"@de ;
    ] .

schema:AnalysisSoftwareShape
    a sh:NodeShape , rdfs:Class ;
    rdfs:subClassOf schema:SoftwareShape ;
    sh:targetClass schema:AnalysisSoftwareShape .
my-data.ttl
@prefix schema: <http://schema.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

schema:SoftwareShape rdfs:subClassOf schema:ToolShape .
schema:AnalysisSoftwareShape rdfs:subClassOf schema:SoftwareShape .
[]
  a schema:AnalysisSoftwareShape ;
  schema:creator "pop"^^xsd:string ;
  schema:version "ü+"^^xsd:string .
package-lock.json
{
  "requires": true,
  "lockfileVersion": 1,
  "dependencies": {
    "@rdfjs/data-model": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-1.2.0.tgz",
      "integrity": "sha512-6ITWcu2sr9zJqXUPDm1XJ8DRpea7PotWBIkTzuO1MCSruLOWH2ICoQOAtlJy30cT+GqH9oAQKPR+CHXejsdizA==",
      "requires": {
        "@types/rdf-js": "*"
      }
    },
    "@rdfjs/dataset": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@rdfjs/dataset/-/dataset-1.0.1.tgz",
      "integrity": "sha512-k/c6g4K881QX7LE3eskg6t1j31zDe+CKwTEiKkSCFk6M25gUJ/BReT/FrLdKmPjhXp+YOgHj97AtEphzTeKVeA==",
      "requires": {
        "@rdfjs/data-model": "^1.1.1"
      }
    },
    "@rdfjs/namespace": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@rdfjs/namespace/-/namespace-1.1.0.tgz",
      "integrity": "sha512-utO5rtaOKxk8B90qzaQ0N+J5WrCI28DtfAY/zExCmXE7cOfC5uRI/oMKbLaVEPj2P7uArekt/T4IPATtj7Tjug==",
      "requires": {
        "@rdfjs/data-model": "^1.1.0"
      }
    },
    "@rdfjs/parser-n3": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/@rdfjs/parser-n3/-/parser-n3-1.1.4.tgz",
      "integrity": "sha512-PUKSNlfD2Zq3GcQZuOF2ndfrLbc+N96FUe2gNIzARlR2er0BcOHBHEFUJvVGg1Xmsg3hVKwfg0nwn1JZ7qKKMw==",
      "requires": {
        "@rdfjs/data-model": "^1.0.1",
        "@rdfjs/sink": "^1.0.2",
        "n3": "^1.3.5",
        "readable-stream": "^3.6.0",
        "readable-to-readable": "^0.1.0"
      }
    },
    "@rdfjs/sink": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/@rdfjs/sink/-/sink-1.0.3.tgz",
      "integrity": "sha512-2KfYa8mAnptRNeogxhQqkWNXqfYVWO04jQThtXKepySrIwYmz83+WlevQtA4VDLFe+kFd2TwgL29ekPe5XVUfA=="
    },
    "@rdfjs/term-set": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@rdfjs/term-set/-/term-set-1.0.1.tgz",
      "integrity": "sha512-7+pSdm8R1nhSyP1xEqcQYFTARD/2iLr98tiAsSUfykCg0T0LrZ6j/3EEKmcKTWLMazMA9deRfmjPvzqMFwLBig==",
      "requires": {
        "@rdfjs/to-ntriples": "^1.0.2"
      }
    },
    "@rdfjs/to-ntriples": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-1.0.2.tgz",
      "integrity": "sha512-ngw5XAaGHjgGiwWWBPGlfdCclHftonmbje5lMys4G2j4NvfExraPIuRZgjSnd5lg4dnulRVUll8tRbgKO+7EDA=="
    },
    "@tpluscode/rdf-ns-builders": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/@tpluscode/rdf-ns-builders/-/rdf-ns-builders-0.1.0.tgz",
      "integrity": "sha512-HxcNaUSZZOIBS8pdYiyRodFejLCcrleCMc/b8qdHA8bw830u5JOomSBZKRgylEAaps69G7PQPWtwvAEFSPRcKQ==",
      "requires": {
        "@rdfjs/namespace": "^1.1.0",
        "@types/rdf-js": "^2.0.11",
        "@types/rdfjs__namespace": "^1.1.1"
      },
      "dependencies": {
        "@types/rdf-js": {
          "version": "2.0.12",
          "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-2.0.12.tgz",
          "integrity": "sha512-NBzHFHp2vHOJkPlSqzsOrkEsD9grKn+PdFjZieIw59pc0FlRG6WEQAjQZvHzFxJlYzC6ZDCFyTA1wBvUnnzUQw==",
          "requires": {
            "@types/node": "*"
          }
        }
      }
    },
    "@types/node": {
      "version": "14.14.13",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz",
      "integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ=="
    },
    "@types/rdf-js": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-4.0.0.tgz",
      "integrity": "sha512-2uaR7ks0380MqzUWGOPOOk9yZIr/6MOaCcaj3ntKgd2PqNocgi8j5kSHIJTDe+5ABtTHqKMSE0v0UqrsT8ibgQ==",
      "requires": {
        "@types/node": "*"
      }
    },
    "@types/rdfjs__namespace": {
      "version": "1.1.3",
      "resolved": "https://registry.npmjs.org/@types/rdfjs__namespace/-/rdfjs__namespace-1.1.3.tgz",
      "integrity": "sha512-seCWK6z0TqF40ioY7lMhR624I9ovo7xhnhvhAvbWlxfXYtsoQ4QEoddaWFmJDqfat2M1Xv6ThRSN2JGm2OtTIw==",
      "requires": {
        "@types/rdf-js": "*"
      }
    },
    "clownface": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/clownface/-/clownface-1.2.0.tgz",
      "integrity": "sha512-01iluBG+GPzFFFvFPhZxuWf8gU8mQVdm6gkvI71xl4fJyJSX7VpX3US4LbMC+0D6+KK75C+Q9AFtYpzv3fKvmg==",
      "requires": {
        "@rdfjs/data-model": "^1.1.0",
        "@rdfjs/namespace": "^1.0.0"
      }
    },
    "debug": {
      "version": "4.3.1",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
      "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
      "requires": {
        "ms": "2.1.2"
      }
    },
    "inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
    },
    "ms": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
    },
    "n3": {
      "version": "1.6.4",
      "resolved": "https://registry.npmjs.org/n3/-/n3-1.6.4.tgz",
      "integrity": "sha512-qiiBhW2nJ59cfQzi0nvZs5tSXkXgDXedIy3zNNuKjTwE8Bcvv95DTFJpOY9geg6of5T7z6cg+ZWcaHIij3svrA==",
      "requires": {
        "queue-microtask": "^1.1.2",
        "readable-stream": "^3.6.0"
      }
    },
    "queue-microtask": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz",
      "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg=="
    },
    "rdf-dataset-indexed": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/rdf-dataset-indexed/-/rdf-dataset-indexed-0.4.0.tgz",
      "integrity": "sha512-xIZ3PGwBHh1DVax9FTq/zhJcrdJTkCgRT2xolF5ZhKj0EOFoT6wdITyfxKSmnD6YNyGyng5F5o2SR62TYask3Q==",
      "requires": {
        "@rdfjs/data-model": "^1.1.1",
        "readable-stream": "^3.3.0"
      }
    },
    "rdf-ext": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/rdf-ext/-/rdf-ext-1.3.0.tgz",
      "integrity": "sha512-bzkJpINH1uZgAeSIlE7K1nLF6Y82vGi9U9sBRuiz864pNk/31ba3dw+yLoIkcvQsPcVwnKBVLvGCR3dvD9xM+Q==",
      "requires": {
        "@rdfjs/data-model": "^1.1.0",
        "@rdfjs/to-ntriples": "^1.0.1",
        "rdf-dataset-indexed": "^0.4.0",
        "rdf-normalize": "^1.0.0"
      }
    },
    "rdf-normalize": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/rdf-normalize/-/rdf-normalize-1.0.0.tgz",
      "integrity": "sha1-U0lrrzYszp2fyh8iFsbDAAf5nMo="
    },
    "rdf-validate-datatype": {
      "version": "0.1.1",
      "resolved": "https://registry.npmjs.org/rdf-validate-datatype/-/rdf-validate-datatype-0.1.1.tgz",
      "integrity": "sha512-Gjwmc37cCj57Si2MXJSUZ2FKQPlMg8G9HOyvrcomQcq8Z3beZ3X9IGjmsz1kSa3+qiQsk5WsAESIFAnEXL8vBw==",
      "requires": {
        "@rdfjs/to-ntriples": "1.0.2",
        "@tpluscode/rdf-ns-builders": "0.1.0"
      }
    },
    "rdf-validate-shacl": {
      "version": "0.2.4",
      "resolved": "https://registry.npmjs.org/rdf-validate-shacl/-/rdf-validate-shacl-0.2.4.tgz",
      "integrity": "sha512-Z/GDqf1/v0BiVIHOQKq6hSYj0lMxeA5oV2fDtjzO5PoSuT+8BLmqNCZCAf+DfhnGyI8GS+e6V4zym8M7P7NYwA==",
      "requires": {
        "@rdfjs/dataset": "^1.0.0",
        "@rdfjs/namespace": "^1.0.0",
        "@rdfjs/term-set": "^1.0.0",
        "clownface": "^1.0.0",
        "debug": "^4.0.0",
        "rdf-validate-datatype": "^0.1.1"
      }
    },
    "readable-stream": {
      "version": "3.6.0",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
      "requires": {
        "inherits": "^2.0.3",
        "string_decoder": "^1.1.1",
        "util-deprecate": "^1.0.1"
      }
    },
    "readable-to-readable": {
      "version": "0.1.3",
      "resolved": "https://registry.npmjs.org/readable-to-readable/-/readable-to-readable-0.1.3.tgz",
      "integrity": "sha512-G+0kz01xJM/uTuItKcqC73cifW8S6CZ7tp77NLN87lE5mrSU+GC8geoSAlfmp0NocmXckQ7W8s8ns73HYsIA3w==",
      "requires": {
        "readable-stream": "^3.6.0"
      }
    },
    "safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
    },
    "string_decoder": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
      "requires": {
        "safe-buffer": "~5.2.0"
      }
    },
    "util-deprecate": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
    }
  }
}

It seems to be linked to the function defined here: https://github.com/zazuko/rdf-validate-shacl/blob/master/src/rdflib-graph.js#L19

It finds in the example SoftwareShape for ToolShape but not AnalysisSoftwareShape.

Hey there,
Thanks for the report.

I'm not really sure if inheritance between shapes is defined by SHACL. Do you know anything about that @bergos @tpluscode ?

The example uses Implicit Class Targets, see https://www.w3.org/TR/shacl/#implicit-targetClass - the instance of AnalysisSoftwareShape will be validated against the AnalysisSoftwareShape class as shape. The rather cryptic terms "SHACL shape" and "SHACL instance" mean that rdfs:subClassOf triples must be walked up implicitly. Consider any instance of AnalysisSoftwareShape also an instance of SoftwareShape, and recursively of ToolShape. Given that any instance of a subclass is also an instance of the superclass(es), they are also targeted by the SHACL definitions of those superclasses.

So yes, it needs to walk up to the root class and apply all constraints.

BTW: the sh:targetClass statements in the example are unnecessary. The usual design pattern for that would be to have

ex:ToolShape
    a sh:NodeShape ;
    sh:targetClass ex:Tool .
    
ex:Tool
    a rdfs:Class .

ex:MyTool
    a ex:Tool .

in which case the ToolShape will apply to MyTool. The other main pattern is

ex:Tool
    a rdfs:Class, sh:NodeShape .

ex:MyTool
    a ex:Tool .

assuming all constraints are directly attached to ex:Tool.

Thanks Holger, that's very helpful. I didn't know about the "Implicit Class Targets" feature, which opens up a lot of possibilities :)

Fixed in #42