Using shape expressions to become interoperable just got a whole lot easier

I wrote a new tool called shex-codegen with which you can generate the code to query and mutate shapes. It can generate shape objects that have methods through which it is very easy to read, create or modify nodes of a certain shape.

If we would want to create a solid profile e.g. with this shape expression:

PREFIX srs: <https://shaperepo.com/schemas/solidProfile#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX schem: <http://schema.org/>
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

srs:SolidProfileShape EXTRA a {
  a [ schem:Person ]
    // rdfs:comment  "Declares the node to be a schema.org Person" ;
  a [ foaf:Person ]
    // rdfs:comment  "Declares the node to be a FOAF Person" ;
  vcard:hasPhoto IRI ?
    // rdfs:comment  "A link to the person's photo" ;
  foaf:name xsd:string ?
    // rdfs:comment  "An alternate way to define a person's name" ;
}

And with this config:

schema: "solidProfile.shex"
generates:
  node_modules/@generated/shex.ts:
    - typescript
    - typescript-methods

In this example the codegen will try to read the shex file and generate a new file to node_modules/@generated/shex.ts with roughly this content:

import { NamedNode, Literal } from "rdflib";
import { Shape } from "shex-methods";

export type SolidProfileShape = {
  id: string;
  hasPhoto?: string | NamedNode; // A link to the person's photo
  name?: string | Literal; // An alternate way to define a person's name
} & {
  type: (
    | SolidProfileShapeType.SchemPerson
    | SolidProfileShapeType.FoafPerson
  )[]; // Defines the node as a Person
};

export enum SolidProfileShapeType {
  SchemPerson = "http://schema.org/Person",
  FoafPerson = "http://xmlns.com/foaf/0.1/Person",
}

export enum SolidProfileShapeContext {
  "type" = "rdf:type",
  "name" = "foaf:name",
  "hasPhoto" = "vcard:hasPhoto",
}

export const solidProfile = new Shape<SolidProfileShape>({
  id: "https://shaperepo.com/schemas/solidProfile#SolidProfileShape",
  shape: solidProfileShex,
  context: SolidProfileShapeContext,
  type: SolidProfileShapeType,
});

You can then use the generated solidProfile instance to create a new node that fits the shape expression:

import { solidProfile } from "@generated/shex"
const newProfile = await solidProfile.create({
  doc: webIdDoc,
  data: {
    id: webId,
    type: [SolidProfileShapeType.SchemPerson],
    name: "Ludwig",
    hasPhoto: "https://avatars.githubusercontent.com/u/35169452?v=4",
  },
});
const { data, errors } = newProfile;

Authenticating can be done by updating the ._fetch method of the fetcher of a shape object. E.g. with solid-node-client:

const client = new SolidNodeClient();
await client.login(config);
solidProfile.fetcher._fetch = client.fetch.bind(client);

Feel free to try it and leave some feedback or check out the repo if you want to learn more.

Now developers just have to agree on the underlying shape expression/schema and then the cost to become interoperable goes really low :smiley:

10 Likes

Looks very promising. Can I use it to read objects from an rdflib store?

Yes for finding all nodes of a certain shape you can use the findAll() method or findOne() to read a specific node. For now it is implemented to fetch the doc that is passed as an arg, but i plan to also implement options for “local” or no-fetch operations.

At the moment it is also implemented in a way that it looks for nodes that match the rdf:type of the shape expression. So, to use findAll() without ids is only possible for shapes that have a specified type. I am currently still thinking of how to do it more generally because some shapes also use wdt:p31 (instance of - Wikidata) and others don’t have any type to pre-match on and validating all nodes is expensive at scale.

1 Like

Nice project. One small thing… the package.json says it is GPL-3.0. Might be best to make that explicit with a LICENSE file.

1 Like

Demo:

1 Like