Skip to main content

Shapes

About shapes

LINCD Introduces Shape classes, which are modeled after Shapes from the SHACL ontology..

To understand Shape classes, you first must understand the basics of SHACL Shapes:

SHACL Shapes

SHACL is an ontology for validating linked data against a set of conditions. These conditions are defined as shapes, which are themselves expressed with linked data.

Each SHACL shape defines a certain set of conditions for the data in the graph.

A shapes' instances are all the nodes that match the defined shape.

Shapes, amongst other things, can state that their instance nodes:

  • need to have a certain rdf:type
  • can only have a certain amount of values for a certain property (e.g. an instance node must have a rdfs:label defined, but it cannot have two rdfs:label values)
  • property values must have a certain type (e.g. the values of foaf:knows must be of type foaf:Person)

An example

ex:PersonShape
a sh:NodeShape ;
sh:targetClass foaf:Person ;
sh:property [
sh:path foaf:name ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:datatype xsd:string ;
];

(written in TURTLE notation)

this ex:PersonShape states that it's instances must have:

  • rdf:type foaf:Person
  • exactly one edge with the property foaf:name, and the value must be a xsd:string.

A valid instance of this shape would be:

ex:Alice
a ex:Person ;
foaf:name "Alice" .

Connection to LINCD.js

SHACL is the best data validation tool available in the world of linked data. As such it is a cornerstones of LINCD: SHACL Shapes are the glue that helps us link data to code.

LINCD creates a SHACL Shape for each component, that expresses exactly which data the component requires.

These shapes can be used in two ways.

  1. If we start with data, we can see which components are available for that data
  2. If we start with a component, we know exactly how we need to shape or map our data in order to use it.

LINCD supports both of these use cases.

To generate these SHACL shapes, LINCD uses Shape Classes:

Linked Shapes

LINCD.js introduces the Shape class. By extending this class you can define a SHACL shape in typescript and simultaneously make it easily accessible by other code.

This is best explained with an example:

@linkedShape
export class Person extends Shape {
/**
* indicates that instances of this shape need to have this rdf.type
*/
static targetClass: NamedNode = foaf.Person;

/**
* instances of this shape need to have exactly one foaf.name property
*/
@literalProperty({
path: foaf.name,
required: true,
maxCount: 1,
})
get name() {
return this.getValue(foaf.name);
}

set name(val: string) {
this.overwrite(foaf.name, new Literal(val));
}
}

For this Person class, LINCD.js generates a SHACL shape under the hood (in Linked Data) that matches exactly the ex:PersonShape defined earlier on this page.

Again, the generated SHACL shape of this example states that for a node to match this shape, it must:

  • have rdf:type foaf:Person
  • have exactly one value for foaf.name and the value must be a xsd:string

LINCD Shapes can use several decorators and static properties to define data bindings and restrictions:

Decorators

@linkedShape

A Class decorator. Indicates that this class defines a shape. Creates the SHACL shape for this class.

See linkedShape for the full definition

Property decorators

See ShapeDecorators for a full list of decorators, including examples.

Some main decorators are:

Static properties

Classes that extend the Shape class can define several restrictions of their shape with the static properties.

See their definitions here:

Using shapes

Creating an instance of the shape class

First import your shape. In our example this would be:

import {Person} from 'lincd-foaf/lib/shapes/Person';

Instance of a new node

When creating a new NamedNode you will also have to manually set it's rdf:type.

let personNode = NamedNode.create();
personNode.set(rdf.type, foaf.Person);

Instead when using shapes, you can simply directly create a new instance of a shape with its constructor.

let person = new Person();

this will automatically create a new NamedNode and set it's rdf:type to the targetType of the Shape, which in this case is foaf:Person.

Instance of an existing node

For existing nodes you can simply pass the node to the constructor:

let person = new Person(node);

or

let person = node.getAs(Person);

Using accessors

Once you have an instance of a shape class, you can simply use its property accessors to read or update the graph:

Setting properties

When using NamedNodes you will have to manually set exactly the right properties.

personNode.set(foaf.name, new Literal('Josh', xsd.string));

With shapes you can simply use the accessors (getters/setters).

person.name = 'Josh';

This will automatically set the right property for you (foaf:name) and create a Literal with the right data type (xsd:string)

This simplifies your code, and also it means you no longer need to know which property (like foaf:name) to use. The shape handles that for you.

Does the shape match your use case?

Shapes are made for specific use cases. They likely provide the functionality that certain packages/components need. If you're using the components they are made for you can use them as is. But if you want to reuse a shape in a different context, you have to see if it matches your needs.

Shapes are not exhaustive. That is, they may not provide a get/set method for each relevant property that you may need.

Therefor it is totally fine to have multiple shape classes for the same targetType. And if the existing shapes do not match your use case, you can either extend one or create an entirely new shape of your own.

Creating a new shape class

Go to the root folder of your module in your terminal and type

yarn lincd create-shape [name] [targetClass]

where name will be the name of the Shape and targetClass can be a prefixed URI that is defined in your module, in our example it would be:

yarn lincd create-shape Person

this will create the following template

import {Shape} from 'lincd/lib/shapes/Shape';
import {NamedNode} from 'lincd/lib/models';
import {linkedShape} from '../module';

@linkedShape
export class NameOfYourShape extends Shape {
targetType: NamedNode;
}