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 twordfs:label
values) - property values must have a certain type (e.g. the values of
foaf:knows
must be of typefoaf: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 axsd: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.
- If we start with data, we can see which components are available for that data
- 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 axsd: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:
- targetNode
- (more to come)
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;
}