Added schema-converter-shex

main
jaxoncreed 2 years ago
parent 3961705934
commit 02de7a1757
  1. 722
      package-lock.json
  2. 3
      package.json
  3. 5
      packages/cli/package.json
  4. 4
      packages/cli/src/build.ts
  5. 367
      packages/ldo/README.md
  6. 2
      packages/ldo/src/LdoDataset.ts
  7. 168
      packages/ldo/test/LdoDataset.test.ts
  8. 151
      packages/ldo/test/methods.test.ts
  9. 1112
      packages/ldo/test/profileData.ts
  10. 16
      packages/ldo/test/sampleData.ts
  11. 3
      packages/schema-converter-shex/.eslintrc
  12. 120
      packages/schema-converter-shex/README.md
  13. 5
      packages/schema-converter-shex/jest.config.js
  14. 40
      packages/schema-converter-shex/package.json
  15. 182
      packages/schema-converter-shex/src/context/JsonLdContextBuilder.ts
  16. 83
      packages/schema-converter-shex/src/context/ShexJContextVisitor.ts
  17. 21
      packages/schema-converter-shex/src/context/shexjToContext.ts
  18. 3
      packages/schema-converter-shex/src/index.ts
  19. 5
      packages/schema-converter-shex/src/typing/ShapeInterfaceDeclaration.ts
  20. 355
      packages/schema-converter-shex/src/typing/ShexJTypingTransformer.ts
  21. 59
      packages/schema-converter-shex/src/typing/shexjToTyping.ts
  22. 16
      packages/schema-converter-shex/test/context.test.ts
  23. 1160
      packages/schema-converter-shex/test/testData/activityPub.ts
  24. 42
      packages/schema-converter-shex/test/testData/circular.ts
  25. 43
      packages/schema-converter-shex/test/testData/extendsSimple.ts
  26. 48
      packages/schema-converter-shex/test/testData/oldExtends.ts
  27. 506
      packages/schema-converter-shex/test/testData/profile.ts
  28. 81
      packages/schema-converter-shex/test/testData/reducedProfile.ts
  29. 60
      packages/schema-converter-shex/test/testData/reusedPredicates.ts
  30. 50
      packages/schema-converter-shex/test/testData/simple.ts
  31. 29
      packages/schema-converter-shex/test/testData/testData.ts
  32. 16
      packages/schema-converter-shex/test/typing.test.ts
  33. 7
      packages/schema-converter-shex/tsconfig.build.json

722
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -8,7 +8,8 @@
"test": "lerna run test",
"build": "lerna run build",
"demo-react": "lerna run start --scope @ldo/demo-react",
"lint": "lerna run lint"
"lint": "lerna run lint",
"postinstall": "npm run build"
},
"devDependencies": {
"@types/node": "^20.5.7",

@ -19,7 +19,6 @@
"test": "jest --coverage",
"test:watch": "jest --watch",
"prepublishOnly": "npm run test && npm run build",
"postinstall": "npm run build",
"lint": "eslint src/** --fix --no-error-on-unmatched-pattern"
},
"repository": {
@ -45,11 +44,11 @@
},
"dependencies": {
"@shexjs/parser": "^1.0.0-alpha.24",
"@ldo/schema-converter-shex": "^0.0.0",
"child-process-promise": "^2.2.1",
"commander": "^9.3.0",
"ejs": "^3.1.8",
"fs-extra": "^10.1.0",
"loading-cli": "^1.1.0",
"shexj2typeandcontext": "^2.0.0"
"loading-cli": "^1.1.0"
}
}

@ -2,7 +2,7 @@ import fs from "fs-extra";
import path from "path";
import type { Schema } from "shexj";
import parser from "@shexjs/parser";
import shexjToTypeAndContext from "shexj2typeandcontext";
import schemaConverterShex from "@ldo/schema-converter-shex";
import { renderFile } from "ejs";
import prettier from "prettier";
import loading from "loading-cli";
@ -42,7 +42,7 @@ export async function build(options: BuildOptions) {
.construct("https://ldo.js.org/")
.parse(shexC);
// Convert the content to types
const [typings, context] = await shexjToTypeAndContext(schema);
const [typings, context] = await schemaConverterShex(schema);
await Promise.all(
["context", "schema", "shapeTypes", "typings"].map(
async (templateName) => {

@ -0,0 +1,367 @@
# LDO (Linked Data Objects)
LDO (Linked Data Objects) is a library that lets you easily manipulate RDF as if it were a standard TypeScript object that follows a [ShEx](https://shex.io) shape you define.
For a full tutorial of using LDO to build React Solid applications, see [this tutorial](https://medium.com/@JacksonMorgan/building-solid-apps-with-ldo-6127a5a1979c).
## Setup
### Automatic Setup
To setup LDO, `cd` into your typescript project and run `npx ldo-cli init`.
```bash
cd my-typescript-project
npx ldo-cli init
```
### Manual Setup
The following is handled by the __automatic setup__:
Install the LDO dependencies.
```bash
npm install ldo
npm install ldo-cli --save-dev
```
Create a folder to store your ShEx shapes:
```bash
mkdir shapes
```
Create a script to build ShEx shapes and convert them into Linked Data Objects. You can put this script in `package.json`
```json
{
...
scripts: {
...
"build:ldo": "ldo build --input ./shapes --output ./ldo"
...
}
...
}
```
## Creating ShEx Schemas
LDO uses [ShEx](https://shex.io) as a schema for the RDF data in your project. To add a ShEx schema to your project, simply create a file ending in `.shex` to the `shapes` folder.
For more information on writing ShEx schemas see the [ShEx Primer](http://shex.io/shex-primer/index.html).
`./shapes/foafProfile.shex`:
```shex
PREFIX ex: <https://example.com/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:FoafProfile EXTRA a {
a [ foaf:Person ]
// rdfs:comment "Defines the node as a Person (from foaf)" ;
foaf:name xsd:string ?
// rdfs:comment "Define a person's name." ;
foaf:img xsd:string ?
// rdfs:comment "Photo link but in string form" ;
foaf:knows @ex:FoafProfile *
// rdfs:comment "A list of WebIds for all the people this user knows." ;
}
```
To build the shape, run:
```bash
npm run build:ldo
```
This will generate five files:
- `./ldo/foafProfile.shapeTypes.ts` <-- This is the important file
- `./ldo/foafProfile.typings.ts`
- `./ldo/foafProfile.schema.ts`
- `./ldo/foafProfile.context.ts`
## Simple Example
Below is a simple example of LDO in a real use-case (changing the name on a Solid Pod)
```typescript
import { parseRdf, startTransaction, toSparqlUpdate, toTurtle } from "ldo";
import { FoafProfileShapeType } from "./ldo/foafProfile.shapeTypes";
async function run() {
const rawTurtle = `
<#me> a <http://xmlns.com/foaf/0.1/Person>;
<http://xmlns.com/foaf/0.1/name> "Jane Doe".
`;
/**
* Step 1: Convert Raw RDF into a Linked Data Object
*/
const ldoDataset = await parseRdf(rawTurtle, {
baseIRI: "https://solidweb.me/jane_doe/profile/card",
});
// Create a linked data object by telling the dataset the type and subject of
// the object
const janeProfile = ldoDataset
// Tells the LDO dataset that we're looking for a FoafProfile
.usingType(FoafProfileShapeType)
// Says the subject of the FoafProfile
.fromSubject("https://solidweb.me/jane_doe/profile/card#me");
/**
* Step 2: Manipulate the Linked Data Object
*/
// Logs "Jane Doe"
console.log(janeProfile.name);
// Logs "Person"
console.log(janeProfile.type);
// Logs 0
console.log(janeProfile.knows?.length);
// Begins a transaction that tracks your changes
startTransaction(janeProfile);
janeProfile.name = "Jane Smith";
janeProfile.knows?.push({
"@id": "https://solidweb.me/john_smith/profile/card#me",
type: {
"@id": "Person",
},
name: "John Smith",
knows: [janeProfile],
});
// Logs "Jane Smith"
console.log(janeProfile.name);
// Logs "John Smith"
console.log(janeProfile.knows?.[0].name);
// Logs "Jane Smith"
console.log(janeProfile.knows?.[0].knows?.[0].name);
/**
* Step 3: Convert it back to RDF
*/
// Logs:
// <https://solidweb.me/jane_doe/profile/card#me> a <http://xmlns.com/foaf/0.1/Person>;
// <http://xmlns.com/foaf/0.1/name> "Jane Smith";
// <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/john_smith/profile/card#me>.
// <https://solidweb.me/john_smith/profile/card#me> a <http://xmlns.com/foaf/0.1/Person>;
// <http://xmlns.com/foaf/0.1/name> "John Smith";
// <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/jane_doe/profile/card#me>.
console.log(await toTurtle(janeProfile));
// Logs:
// DELETE DATA {
// <https://solidweb.me/jane_doe/profile/card#me> <http://xmlns.com/foaf/0.1/name> "Jane Doe" .
// };
// INSERT DATA {
// <https://solidweb.me/jane_doe/profile/card#me> <http://xmlns.com/foaf/0.1/name> "Jane Smith" .
// <https://solidweb.me/jane_doe/profile/card#me> <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/john_smith/profile/card#me> .
// <https://solidweb.me/john_smith/profile/card#me> <http://xmlns.com/foaf/0.1/name> "John Smith" .
// <https://solidweb.me/john_smith/profile/card#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .
// <https://solidweb.me/john_smith/profile/card#me> <http://xmlns.com/foaf/0.1/knows> <https://solidweb.me/jane_doe/profile/card#me> .
// }
console.log(await toSparqlUpdate(janeProfile));
}
run();
```
## Getting an LDO Dataset
An LDO Dataset is a kind of [RDF JS Dataset](https://rdf.js.org/dataset-spec/) that can create linked data objects.
LDO datasets can be created in two ways:
`createLdoDataset(initialDataset?: Dataset<Quad, Quad> | Quad[])`
```typescript
import { createLdoDataset } from "ldo";
const ldoDataset = createLdoDataset();
```
- `initialDataset`: An optional dataset or array of quads for the new dataset.
`parseRdf(data: string, parserOptions?: ParserOptions)`
```typescript
import { parseRdf } from "ldo";
const rawTurtle = "...";
const ldoDataset = parseRdf(rawTurtle, { baseIRI: "https://example.com/" });
```
- `data`: The raw data to parse as a `string`.
- `options` (optional): Parse options containing the following keys:
- `format` (optional): The format the data is in. The following are acceptable formats: `Turtle`, `TriG`, `N-Triples`, `N-Quads`, `N3`, `Notation3`.
- `baseIRI` (optional): If this data is hosted at a specific location, you can provide the baseIRI of that location.
- `blankNodePrefix` (optional): If blank nodes should have a prefix, that should be provided here.
- `factory` (optional): a RDF Data Factory from [`@rdfjs/data-model`](https://www.npmjs.com/package/@rdfjs/data-model).
## Getting a Linked Data Object
Once you have an LdoDataset we can get a Linked Data Object. A linked data object feels just like a JavaScript object literal, but when you make modifications to it, it will affect the underlying LdoDataset.
Thie first step is defining which Shape Type you want to retrieve from the dataset. We can use the generated shape types and the `usingType()` method for this.
```typescript
import { FoafProfileShapeType } from "./ldo/foafProfile.shapeTypes.ts";
// ... Get the LdoDataset
ldoDataset.usingType(FoafProfileShapeType);
```
Next, we want to identify exactly what part of the dataset we want to extract. We can do this in a few ways:
### `.fromSubject(entryNode)`
`fromSubject` lets you define a an `entryNode`, the place of entry for the graph. The object returned by `jsonldDatasetProxy` will represent the given node. This parameter accepts both `namedNode`s and `blankNode`s. `fromSubject` takes a generic type representing the typescript type of the given subject.
```typescript
const profile = ldoDataset
.usingType(FoafProfileShapeType)
.fromSubject("http://example.com/Person1");
```
### `.matchSubject(predicate?, object?, graph?)`
`matchSubject` returns a Jsonld Dataset Proxy representing all subjects in the dataset matching the given predicate, object, and graph.
```typescript
const profiles = ldoDataset
.usingType(FoafProfileShapeType)
.matchSubject(
namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
namedNode("http://xmlns.com/foaf/0.1/Person")
);
profiles.forEach((person) => {
console.log(person.fn);
});
```
### `.matchObject(subject?, predicate?, object?)`
`matchObject` returns a Jsonld Dataset Proxy representing all objects in the dataset matching the given subject, predicate, and graph.
```typescript
const friendsOfPerson1 = ldoDataset
.usingType(FoafProfileShapeType)
.matchSubject(
namedNode("http://example.com/Person1"),
namedNode("http://xmlns.com/foaf/0.1/knows")
);
friendsOfPerson1.forEach((person) => {
console.log(person.fn);
});
```
### `.fromJson(inputData)`
`fromJson` will take any regular Json, add the information to the dataset, and return a Jsonld Dataset Proxy representing the given data.
```typescript
const person2 = ldoDataset
.usingType(FoafProfileShapeType)
.fromJson({
"@id": "http://example.com/Person2",
fn: ["Jane Doe"],
});
```
## Getting and Setting Data on a Linked Data Object
Once you've created a Linked Data Object, you can get and set data as if it were a normal TypeScript Object. For specific details, see the documentation at [JSONLD Dataset Proxy](https://github.com/o-development/jsonld-dataset-proxy/blob/master/Readme.md).
```typescript
import { LinkedDataObject } from "ldo";
import { FoafProfileFactory } from "./ldo/foafProfile.ldoFactory.ts";
import { FoafProfile } from "./ldo/foafProfile.typings";
aysnc function start() {
const profile: FoafProfile = // Create LDO
// Logs "Aang"
console.log(profile.name);
// Logs "Person"
console.log(profile.type);
// Logs 1
console.log(profile.knows?.length);
// Logs "Katara"
console.log(profile.knows?.[0].name);
profile.name = "Bonzu Pippinpaddleopsicopolis III"
// Logs "Bonzu Pippinpaddleopsicopolis III"
console.log(profile.name);
profile.knows?.push({
type: "Person",
name: "Sokka"
});
// Logs 2
console.log(profile.knows?.length);
// Logs "Katara" and "Sokka"
profile.knows?.forEach((person) => console.log(person.name));
}
```
## Converting a Linked Data Object back to RDF
A linked data object can be converted into RDF in multiple ways:
### `toTurtle(linkedDataObject)`
```typescript
import { toTurtle } from "ldo"
// ...
const rawTurtle: string = await toTurtle(profile);
```
### `toNTiples(linkedDataObject)`
```typescript
import { toNTriples } from "ldo"
// ...
const rawNTriples: string = await toNTriples(profile);
```
### `serialize(linkedDataObject, options)`
```typescript
const rawTurtle: string = await profile.$serialize({
format: "Turtle",
prefixes: {
ex: "https://example.com/",
foaf: "http://xmlns.com/foaf/0.1/",
}
});
```
`serialize(linkedDataObject, options)` provides general serialization based on provided options:
- `foramt` (optional): The format to serialize to. The following are acceptable formats: `Turtle`, `TriG`, `N-Triples`, `N-Quads`, `N3`, `Notation3`.
- `prefixes`: The prefixes for those serializations that use prefixes.
## Transactions
Sometimes, you want to keep track of changes you make for the object. This is where transactions come in handy.
To start a transaction, use the `startTransaction(linkedDataObject)` function. From then on, all transactions will be tracked, but not added to the original ldoDataset. You can view the changes using the `transactionChanges(linkedDataObject)` or `toSparqlUpdate(linkedDataObject)` methods. When you're done with the transaction, you can run the `commitTransaction(linkedDataObject)` method to add the changes to the original ldoDataset.
```typescript
import {
startTransaction,
transactionChanges,
toSparqlUpdate,
commitTransaction,
} from "ldo";
// ... Get the profile linked data object
startTransaction(profile);
profile.name = "Kuzon"
const changes = transactionChanges(profile));
// Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Kuzon"
console.log(changes.added?.toString())
// Logs: <https://example.com/aang> <http://xmlns.com/foaf/0.1/name> "Aang"
console.log(changes.removed?.toString())
console.log(await toSparqlUpdate(profile));
commitTransaction(profile);
```
## Other LDO Helper Functions
### `getDataset(linkedDataObject)`
Returns the Linked Data Object's underlying RDFJS dataset. Modifying this dataset will change the Linked Data Object as well.
```typescript
import { Dataset } from "@rdfjs/types";
import { getDataset } from "ldo"
const dataset: Dataset = dataset(profile);
```
## Sponsorship
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/).
[<img src="https://nlnet.nl/logo/banner.png" alt="nlnet foundation logo" width="300" />](https://nlnet.nl/)
[<img src="https://nlnet.nl/image/logos/NGI0Entrust_tag.svg" alt="NGI Zero Entrust Logo" width="300" />](https://nlnet.nl/)
## Liscense
MIT

@ -3,7 +3,7 @@ import jsonldDatasetProxy from "jsonld-dataset-proxy";
import { WrapperSubscribableDataset } from "o-dataset-pack";
import { LdoBuilder } from "./LdoBuilder";
import type { ShapeType } from "./ShapeType";
import type { LdoBase } from "./util";
import type { LdoBase } from "./index";
/**
* Utility for building a linked data object

@ -0,0 +1,168 @@
import { literal, namedNode, quad } from "@rdfjs/data-model";
import { createDataset } from "o-dataset-pack";
import type { SolidProfileShape } from "./profileData";
import { ProfileShapeType } from "./profileData";
import type { LdoBuilder, LdoDataset } from "../src";
import { createLdoDataset, graphOf, parseRdf, toTurtle } from "../src";
import { sampleJsonld, sampleTurtle } from "./sampleData";
import type { SubjectProxy } from "jsonld-dataset-proxy";
import { _proxyContext } from "jsonld-dataset-proxy";
describe("LdoDataset", () => {
let ldoDataset: LdoDataset;
let profileBuilder: LdoBuilder<SolidProfileShape>;
beforeEach(async () => {
ldoDataset = await parseRdf(sampleTurtle, {
baseIRI: "https://solidweb.org/jackson/profile/card",
});
profileBuilder = ldoDataset.usingType(ProfileShapeType);
});
it("Creates a blank profile", async () => {
ldoDataset = createLdoDataset();
profileBuilder = ldoDataset.usingType(ProfileShapeType);
const profile = profileBuilder.fromSubject(
"https://example.com/person1#me",
);
profile.fn = "Diplo";
expect(await toTurtle(profile)).toBe(
'<https://example.com/person1#me> <http://www.w3.org/2006/vcard/ns#fn> "Diplo".\n',
);
});
it("initializes a profile using the fromJson method", () => {
const profile = profileBuilder.fromJson({
type: [{ "@id": "Person" }, { "@id": "Person2" }],
inbox: { "@id": "https://inbox.com" },
fn: "Diplo",
});
expect(profile.inbox).toEqual({ "@id": "https://inbox.com" });
expect(profile.fn).toBe("Diplo");
expect(profile["@id"]).toBe(undefined);
});
it("initializes a profile with an id using the fromJson method", () => {
const profile = profileBuilder.fromJson({
"@id": "https://example.com/person1",
type: [{ "@id": "Person" }, { "@id": "Person2" }],
inbox: { "@id": "https://inbox.com" },
fn: "Diplo",
});
expect(profile.inbox).toEqual({ "@id": "https://inbox.com" });
expect(profile.fn).toBe("Diplo");
expect(profile["@id"]).toBe("https://example.com/person1");
});
it("retrieves a subject with a named node", async () => {
const profile = await profileBuilder.fromSubject(
namedNode("https://solidweb.org/jackson/profile/card#me"),
);
expect(profile.fn).toBe("Jackson Morgan");
});
it("retrieves a subject with a string id", async () => {
const profile = profileBuilder.fromSubject(
"https://solidweb.org/jackson/profile/card#me",
);
expect(profile.fn).toBe("Jackson Morgan");
});
it("uses an existing dataset as the basis for the ldo", async () => {
const dataset = createDataset();
dataset.add(
quad(
namedNode("https://example.com/person1"),
namedNode("http://xmlns.com/foaf/0.1/name"),
literal("Captain cool"),
),
);
const profile = createLdoDataset(dataset)
.usingType(ProfileShapeType)
.fromSubject("https://example.com/person1");
expect(profile.name).toBe("Captain cool");
});
it("uses an existing array of quads as the basis for the ldo", async () => {
const quads = [
quad(
namedNode("https://example.com/person1"),
namedNode("http://xmlns.com/foaf/0.1/name"),
literal("Captain cool"),
),
];
const profile = createLdoDataset(quads)
.usingType(ProfileShapeType)
.fromSubject("https://example.com/person1");
expect(profile.name).toBe("Captain cool");
});
it("parses JsonLD", async () => {
await expect(async () => parseRdf(sampleJsonld)).rejects.toThrow(
"Not Implemented",
);
// ldoDataset = await parseRdf(sampleJsonld);
// const profile = ldoDataset
// .usingType(ProfileShapeType)
// .fromSubject("https://example.com/item");
// expect(profile.name).toBe("Captain of Coolness");
});
it("parses an existing dataset", async () => {
const ldoDataset = await parseRdf(createDataset());
expect(typeof ldoDataset.usingType).toBe("function");
});
it("Sets the proper write graphs", () => {
const profile = profileBuilder
.write("https://example.com/exampleGraph")
.fromSubject("https://example.com/person1");
profile.name = "Jackson";
expect(graphOf(profile, "name")[0].value).toBe(
"https://example.com/exampleGraph",
);
});
it("Lets a match query retrieve subjects", () => {
const profiles = profileBuilder.matchSubject(
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"http://xmlns.com/foaf/0.1/Person",
);
expect(profiles[0].fn).toBe("Jackson Morgan");
});
it("Handles alternate optionality for subject match", () => {
const profiles = profileBuilder.matchSubject(
undefined,
undefined,
"https://someGraph.com",
);
expect(profiles.length).toBe(0);
});
it("Lets a match query retrieve objects", () => {
const profiles = profileBuilder.matchObject(
null,
"http://xmlns.com/foaf/0.1/primaryTopic",
);
expect(profiles[0].fn).toBe("Jackson Morgan");
});
it("Handles alternate optionality for object match", () => {
const profiles = profileBuilder.matchObject(
"https://someSubject",
undefined,
"https://someGraph.com",
);
expect(profiles.length).toBe(0);
});
it("Sets language preferences", () => {
const profile = profileBuilder
.setLanguagePreferences("@none", "en")
.fromSubject("https://solidweb.org/jackson/profile/card#me");
expect(
(profile as unknown as SubjectProxy)[_proxyContext].languageOrdering,
).toEqual(["@none", "en"]);
});
});

@ -0,0 +1,151 @@
import { namedNode } from "@rdfjs/data-model";
import type { SubjectProxy } from "jsonld-dataset-proxy";
import {
getProxyFromObject,
graphOf,
_getUnderlyingDataset,
_proxyContext,
} from "jsonld-dataset-proxy";
import { createDataset } from "o-dataset-pack";
import type { SolidProfileShape } from "./profileData";
import { ProfileShapeType } from "./profileData";
import type { LdoDataset } from "../src";
import {
commitTransaction,
createLdoDataset,
getDataset,
serialize,
startTransaction,
toJsonLd,
toNTriples,
toSparqlUpdate,
toTurtle,
transactionChanges,
write,
setLanguagePreferences,
languagesOf,
} from "../src";
describe("methods", () => {
let dataset: LdoDataset;
let profile: SolidProfileShape;
beforeEach(() => {
dataset = createLdoDataset();
profile = dataset
.usingType(ProfileShapeType)
.fromSubject(namedNode("https://example.com/item"));
});
it("Records changes in a transaction", () => {
startTransaction(profile);
profile.name = "Beeboo";
const changes = transactionChanges(profile);
expect(changes.added?.size).toBe(1);
expect(changes.removed).toBe(undefined);
});
it("throws when called with startTransaction if an underlying dataset is not a subscribable dataset", () => {
const proxy = getProxyFromObject(profile);
proxy[_proxyContext] = proxy[_proxyContext].duplicate({
dataset: createDataset(),
});
expect(() => startTransaction(profile)).toThrow(
"Object is not transactable.",
);
});
it("Commits changes", () => {
startTransaction(profile);
profile.name = "Joey";
expect(dataset.size).toBe(0);
commitTransaction(profile);
expect(dataset.size).toBe(1);
expect(profile.name).toBe("Joey");
});
it("throws an error if transaction dependent functions are called without a transaction", async () => {
expect(() => transactionChanges(profile)).toThrow(
"Object is not currently in a transaction",
);
expect(() => commitTransaction(profile)).toThrow(
"Object is not currently in a transaction",
);
await expect(async () => toSparqlUpdate(profile)).rejects.toThrow(
"Object is not currently in a transaction",
);
});
it("provides the correct sparql update", async () => {
profile.name = "Mr. Cool Dude";
startTransaction(profile);
profile.name = "Captain of Coolness";
expect(await toSparqlUpdate(profile)).toBe(
`DELETE DATA { <https://example.com/item> <http://xmlns.com/foaf/0.1/name> "Mr. Cool Dude" . }; INSERT DATA { <https://example.com/item> <http://xmlns.com/foaf/0.1/name> "Captain of Coolness" . }`,
);
});
it("provides a sparql update when nothing has been changed", async () => {
startTransaction(profile);
expect(await toSparqlUpdate(profile)).toBe("");
});
it("translates into turtle", async () => {
profile.name = "Captain of Coolness";
expect(await toTurtle(profile)).toBe(
'<https://example.com/item> <http://xmlns.com/foaf/0.1/name> "Captain of Coolness".\n',
);
});
it("translates into n-triples", async () => {
profile.name = "Captain of Coolness";
expect(await toNTriples(profile)).toBe(
'<https://example.com/item> <http://xmlns.com/foaf/0.1/name> "Captain of Coolness" .\n',
);
});
it("uses the serialize method", async () => {
profile.name = "Captain of Coolness";
expect(await serialize(profile, { format: "Turtle" })).toBe(
'<https://example.com/item> <http://xmlns.com/foaf/0.1/name> "Captain of Coolness".\n',
);
});
it.skip("translates into jsonld", async () => {
profile.name = "Captain of Coolness";
expect(await toJsonLd(profile)).toEqual([
{
"@id": "https://example.com/item",
"http://xmlns.com/foaf/0.1/name": "Captain of Coolness",
},
]);
});
it("errors when asked to convert to JsonLd", async () => {
await expect(async () => toJsonLd(profile)).rejects.toThrow(
"Not Implemented",
);
});
it("returns the underlying dataset", () => {
const underlyingDataset = getDataset(profile);
expect(typeof underlyingDataset.add).toBe("function");
});
it("sets a write graph", () => {
write("https://graphname.com").using(profile);
profile.name = "Jackson";
expect(graphOf(profile, "name")[0].value).toBe("https://graphname.com");
});
it("sets the language preferences", () => {
setLanguagePreferences("@none", "en").using(profile);
expect(
(profile as unknown as SubjectProxy)[_proxyContext].languageOrdering,
).toEqual(["@none", "en"]);
});
it("uses languagesOf", () => {
const result = languagesOf(profile, "name");
expect(result).toEqual({});
});
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,16 @@
export const sampleTurtle = `
<https://solidweb.me/jackson/profile/card> a <http://xmlns.com/foaf/0.1/PersonalProfileDocument>;
<http://xmlns.com/foaf/0.1/maker> <#me>;
<http://xmlns.com/foaf/0.1/primaryTopic> <#me>.
<#me> a <http://xmlns.com/foaf/0.1/Person>;
<http://www.w3.org/ns/solid/terms#oidcIssuer> <https://solidweb.me/>;
<http://www.w3.org/2006/vcard/ns#fn> "Jackson Morgan";
<http://www.w3.org/2006/vcard/ns#hasEmail> <#id1651115504716>.
`;
export const sampleJsonld = [
{
"@id": "https://example.com/item",
"http://xmlns.com/foaf/0.1/name": [{ "@value": "Captain of Coolness" }],
},
];

@ -0,0 +1,3 @@
{
"extends": ["../../.eslintrc"]
}

@ -0,0 +1,120 @@
# ShexJ 2 Type and Context
Turn ShexJ into typescript typings and JSON-LD context.
## Installation
```bash
npm i @ldo/schema-converter-shex
```
## API
See the [full API docs](docs/modules.md).
## Usage
```typescript
import { Schema } from "shexj";
import shexjToTypeAndContext from "@ldo/schema-converter-shex";
async function run() {
/**
* Sample ShexJ. Equivalent to:
*
* <EmployeeShape> { # An <EmployeeShape> has:
* foaf:givenName xsd:string+, # at least one givenName.
* foaf:familyName xsd:string, # one familyName.
* foaf:phone IRI*, # any number of phone numbers.
* foaf:mbox IRI # one FOAF mbox.
* }
*/
const sampleShexj: Schema = {
type: "Schema",
shapes: [
{
type: "Shape",
id: "http://shex.io/webapps/shex.js/doc/EmployeeShape",
expression: {
type: "EachOf",
expressions: [
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/givenName",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#string",
},
min: 1,
max: -1,
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/familyName",
valueExpr: {
type: "NodeConstraint",
datatype: "http://www.w3.org/2001/XMLSchema#string",
},
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/phone",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
min: 0,
max: -1,
},
{
type: "TripleConstraint",
predicate: "http://xmlns.com/foaf/0.1/mbox",
valueExpr: {
type: "NodeConstraint",
nodeKind: "iri",
},
},
],
},
},
],
"@context": "http://www.w3.org/ns/shex.jsonld",
};
const [typings, context] = await shexjToTypeAndContext(sampleShexj);
/*
Logs:
declare namespace {
interface EmployeeShape {
givenName: string[];
familyName: string;
phone?: string[];
mbox: string;
}
}
*/
console.log(typings.typingsString);
/*
Logs:
{
givenName: {
'@id': 'http://xmlns.com/foaf/0.1/givenName',
'@type': 'http://www.w3.org/2001/XMLSchema#string',
'@container': '@set'
},
familyName: {
'@id': 'http://xmlns.com/foaf/0.1/familyName',
'@type': 'http://www.w3.org/2001/XMLSchema#string'
},
phone: { '@id': 'http://xmlns.com/foaf/0.1/phone', '@container': '@set' },
mbox: { '@id': 'http://xmlns.com/foaf/0.1/mbox' }
}
*/
console.log(context);
}
run();
```
## Liscense
MIT

@ -0,0 +1,5 @@
const sharedConfig = require('../../jest.config.js');
module.exports = {
...sharedConfig,
'rootDir': './',
}

@ -0,0 +1,40 @@
{
"name": "@ldo/schema-converter-shex",
"version": "0.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"test": "jest --coverage",
"prepublishOnly": "npm run test && npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/o-development/shexj2typeandcontext.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/o-development/shexj2typeandcontext/issues"
},
"homepage": "https://github.com/o-development/shexj2typeandcontextr#readme",
"devDependencies": {
"@shexjs/parser": "^1.0.0-alpha.24",
"@types/jest": "^27.0.3",
"@types/jsonld": "^1.5.6",
"@types/shexj": "^2.1.3",
"jest": "^27.4.5",
"jsonld": "^5.2.0",
"o-dataset-pack": "^0.2.1",
"shex-test": "^2.1.0",
"ts-jest": "^27.1.2"
},
"files": [
"dist"
],
"dependencies": {
"dts-dom": "^3.6.0",
"jsonld2graphobject": "^0.0.5",
"shexj-traverser": "^2.0.1"
}
}

@ -0,0 +1,182 @@
import { Annotation } from "shexj";
import { ContextDefinition, ExpandedTermDefinition } from "jsonld";
/**
* Name functions
*/
export function iriToName(iri: string): string {
try {
const url = new URL(iri);
if (url.hash) {
return url.hash.slice(1);
} else {
const splitPathname = url.pathname.split("/");
return splitPathname[splitPathname.length - 1];
}
} catch (err) {
return iri;
}
}
export function nameFromObject(obj: {
id?: string;
annotations?: Annotation[];
}): string | undefined {
const labelAnnotationObject = obj.annotations?.find(
(annotation) =>
annotation.predicate === "http://www.w3.org/2000/01/rdf-schema#label"
)?.object;
if (labelAnnotationObject && typeof labelAnnotationObject === "string") {
return toCamelCase(iriToName(labelAnnotationObject));
} else if (
labelAnnotationObject &&
typeof labelAnnotationObject !== "string"
) {
return toCamelCase(labelAnnotationObject.value);
} else if (obj.id) {
return toCamelCase(iriToName(obj.id));
}
}
export function toCamelCase(text: string) {
return text
.replace(/([-_ ]){1,}/g, " ")
.split(/[-_ ]/)
.reduce((cur, acc) => {
return cur + acc[0].toUpperCase() + acc.substring(1);
});
}
/**
* JsonLd Context Builder
*/
export class JsonLdContextBuilder {
private iriAnnotations: Record<string, Annotation[]> = {};
private iriTypes: Record<string, ExpandedTermDefinition> = {};
private generatedNames: Record<string, string> | undefined;
addSubject(iri: string, annotations?: Annotation[]) {
if (!this.iriAnnotations[iri]) {
this.iriAnnotations[iri] = [];
}
if (annotations && annotations.length > 0) {
this.iriAnnotations[iri].push(...annotations);
}
}
addPredicate(
iri: string,
expandedTermDefinition: ExpandedTermDefinition,
isContainer: boolean,
annotations?: Annotation[]
) {
this.addSubject(iri, annotations);
if (!this.iriTypes[iri]) {
this.iriTypes[iri] = expandedTermDefinition;
if (isContainer) {
this.iriTypes[iri]["@container"] = "@set";
}
} else {
const curDef = this.iriTypes[iri];
const newDef = expandedTermDefinition;
// TODO: if you reuse the same predicate with a different cardinality,
// it will overwrite the past cardinality. Perhapse we might want to
// split contexts in the various shapes.
if (isContainer) {
curDef["@container"] = "@set";
}
// If the old and new versions both have types
if (curDef["@type"] && newDef["@type"]) {
if (
Array.isArray(curDef["@type"]) &&
!(curDef["@type"] as string[]).includes(newDef["@type"])
) {
curDef["@type"].push(newDef["@type"]);
} else if (
typeof curDef["@type"] === "string" &&
curDef["@type"] !== newDef["@type"]
) {
// The typings are incorrect. String arrays are allowed on @type
// see https://w3c.github.io/json-ld-syntax/#example-specifying-multiple-types-for-a-node
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
curDef["@type"] = [curDef["@type"], newDef["@type"]];
}
}
}
}
generateNames(): Record<string, string> {
const generatedNames: Record<string, string> = {};
const claimedNames: Set<string> = new Set();
Object.entries(this.iriAnnotations).forEach(([iri, annotations]) => {
let potentialName: string | undefined;
if (annotations.length > 0) {
const labelAnnotationObject = annotations.find(
(annotation) =>
annotation.predicate ===
"http://www.w3.org/2000/01/rdf-schema#label"
)?.object;
if (
labelAnnotationObject &&
typeof labelAnnotationObject === "string"
) {
potentialName = toCamelCase(iriToName(labelAnnotationObject));
} else if (
labelAnnotationObject &&
typeof labelAnnotationObject !== "string"
) {
potentialName = toCamelCase(labelAnnotationObject.value);
}
}
if (!potentialName) {
potentialName = toCamelCase(iriToName(iri));
}
if (claimedNames.has(potentialName)) {
let i = 2;
let newName: string | undefined;
do {
if (!claimedNames.has(`${potentialName}${i}`)) {
newName = `${potentialName}${i}`;
}
i++;
} while (!newName);
potentialName = newName;
}
claimedNames.add(potentialName);
generatedNames[iri] = potentialName;
});
return generatedNames;
}
getNameFromIri(iri: string) {
if (!this.generatedNames) {
this.generatedNames = this.generateNames();
}
if (this.generatedNames[iri]) {
return this.generatedNames[iri];
} else {
return iri;
}
}
generateJsonldContext(): ContextDefinition {
const contextDefnition: ContextDefinition = {};
const namesMap = this.generateNames();
Object.entries(namesMap).forEach(([iri, name]) => {
if (this.iriTypes[iri]) {
contextDefnition[name] = {
"@id":
iri === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
? "@type"
: iri,
...this.iriTypes[iri],
};
} else {
contextDefnition[name] = iri;
}
});
return contextDefnition;
}
}

@ -0,0 +1,83 @@
import ShexJTraverser from "shexj-traverser";
import { JsonLdContextBuilder } from "./JsonLdContextBuilder";
/**
* Visitor
*/
export const ShexJNameVisitor =
ShexJTraverser.createVisitor<JsonLdContextBuilder>({
Shape: {
visitor: async (shape, context) => {
}
},
TripleConstraint: {
visitor: async (tripleConstraint, context) => {
if (tripleConstraint.valueExpr) {
const isContainer =
tripleConstraint.max !== undefined && tripleConstraint.max !== 1;
if (typeof tripleConstraint.valueExpr === "string") {
// TOOD handle string value expr
} else if (tripleConstraint.valueExpr.type === "NodeConstraint") {
if (tripleConstraint.valueExpr.datatype) {
context.addPredicate(
tripleConstraint.predicate,
{
"@type": tripleConstraint.valueExpr.datatype,
},
isContainer,
tripleConstraint.annotations
);
} else if (
tripleConstraint.valueExpr.nodeKind &&
tripleConstraint.valueExpr.nodeKind !== "literal"
) {
context.addPredicate(
tripleConstraint.predicate,
{ "@type": "@id" },
isContainer,
tripleConstraint.annotations
);
} else {
context.addPredicate(
tripleConstraint.predicate,
{},
isContainer,
tripleConstraint.annotations
);
}
} else {
context.addPredicate(
tripleConstraint.predicate,
{
"@type": "@id",
},
isContainer,
tripleConstraint.annotations
);
}
} else {
context.addSubject(
tripleConstraint.predicate,
tripleConstraint.annotations
);
}
},
},
NodeConstraint: {
visitor: async (nodeConstraint, context) => {
if (nodeConstraint.values) {
nodeConstraint.values.forEach((value) => {
if (typeof value === "string") {
context.addSubject(value);
}
});
}
},
},
IriStem: {
visitor: async (iriStem, context) => {
context.addSubject(iriStem.stem);
},
},
});

@ -0,0 +1,21 @@
import { ContextDefinition } from "jsonld";
import { Schema } from "shexj";
import { JsonLdContextBuilder } from "./JsonLdContextBuilder";
import { ShexJNameVisitor } from "./ShexJContextVisitor";
import { jsonld2graphobject } from "jsonld2graphobject";
export async function shexjToContext(
shexj: Schema
): Promise<ContextDefinition> {
const processedShexj: Schema = (await jsonld2graphobject(
{
...shexj,
"@id": "SCHEMA",
"@context": "http://www.w3.org/ns/shex.jsonld",
},
"SCHEMA"
)) as unknown as Schema;
const jsonLdContextBuilder = new JsonLdContextBuilder();
await ShexJNameVisitor.visit(processedShexj, "Schema", jsonLdContextBuilder);
return jsonLdContextBuilder.generateJsonldContext();
}

@ -0,0 +1,3 @@
import { shexjToTyping } from "./typing/shexjToTyping";
export default shexjToTyping;

@ -0,0 +1,5 @@
import { InterfaceDeclaration } from "dts-dom";
export interface ShapeInterfaceDeclaration extends InterfaceDeclaration {
shapeId?: string;
}

@ -0,0 +1,355 @@
import ShexJTraverser from "shexj-traverser";
import * as dom from "dts-dom";
import { Annotation } from "shexj";
import { nameFromObject } from "../context/JsonLdContextBuilder";
import { ShapeInterfaceDeclaration } from "./ShapeInterfaceDeclaration";
export interface ShexJTypeTransformerContext {
getNameFromIri: (iri: string) => string;
}
export function commentFromAnnotations(
annotations?: Annotation[]
): string | undefined {
const commentAnnotationObject = annotations?.find(
(annotation) =>
annotation.predicate === "http://www.w3.org/2000/01/rdf-schema#comment"
)?.object;
if (typeof commentAnnotationObject === "string") {
// It's an IRI
return commentAnnotationObject;
} else {
return commentAnnotationObject?.value;
}
}
export const ShexJTypingTransformer = ShexJTraverser.createTransformer<
{
Schema: {
return: dom.TopLevelDeclaration[];
};
ShapeDecl: {
return: dom.InterfaceDeclaration;
};
Shape: {
return: dom.InterfaceDeclaration;
};
EachOf: {
return: dom.ObjectType | dom.InterfaceDeclaration;
};
TripleConstraint: {
return: dom.PropertyDeclaration;
};
NodeConstraint: {
return: dom.Type;
};
},
ShexJTypeTransformerContext
>({
Schema: {
transformer: async (
_schema,
getTransformedChildren
): Promise<dom.TopLevelDeclaration[]> => {
const transformedChildren = await getTransformedChildren();
const interfaces: dom.TopLevelDeclaration[] = [];
transformedChildren.shapes?.forEach((shape) => {
if (
typeof shape !== "string" &&
(shape as dom.InterfaceDeclaration).kind === "interface"
) {
interfaces.push(shape as dom.InterfaceDeclaration);
}
});
return interfaces;
},
},
ShapeDecl: {
transformer: async (
shapeDecl,
getTransformedChildren
): Promise<dom.InterfaceDeclaration> => {
const shapeName = nameFromObject(shapeDecl) || "Shape";
const { shapeExpr } = await getTransformedChildren();
if ((shapeExpr as dom.InterfaceDeclaration).kind === "interface") {
const shapeInterface = shapeExpr as ShapeInterfaceDeclaration;
shapeInterface.name = shapeName;
// This exists so the LDO-CLI can understand which type corresponds to the shape
shapeInterface.shapeId = shapeDecl.id;
return shapeInterface;
} else {
// TODO: Handle other items
throw new Error(
"Cannot handle ShapeOr, ShapeAnd, ShapeNot, ShapeExternal, or NodeConstraint"
);
}
},
},
Shape: {
transformer: async (shape, getTransformedChildren, setReturnPointer) => {
const newInterface: ShapeInterfaceDeclaration = dom.create.interface("");
setReturnPointer(newInterface);
const transformedChildren = await getTransformedChildren();
// Add @id and @context
newInterface.members.push(
dom.create.property(
"@id",
dom.type.string,
dom.DeclarationFlags.Optional
)
);
newInterface.members.push(
dom.create.property(
"@context",
dom.create.namedTypeReference("ContextDefinition"),
dom.DeclarationFlags.Optional
)
);
if (typeof transformedChildren.expression === "string") {
// TODO: handle string
} else if (
(transformedChildren.expression as dom.ObjectType).kind === "object" ||
(transformedChildren.expression as dom.InterfaceDeclaration).kind ===
"interface"
) {
newInterface.members.push(
...(transformedChildren.expression as dom.ObjectType).members
);
} else if (
(transformedChildren.expression as dom.PropertyDeclaration).kind ===
"property"
) {
newInterface.members.push(
transformedChildren.expression as dom.PropertyDeclaration
);
}
// Use EXTENDS
if (transformedChildren.extends) {
newInterface.baseTypes = [];
transformedChildren.extends.forEach((extendsItem) => {
if ((extendsItem as dom.InterfaceDeclaration).kind === "interface") {
newInterface.baseTypes?.push(
extendsItem as dom.InterfaceDeclaration
);
}
});
}
return newInterface;
},
},
EachOf: {
transformer: async (eachOf, getTransformedChildren, setReturnPointer) => {
const transformedChildren = await getTransformedChildren();
const name = nameFromObject(eachOf);
const objectType = name
? dom.create.interface(name)
: dom.create.objectType([]);
const eachOfComment = commentFromAnnotations(eachOf.annotations);
setReturnPointer(objectType);
// Get Input property expressions
const inputPropertyExpressions: dom.PropertyDeclaration[] = [];
transformedChildren.expressions
.filter(
(
expression
): expression is dom.ObjectType | dom.PropertyDeclaration => {
return (
(expression as dom.PropertyDeclaration).kind === "property" ||
(expression as dom.ObjectType).kind === "object" ||
(expression as dom.InterfaceDeclaration).kind === "interface"
);
}
)
.forEach(
(
expression:
| dom.ObjectType
| dom.InterfaceDeclaration
| dom.PropertyDeclaration
) => {
if (expression.kind === "property") {
inputPropertyExpressions.push(expression);
} else {
expression.members.forEach((objectMember) => {
if (objectMember.kind === "property") {
inputPropertyExpressions.push(objectMember);
}
});
}
}
);
// Merge property expressions
const properties: Record<string, dom.PropertyDeclaration> = {};
inputPropertyExpressions.forEach((expression) => {
const propertyDeclaration = expression as dom.PropertyDeclaration;
// Combine properties if they're duplicates
if (properties[propertyDeclaration.name]) {
const oldPropertyDeclaration = properties[propertyDeclaration.name];
const oldPropertyTypeAsArray =
oldPropertyDeclaration.type as dom.ArrayTypeReference;
const oldProeprtyType =
oldPropertyTypeAsArray.kind === "array"
? oldPropertyTypeAsArray.type
: oldPropertyDeclaration.type;
const propertyTypeAsArray =
propertyDeclaration.type as dom.ArrayTypeReference;
const propertyType =
propertyTypeAsArray.kind === "array"
? propertyTypeAsArray.type
: propertyDeclaration.type;
const isOptional =
propertyDeclaration.flags === dom.DeclarationFlags.Optional ||
oldPropertyDeclaration.flags === dom.DeclarationFlags.Optional;
properties[propertyDeclaration.name] = dom.create.property(
propertyDeclaration.name,
dom.type.array(dom.create.union([oldProeprtyType, propertyType])),
isOptional
? dom.DeclarationFlags.Optional
: dom.DeclarationFlags.None
);
// Set JS Comment
properties[propertyDeclaration.name].jsDocComment =
oldPropertyDeclaration.jsDocComment &&
propertyDeclaration.jsDocComment
? `${oldPropertyDeclaration.jsDocComment} | ${propertyDeclaration.jsDocComment}`
: oldPropertyDeclaration.jsDocComment ||
propertyDeclaration.jsDocComment ||
eachOfComment;
} else {
properties[propertyDeclaration.name] = propertyDeclaration;
}
});
objectType.members.push(...Object.values(properties));
return objectType;
},
},
TripleConstraint: {
transformer: async (
tripleConstraint,
getTransformedChildren,
setReturnPointer,
context
) => {
const transformedChildren = await getTransformedChildren();
const propertyName = context.getNameFromIri(tripleConstraint.predicate);
const isArray =
tripleConstraint.max !== undefined && tripleConstraint.max !== 1;
const isOptional = tripleConstraint.min === 0;
let type: dom.Type = dom.type.any;
if (transformedChildren.valueExpr) {
type = transformedChildren.valueExpr as dom.Type;
}
const propertyDeclaration = dom.create.property(
propertyName,
isArray ? dom.type.array(type) : type,
isOptional ? dom.DeclarationFlags.Optional : dom.DeclarationFlags.None
);
propertyDeclaration.jsDocComment = commentFromAnnotations(
tripleConstraint.annotations
);
return propertyDeclaration;
},
},
NodeConstraint: {
transformer: async (
nodeConstraint,
_getTransformedChildren,
setReturnPointer,
context
) => {
if (nodeConstraint.datatype) {
switch (nodeConstraint.datatype) {
case "http://www.w3.org/2001/XMLSchema#string":
case "http://www.w3.org/2001/XMLSchema#ENTITIES":
case "http://www.w3.org/2001/XMLSchema#ENTITY":
case "http://www.w3.org/2001/XMLSchema#ID":
case "http://www.w3.org/2001/XMLSchema#IDREF":
case "http://www.w3.org/2001/XMLSchema#IDREFS":
case "http://www.w3.org/2001/XMLSchema#language":
case "http://www.w3.org/2001/XMLSchema#Name":
case "http://www.w3.org/2001/XMLSchema#NCName":
case "http://www.w3.org/2001/XMLSchema#NMTOKEN":
case "http://www.w3.org/2001/XMLSchema#NMTOKENS":
case "http://www.w3.org/2001/XMLSchema#normalizedString":
case "http://www.w3.org/2001/XMLSchema#QName":
case "http://www.w3.org/2001/XMLSchema#token":
return dom.type.string;
case "http://www.w3.org/2001/XMLSchema#date":
case "http://www.w3.org/2001/XMLSchema#dateTime":
case "http://www.w3.org/2001/XMLSchema#duration":
case "http://www.w3.org/2001/XMLSchema#gDay":
case "http://www.w3.org/2001/XMLSchema#gMonth":
case "http://www.w3.org/2001/XMLSchema#gMonthDay":
case "http://www.w3.org/2001/XMLSchema#gYear":
case "http://www.w3.org/2001/XMLSchema#gYearMonth":
case "http://www.w3.org/2001/XMLSchema#time":
return dom.type.string;
case "http://www.w3.org/2001/XMLSchema#byte":
case "http://www.w3.org/2001/XMLSchema#decimal":
case "http://www.w3.org/2001/XMLSchema#int":
case "http://www.w3.org/2001/XMLSchema#integer":
case "http://www.w3.org/2001/XMLSchema#long":
case "http://www.w3.org/2001/XMLSchema#negativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonNegativeInteger":
case "http://www.w3.org/2001/XMLSchema#nonPositiveInteger":
case "http://www.w3.org/2001/XMLSchema#positiveInteger":
case "http://www.w3.org/2001/XMLSchema#short":
case "http://www.w3.org/2001/XMLSchema#unsignedLong":
case "http://www.w3.org/2001/XMLSchema#unsignedInt":
case "http://www.w3.org/2001/XMLSchema#unsignedShort":
case "http://www.w3.org/2001/XMLSchema#unsignedByte":
return dom.type.number;
case "http://www.w3.org/2001/XMLSchema#boolean":
return dom.type.boolean;
case "http://www.w3.org/2001/XMLSchema#hexBinary":
return dom.type.string;
case "http://www.w3.org/2001/XMLSchema#anyURI":
return dom.type.string;
default:
return dom.type.string;
}
}
if (nodeConstraint.nodeKind) {
switch (nodeConstraint.nodeKind) {
case "iri":
return dom.create.objectType([
dom.create.property("@id", dom.type.string),
]);
case "bnode":
return dom.create.objectType([]);
case "nonliteral":
return dom.create.objectType([
dom.create.property(
"@id",
dom.type.string,
dom.DeclarationFlags.Optional
),
]);
case "literal":
default:
return dom.type.string;
}
}
if (nodeConstraint.values) {
const valuesUnion = dom.create.union([]);
nodeConstraint.values.forEach((value) => {
if (typeof value === "string") {
valuesUnion.members.push(
dom.create.objectType([
dom.create.property(
"@id",
dom.type.stringLiteral(context.getNameFromIri(value))
),
])
);
}
});
return valuesUnion;
}
return dom.type.undefined;
},
},
});

@ -0,0 +1,59 @@
import { ContextDefinition } from "jsonld";
import { Schema } from "shexj";
import { JsonLdContextBuilder } from "../context/JsonLdContextBuilder";
import { ShexJNameVisitor } from "../context/ShexJContextVisitor";
import { jsonld2graphobject } from "jsonld2graphobject";
import { ShexJTypingTransformer } from "./ShexJTypingTransformer";
import * as dom from "dts-dom";
export interface TypeingReturn {
typingsString: string;
typings: {
typingString: string;
dts: dom.TopLevelDeclaration;
}[];
}
export async function shexjToTyping(
shexj: Schema
): Promise<[TypeingReturn, ContextDefinition]> {
const processedShexj: Schema = (await jsonld2graphobject(
{
...shexj,
"@id": "SCHEMA",
"@context": "http://www.w3.org/ns/shex.jsonld",
},
"SCHEMA"
)) as unknown as Schema;
const jsonLdContextBuilder = new JsonLdContextBuilder();
await ShexJNameVisitor.visit(processedShexj, "Schema", jsonLdContextBuilder);
const declarations = await ShexJTypingTransformer.transform(
processedShexj,
"Schema",
{
getNameFromIri:
jsonLdContextBuilder.getNameFromIri.bind(jsonLdContextBuilder),
}
);
const typings = declarations.map((declaration) => {
return {
typingString: dom
.emit(declaration, {
rootFlags: dom.ContextFlags.InAmbientNamespace,
})
.replace("\r\n", "\n"),
dts: declaration,
};
});
const typingsString =
`import {ContextDefinition} from "jsonld"\n\n` +
typings.map((typing) => `export ${typing.typingString}`).join("");
const typeingReturn: TypeingReturn = {
typingsString,
typings,
};
return [typeingReturn, jsonLdContextBuilder.generateJsonldContext()];
}

@ -0,0 +1,16 @@
import { testData } from "./testData/testData";
import { shexjToContext } from "../src/context/shexjToContext";
import parser from "@shexjs/parser";
import type { Schema } from "shexj";
describe("context", () => {
testData.forEach(({ name, shexc, successfulContext }) => {
it(`Creates a context for ${name}`, async () => {
const schema: Schema = parser
.construct("https://ldo.js.org/")
.parse(shexc);
const context = await shexjToContext(schema);
expect(context).toEqual(successfulContext);
});
});
});

File diff suppressed because one or more lines are too long

@ -0,0 +1,42 @@
import type { TestData } from "./testData";
/**
* Circular
*/
export const circular: TestData = {
name: "circular",
shexc: `
PREFIX example: <http://example.com/>
example:ParentShape {
a [ example:Parent ]? ;
example:hasChild @example:ChildShape ;
}
example:ChildShape {
a [ example:Child ]? ;
example:hasParent @example:ParentShape ;
}
`,
sampleTurtle: `
@prefix example: <http://example.com/> .
example:SampleParent
a example:Parent ;
example:hasChild example:SampleChild .
example:SampleChild
a example:Child ;
example:hasParent example:SampleParent .
`,
baseNode: "http://example.com/SampleParent",
successfulContext: {
type: { "@id": "@type" },
Parent: "http://example.com/Parent",
hasChild: { "@id": "http://example.com/hasChild", "@type": "@id" },
Child: "http://example.com/Child",
hasParent: { "@id": "http://example.com/hasParent", "@type": "@id" },
},
successfulTypings:
'import {ContextDefinition} from "jsonld"\n\nexport interface ParentShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n type?: {\r\n "@id": "Parent";\r\n };\r\n hasChild: ChildShape;\r\n}\r\n\r\nexport interface ChildShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n type?: {\r\n "@id": "Child";\r\n };\r\n hasParent: ParentShape;\r\n}\r\n\r\n',
};

@ -0,0 +1,43 @@
import type { TestData } from "./testData";
/**
* Circular
*/
export const extendsSimple: TestData = {
name: "extends simple",
shexc: `
PREFIX ex: <https://example.com/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
ex:EntityShape {
ex:entityId .
}
ex:PersonShape EXTENDS @ex:EntityShape {
foaf:name .
}
ex:EmployeeShape EXTENDS @ex:PersonShape {
ex:employeeNumber .
}
`,
sampleTurtle: `
@prefix example: <http://example.com/> .
example:SampleParent
a example:Parent ;
example:hasChild example:SampleChild .
example:SampleChild
a example:Child ;
example:hasParent example:SampleParent .
`,
baseNode: "http://example.com/SampleParent",
successfulContext: {
entityId: "https://example.com/entityId",
name: "http://xmlns.com/foaf/0.1/name",
employeeNumber: "https://example.com/employeeNumber",
},
successfulTypings:
'import {ContextDefinition} from "jsonld"\n\nexport interface EntityShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n entityId: any;\r\n}\r\n\r\nexport interface PersonShapeextends EntityShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n name: any;\r\n}\r\n\r\nexport interface EmployeeShapeextends PersonShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n employeeNumber: any;\r\n}\r\n\r\n',
};

@ -0,0 +1,48 @@
import type { TestData } from "./testData";
/**
* Circular
*/
export const oldExtends: TestData = {
name: "old extends",
shexc: `
PREFIX ex: <https://example.com/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
ex:EntityShape {
$ex:EntityRef (
ex:entityId .
)
}
ex:PersonShape {
$ex:PersonRef (
&ex:EntityRef ;
foaf:name .
)
}
ex:EmployeeShape EXTENDS @ex:PersonShape {
&ex:PersonRef ;
ex:employeeNumber .
}
`,
sampleTurtle: `
@prefix ex: <http://example.com/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
ex:SampleEmployee
ex:entityId "123"^^xsd:integer ;
foaf:name "Jacko" ;
ex:employeeNumber "456"^^xsd:integer ;
`,
baseNode: "http://example.com/SampleParent",
successfulContext: {
entityId: "https://example.com/entityId",
name: "http://xmlns.com/foaf/0.1/name",
employeeNumber: "https://example.com/employeeNumber",
},
successfulTypings:
'import {ContextDefinition} from "jsonld"\n\nexport interface EntityShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n entityId: any;\r\n}\r\n\r\nexport interface PersonShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n entityId: any;\r\n name: any;\r\n}\r\n\r\nexport interface EmployeeShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n entityId: any;\r\n name: any;\r\n employeeNumber: any;\r\n}\r\n\r\n',
};

File diff suppressed because one or more lines are too long

@ -0,0 +1,81 @@
import type { TestData } from "./testData";
/**
* Reduced Profile
*/
export const reducedProfile: TestData = {
name: "reduced profile",
shexc: `
PREFIX srs: <https://shaperepo.com/schemas/solidProfile#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX schem: <http://schema.org/>
PREFIX vcard: <http://www.w3.org/2006/vcard/ns#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX acl: <http://www.w3.org/ns/auth/acl#>
PREFIX cert: <http://www.w3.org/ns/auth/cert#>
PREFIX ldp: <http://www.w3.org/ns/ldp#>
PREFIX sp: <http://www.w3.org/ns/pim/space#>
PREFIX solid: <http://www.w3.org/ns/solid/terms#>
srs:SolidProfileShape EXTRA a {
a [ schem:Person ]
// rdfs:comment "Defines the node as a Person" ;
a [ foaf:Person ]
// rdfs:comment "Defines the node as a Person" ;
vcard:hasEmail @srs:EmailShape *
// rdfs:comment "The person's email." ;
foaf:name xsd:string ?
// rdfs:comment "An alternate way to define a person's name" ;
}
srs:EmailShape EXTRA a {
a [
vcard:Dom
vcard:Home
vcard:ISDN
vcard:Internet
vcard:Intl
vcard:Label
vcard:Parcel
vcard:Postal
vcard:Pref
vcard:Work
vcard:X400
] ?
// rdfs:comment "The type of email." ;
vcard:value IRI
// rdfs:comment "The value of an email as a mailto link (Example <mailto:jane@example.com>)" ;
}
`,
sampleTurtle: ``,
baseNode: "",
successfulContext: {
type: { "@id": "@type" },
Person: "http://schema.org/Person",
Person2: "http://xmlns.com/foaf/0.1/Person",
hasEmail: {
"@id": "http://www.w3.org/2006/vcard/ns#hasEmail",
"@type": "@id",
"@container": "@set",
},
Dom: "http://www.w3.org/2006/vcard/ns#Dom",
Home: "http://www.w3.org/2006/vcard/ns#Home",
ISDN: "http://www.w3.org/2006/vcard/ns#ISDN",
Internet: "http://www.w3.org/2006/vcard/ns#Internet",
Intl: "http://www.w3.org/2006/vcard/ns#Intl",
Label: "http://www.w3.org/2006/vcard/ns#Label",
Parcel: "http://www.w3.org/2006/vcard/ns#Parcel",
Postal: "http://www.w3.org/2006/vcard/ns#Postal",
Pref: "http://www.w3.org/2006/vcard/ns#Pref",
Work: "http://www.w3.org/2006/vcard/ns#Work",
X400: "http://www.w3.org/2006/vcard/ns#X400",
value: { "@id": "http://www.w3.org/2006/vcard/ns#value", "@type": "@id" },
name: {
"@id": "http://xmlns.com/foaf/0.1/name",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
},
successfulTypings:
'import {ContextDefinition} from "jsonld"\n\nexport interface SolidProfileShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n /**\r\n * Defines the node as a Person | Defines the node as a Person\r\n */\r\n type: ({\r\n "@id": "Person";\r\n } | {\r\n "@id": "Person2";\r\n })[];\r\n /**\r\n * The person\'s email.\r\n */\r\n hasEmail?: (EmailShape)[];\r\n /**\r\n * An alternate way to define a person\'s name\r\n */\r\n name?: string;\r\n}\r\n\r\nexport interface EmailShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n /**\r\n * The type of email.\r\n */\r\n type?: {\r\n "@id": "Dom";\r\n } | {\r\n "@id": "Home";\r\n } | {\r\n "@id": "ISDN";\r\n } | {\r\n "@id": "Internet";\r\n } | {\r\n "@id": "Intl";\r\n } | {\r\n "@id": "Label";\r\n } | {\r\n "@id": "Parcel";\r\n } | {\r\n "@id": "Postal";\r\n } | {\r\n "@id": "Pref";\r\n } | {\r\n "@id": "Work";\r\n } | {\r\n "@id": "X400";\r\n };\r\n /**\r\n * The value of an email as a mailto link (Example <mailto:jane@example.com>)\r\n */\r\n value: {\r\n "@id": string;\r\n };\r\n}\r\n\r\n',
};

@ -0,0 +1,60 @@
import type { TestData } from "./testData";
/**
* Reused Predicate
*/
export const reusedPredicates: TestData = {
name: "reused predicates",
shexc: `
PREFIX app: <https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
app:DocumentShape {
rdf:type [ app:Document ] ;
app:vocabulary @app:VocabularyShape* ;
app:law @app:LawShape ;
}
app:LawShape {
rdf:type [ app:Law ] ;
app:name xsd:string ;
app:path IRI ;
}
app:VocabularyShape {
rdf:type [ app:Vocabulary ] ;
app:name xsd:string ;
app:path IRI ;
}
`,
sampleTurtle: ``,
baseNode: "http://example.com/SampleParent",
successfulContext: {
type: { "@id": "@type" },
Document: "https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#Document",
vocabulary: {
"@id":
"https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#vocabulary",
"@type": "@id",
"@container": "@set",
},
Vocabulary:
"https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#Vocabulary",
name: {
"@id": "https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#name",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
path: {
"@id": "https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#path",
"@type": "@id",
},
law: {
"@id": "https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#law",
"@type": "@id",
},
Law: "https://www.forsakringskassan.se/vocabs/fk-sem-poc.ttl#Law",
},
successfulTypings:
'import {ContextDefinition} from "jsonld"\n\nexport interface DocumentShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n type: {\r\n "@id": "Document";\r\n };\r\n vocabulary?: (VocabularyShape)[];\r\n law: LawShape;\r\n}\r\n\r\nexport interface LawShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n type: {\r\n "@id": "Law";\r\n };\r\n name: string;\r\n path: {\r\n "@id": string;\r\n };\r\n}\r\n\r\nexport interface VocabularyShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n type: {\r\n "@id": "Vocabulary";\r\n };\r\n name: string;\r\n path: {\r\n "@id": string;\r\n };\r\n}\r\n\r\n',
};

@ -0,0 +1,50 @@
import type { TestData } from "./testData";
/**
* SIMPLE
*/
export const simple: TestData = {
name: "simple",
shexc: `
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX example: <https://example.com/>
example:EmployeeShape { # An <EmployeeShape> has:
foaf:givenName xsd:string+, # at least one givenName.
foaf:familyName xsd:string, # one familyName.
foaf:phone IRI*, # any number of phone numbers.
foaf:mbox IRI # one FOAF mbox.
}
`,
sampleTurtle: `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://a.example/Employee7>
foaf:givenName "Robert"^^xsd:string, "Taylor"^^xsd:string ;
foaf:familyName "Johnson"^^xsd:string ;
# no phone number needed
foaf:mbox <mailto:rtj@example.com>
.
`,
baseNode: "http://a.example/Employee7",
successfulContext: {
givenName: {
"@id": "http://xmlns.com/foaf/0.1/givenName",
"@type": "http://www.w3.org/2001/XMLSchema#string",
"@container": "@set",
},
familyName: {
"@id": "http://xmlns.com/foaf/0.1/familyName",
"@type": "http://www.w3.org/2001/XMLSchema#string",
},
phone: {
"@id": "http://xmlns.com/foaf/0.1/phone",
"@type": "@id",
"@container": "@set",
},
mbox: { "@id": "http://xmlns.com/foaf/0.1/mbox", "@type": "@id" },
},
successfulTypings:
'import {ContextDefinition} from "jsonld"\n\nexport interface EmployeeShape {\n "@id"?: string;\r\n "@context"?: ContextDefinition;\r\n givenName: string[];\r\n familyName: string;\r\n phone?: {\r\n "@id": string;\r\n }[];\r\n mbox: {\r\n "@id": string;\r\n };\r\n}\r\n\r\n',
};

@ -0,0 +1,29 @@
import type { ContextDefinition } from "jsonld";
import { activityPub } from "./activityPub";
import { circular } from "./circular";
import { profile } from "./profile";
import { reducedProfile } from "./reducedProfile";
import { simple } from "./simple";
import { extendsSimple } from "./extendsSimple";
import { reusedPredicates } from "./reusedPredicates";
// import { oldExtends } from "./oldExtends";
export interface TestData {
name: string;
shexc: string;
sampleTurtle: string;
baseNode: string;
successfulContext: ContextDefinition;
successfulTypings: string;
}
export const testData: TestData[] = [
simple,
circular,
profile,
reducedProfile,
activityPub,
extendsSimple,
// oldExtends,
reusedPredicates,
];

@ -0,0 +1,16 @@
import parser from "@shexjs/parser";
import { testData } from "./testData/testData";
import { shexjToTyping } from "../src/typing/shexjToTyping";
import type { Schema } from "shexj";
describe("typing", () => {
testData.forEach(({ name, shexc, successfulTypings }) => {
it(`Creates a typings for ${name}`, async () => {
const schema: Schema = parser
.construct("https://ldo.js.org/")
.parse(shexc);
const [typings] = await shexjToTyping(schema);
expect(typings.typingsString).toBe(successfulTypings);
});
});
});

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["./src"]
}
Loading…
Cancel
Save