This document explains how Active Data Shapes (ADS) can be used to declare SHACL constraint and constraint components. This makes it possible to use the full expressiveness and flexibility of JavaScript for constraint validation.

Scope of This Document

This document is part of the Active Data Shapes framework. We assume that you are familiar with the Active DASH Tutorial. You will also find it easier to follow if you are familiar with SHACL-SPARQL.

Getting Started

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.

This document introduces 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.

ex:AdultPerson
    a rdfs:Class, sh:NodeShape ;
    sh:property ex:AdultPerson-age .

ex:AdultPerson-age
    a sh:PropertyShape ;
    sh:path ex:age ;
    sh:datatype xsd:integer ;
    sh:maxCount 1 ;
    dash:scriptConstraint [
        a dash:ScriptConstraint ;
        sh:message "Age must be >= 18" ;
        dash:js "value >= 18" ;
    ] .

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 the last line.

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.

ex:Person
    a rdfs:Class, sh:NodeShape ;
    sh:property ex:Person-supervision .

ex:Person-supervision
    a sh:PropertyShape ;
    sh:path ex:supervision ;
    sh:class ex:Person ;
    dash:scriptConstraint [
        a dash:ScriptConstraint ;
        sh:message "At least one of the supervisors must be an adult" ;
        dash:onAllValues true ;
        dash:js "values.some(value => value.instanceOf(ex.AdultPerson))" ;
    ] .

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.

Example: Constraint component

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.

ex:IsJSONArrayConstraintComponent
  a sh:ConstraintComponent ;
  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)." ;
  rdfs:label "Is JSON array constraint component" ;
  sh:message "Value must be a JSON array: {$value}" ;
  sh:parameter ex:IsJSONArrayConstraintComponent-isJSONArray ;
  sh:validator ex:IsJSONArrayScriptValidator ;
.
ex:IsJSONArrayConstraintComponent-isJSONArray
  a sh:Parameter ;
  sh:path ex:isJSONArray ;
  sh:datatype xsd:boolean ;
  sh:description "True to activate the check that all values must be JSON arrays." ;
  sh:name "is JSONArray" ;
.
ex:IsJSONArrayScriptValidator
  a dash:ScriptValidator ;
  dash:js """if(isJSONArray) {
	let array = JSON.parse(value);
	Array.isArray(array);
}""" ;
  rdfs:label "IsJSONArray script validator" ;
.

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.

ex:TestShape
  a rdfs:Class ;
  a sh:NodeShape ;
  rdfs:label "Test shape" ;
  rdfs:subClassOf rdfs:Resource ;
  sh:property ex:TestShape-property ;
.
ex:TestShape-property
  a sh:PropertyShape ;
  sh:path ex:property ;
  ex:isJSONArray true ;
  sh:datatype xsd:string ;
  sh:name "property" ;
.

Here is a valid instance from a data graph:

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