parent
3961705934
commit
02de7a1757
File diff suppressed because it is too large
Load Diff
@ -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 |
@ -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…
Reference in new issue