Using ADS with SHACL

Constraint Validation

While the SHACL Core language defines built-in constraint properties such as sh:minCount, SHACL is also an extensible language, allowing extension languages such as SPARQL to declare constraints that go beyond the Core.

In this section we introduce how the Active Data Shapes framework can be used as one such extension mechanism, allowing SHACL users to declare constraints and also constraint components, using JavaScript. JavaScript offers significantly greater expressiveness than, for example, SPARQL, allowing the representation of very complex conditions and taking better control over the execution speed.

Script Constraints

The property dash:scriptConstraint links a shape with instances of dash:ScriptConstraint. This is similar to how sh:sparql links a shape with instances of sh:SPARQLConstraint. Each dash:ScriptConstraint must have a value for dash:js to represent the JavaScript expression that shall be evaluated. It may also use sh:message to declare which validation messages shall be produced. The optional property dash:onAllValues may be used to control what variables can be used by the script, as explained in the following examples.

Example: Validating an individual value

This example illustrates the most simple form of script-based constraints: a single JavaScript expression that gets executed for each individual value of a property. Here we have a class ex:AdultPerson with a property ex:age that must not have values smaller than 18.

 1ex:AdultPerson
 2    a rdfs:Class, sh:NodeShape ;
 3    sh:property ex:AdultPerson-age .
 4
 5ex:AdultPerson-age
 6    a sh:PropertyShape ;
 7    sh:path ex:age ;
 8    sh:datatype xsd:integer ;
 9    sh:maxCount 1 ;
10    dash:scriptConstraint [
11        a dash:ScriptConstraint ;
12        sh:message "Age must be >= 18" ;
13        dash:js "value >= 18" ;
14    ] .

Such constraints will be executed for each value node of the property. In each script execution, the variable value will point at the actual property value. In this case, assuming that the value(s) of ex:age are xsd:integer literals, the value will be a JavaScript number. As usual with ADS, xsd:string literals are represented as JavaScript strings, while xsd:boolean literals become true or false in JavaScript. Literals of other datatypes become instances of LiteralNode, and NamedNode is used to represent URIs or blank nodes.

The dash:js script can also access the current focus node using the variable focusNode as an instance of NamedNode, if needed.

The dash:js expression will be evaluated against the current data graph. As usual in JavaScript, such expressions may consist of multiple lines, and the final result of the expression will be the result of its last line.

  • If the result is a string then the validation will produce a validation result with the given string as sh:resultMessage.

  • If the result is true then the validation passes OK without violations.

  • If the result is false the validation will produce a validation result that uses the provided sh:message or a generic fallback message. The values of sh:message can access the values {$focusNode}, {$value} etc as template variables.

  • If the result an object, a validation result will be produced using the value of the field message of the object as result message. If the field value has a value then this will become the sh:value in the violation.

Alternatively, the script may return an array then each array member will be mapped to one validation result, following the mapping rules above.

Example: Validating all values at once

In some scenarios, constraints need to look at all value nodes at once, for example to count the number of available values. This mode is activated if dash:onAllValues is true, and the JavaScript variable values will then be an array of the value nodes.

In this example we verify that at least one of the values of the ex:supervision property of a person is an adult.

 1ex:Person
 2    a rdfs:Class, sh:NodeShape ;
 3    sh:property ex:Person-supervision .
 4
 5ex:Person-supervision
 6    a sh:PropertyShape ;
 7    sh:path ex:supervision ;
 8    sh:class ex:Person ;
 9    dash:scriptConstraint [
10        a dash:ScriptConstraint ;
11        sh:message "At least one of the supervisors must be an adult" ;
12        dash:onAllValues true ;
13        dash:js "values.some(value => value.instanceOf(ex.AdultPerson))" ;
14    ] .

Script Constraint Components

SHACL constraint components are reusable building blocks that declare property types that others may use. Similar to SPARQL-based Constraint Components, ADS makes it possible to declare new constraint components using JavaScript.

Create an instance of sh:ConstraintComponent to get started, and declare the parameters as usual in SHACL. Then declare a dash:ScriptValidator as value of sh:validator and store the ADS script that shall be executed using dash:js at the validator. Within the JavaScript code, the same variables may be used as for constraints, and dash:onAllValues has the same meaning. In addition, the parameters will be mapped to JavaScript variables that have the same name as the local name of the parameter. This is similar to how SPARQL-based constraint components work.

This is best explained using an example.

This example declares a SHACL constraint component based on a parameter ex:isJSONArray which can be used to validate that all values of a given property can be parsed into JSON arrays.

 1ex:IsJSONArrayConstraintComponent
 2  a sh:ConstraintComponent ;
 3  rdfs:comment "As an example of a Script-based constraint component, this can be used to verify that all value nodes are valid JSON arrays (encoded as strings)." ;
 4  rdfs:label "Is JSON array constraint component" ;
 5  sh:message "Value must be a JSON array: {$value}" ;
 6  sh:parameter ex:IsJSONArrayConstraintComponent-isJSONArray ;
 7  sh:validator ex:IsJSONArrayScriptValidator ;
 8.
 9ex:IsJSONArrayConstraintComponent-isJSONArray
10  a sh:Parameter ;
11  sh:path ex:isJSONArray ;
12  sh:datatype xsd:boolean ;
13  sh:description "True to activate the check that all values must be JSON arrays." ;
14  sh:name "is JSONArray" ;
15.
16ex:IsJSONArrayScriptValidator
17  a dash:ScriptValidator ;
18  rdfs:label "IsJSONArray script validator" ;
19  dash:js """
20    if(isJSONArray) {
21      let array = JSON.parse(value);
22      Array.isArray(array);
23    }""" ;
24.

Note that the dash:js script queries the value of isJSONArray which will be a boolean that is automatically set to the correct value by the engine.

Once declared, the new property can be used as in the following example shapes graph. Here we have set ex:isJSONArray to true which will trigger the execution of the script validator, which will verify that the value is true before proceeding with the actual check.

 1ex:TestShape
 2  a rdfs:Class ;
 3  a sh:NodeShape ;
 4  rdfs:label "Test shape" ;
 5  rdfs:subClassOf rdfs:Resource ;
 6  sh:property ex:TestShape-property ;
 7.
 8ex:TestShape-property
 9  a sh:PropertyShape ;
10  sh:path ex:property ;
11  ex:isJSONArray true ;
12  sh:datatype xsd:string ;
13  sh:name "property" ;
14.

Here is a valid instance from a data graph:

1ex:ValidInstance
2  a ex:TestShape ;
3  ex:property "[1, 2, 3]" ;
4  ex:property "[]" ;
5  rdfs:label "Valid instance" ;
6.

Inferring Values

The SHACL Advanced Features include a mechanism to dynamically compute (infer) values of certain properties, based on the sh:values property. Active Data Shapes technology introduces a new kind of SHACL Node Expression based on scripts that makes it possible to dynamically compute property values from JavaScript expressions. These node expressions are blank nodes that have the script as value of dash:js. In the script, you can use the variable focusNode to reference the current context node.

In the following example, we introduce a new derived property skos:narrowerConceptCount which is an integer computed as the number of narrower concepts.

 1skos:Concept-narrowerConceptCount
 2  a sh:PropertyShape ;
 3  sh:path skos:narrowerConceptCount ;
 4  sh:datatype xsd:integer ;
 5  sh:description "The number of narrower concepts of this." ;
 6  sh:group skos:HierarchicalRelationships ;
 7  sh:maxCount 1 ;
 8  sh:name "narrower concept count" ;
 9  sh:order "10"^^xsd:decimal ;
10  sh:values [
11      dash:js "focusNode.narrower.length" ;
12    ] .
13
14skos:Concept sh:property skos:Concept-narrowerConceptCount .

With the new property defined as above, forms of SKOS concepts will now show the count:

Form panel with values that are inferred by an ADS script

ADS scripts can be used to infer/derive new property values such as the narrower concept count above

The dash:js expression may also return an array, in which case multiple values would be inferred.

Warning

Make sure to use this (powerful) feature with care, as it might have a performance impact if the system needs to repeatedly fire up a Script Runtime for each individual value on a form. Performance should however, be good as long as you call your inference as part of some other script. In the above case, you could now query the new property like any other property of the instance:

Script Editor panel with an example call to an inferred property value

Inferred property values such as the narrower concept count can be queried like any other property

As usual, inferred properties cannot be directly modified, i.e. you cannot assign them. Change the underlying values (here: skos:broader) instead.

Pro tip: if you want the Form panel and other parts of the UI update automatically after changes to the asserted properties annotate your node expression with dash:dependencyPredicate.

1skos:Concept-narrowerConceptCount
2  ...
3  sh:values [
4      dash:js "focusNode.narrower.length" ;
5      dash:dependencyPredicate skos:broader ;
6    ] .

The above will mean that the counter will automatically update whenever some skos:broader triple has been changed (we are using skos:broader as the sh:inversePath in the computation of the narrower property).

Current limitations: these script-based inferred values only work within graphs that are under teamwork control, i.e. you can only query them within EDG asset collections, not from other (file based) graphs in your installation. Furthermore, make sure that the focus node only has a single type, so that it is able to pick the right JavaScript class for the focusNode variable.