Skip to content

Commit

Permalink
feat: proof of concept finished
Browse files Browse the repository at this point in the history
  • Loading branch information
jenspots committed Mar 30, 2024
1 parent 59ba5e7 commit f623f4e
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 77 deletions.
56 changes: 29 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
import { Stream, Writer } from "@treecg/connector-types";
import { Validator } from "shacl-engine";
import { fromFile } from "rdf-utils-fs";
import rdfDataModel from "@rdfjs/data-model";
import rdf from "rdf-ext";
import formats from "@rdfjs/formats-common";
import rdf, { PrefixMapFactory } from "rdf-ext";
import { Readable } from "stream";
import { ShaclError } from "./error";

async function buildValidator(path: string): Promise<Validator> {
// Initialize the SHACL dataset.
const stream = fromFile(path);
const dataset = await rdf.dataset().import(stream);

// Initialize the validator.
return new Validator(dataset, {
factory: rdfDataModel,
});
}
import formatsPretty from "@rdfjs/formats/pretty.js";
import Serializer from "@rdfjs/serializer-turtle";
import { Validator } from "shacl-engine";

export async function validate(
path: string,
reader: Stream<string>,
writer: Writer<string>,
error: Writer<string>,
error?: Writer<string>,
): Promise<() => Promise<void>> {
const validator = await buildValidator(path);
// Initialize the shared serializer.
const prefixes = new PrefixMapFactory().prefixMap();
prefixes.set("ex", rdf.namedNode("http://example.org#"));
prefixes.set("sh", rdf.namedNode("http://www.w3.org/ns/shacl#"));
const serializer = new Serializer({ prefixes });
const parser = rdf.formats.parsers.get("text/turtle")!;

// TODO: accept other data types.
const parser = formats.parsers.get("text/turtle");
if (!parser) {
throw ShaclError.invalidRdfFormat();
}
// Extend formatting with pretty formats.
rdf.formats.import(formatsPretty);

// Create shape stream.
const res = await rdf.fetch("./tests/shacl/point.ttl");
const shapes = await res.dataset();

// Parse input stream using shape stream.
// @ts-expect-error Factory is valid.
const validator = new Validator(shapes, { factory: rdf });

return async () => {
// Anything that passes through this processor and identifies with a
Expand All @@ -43,18 +40,23 @@ export async function validate(

// Run through validator.
const report = await validator.validate({ dataset });
const reportRaw = serializer.transform(report.dataset);

// Pass through data if valid.
if (report.conforms) {
await writer.push(data);
} else {
await error.push(data);
}

// Send report if error channel is given.
if (error) {
await error.push(reportRaw);
}
});

// If the input stream closes itself, so should the output streams.
reader.on("end", () => {
writer.end();
error.end();
error?.end();
});
};
}
31 changes: 31 additions & 0 deletions tests/data/invalid.report.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@prefix ex: <http://example.org#>.
@prefix sh: <http://www.w3.org/ns/shacl#>.

[ a sh:ValidationReport;
sh:conforms false;
sh:result [ a sh:ValidationResult;
sh:focusNode ex:ValidPoint;
sh:resultMessage "Requires an integer X coordinate";
sh:resultPath ex:x;
sh:resultSeverity sh:Violation;
sh:sourceConstraintComponent sh:DatatypeConstraintComponent;
sh:sourceShape [];
sh:value "1"
], [ a sh:ValidationResult;
sh:focusNode ex:ValidPoint;
sh:resultMessage "Requires an integer Y coordinate";
sh:resultPath ex:y;
sh:resultSeverity sh:Violation;
sh:sourceConstraintComponent sh:DatatypeConstraintComponent;
sh:sourceShape [];
sh:value "2"
], [ a sh:ValidationResult;
sh:focusNode ex:ValidPoint;
sh:resultMessage "Predicate is not allowed (closed shape)";
sh:resultPath ex:z;
sh:resultSeverity sh:Violation;
sh:sourceConstraintComponent sh:ClosedConstraintComponent;
sh:sourceShape ex:PointShape;
sh:value "3"
]
].
5 changes: 5 additions & 0 deletions tests/data/valid.report.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@prefix sh: <http://www.w3.org/ns/shacl#>.

[ a sh:ValidationReport;
sh:conforms true
].
File renamed without changes.
99 changes: 49 additions & 50 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,74 @@
import { validate } from "../src";
import { SimpleStream } from "@ajuvercr/js-runner";
import { describe, test, expect } from "vitest";
import { Validator } from "shacl-engine";
import rdf, { PrefixMapFactory } from "rdf-ext";
import formatsPretty from "@rdfjs/formats/pretty.js";
import Serializer from "@rdfjs/serializer-turtle";
import * as fs from "fs";

describe("shacl", () => {
test("library", async () => {
// Initialize the shared serializer.
const prefixes = new PrefixMapFactory().prefixMap();
prefixes.set("ex", rdf.namedNode("http://example.org#"));
prefixes.set("sh", rdf.namedNode("http://www.w3.org/ns/shacl#"));
const serializer = new Serializer({ prefixes });

// Extend formatting with pretty formats.
rdf.formats.import(formatsPretty);

// Create input stream.
let res = await rdf.fetch("./tests/data/invalid.ttl");
const dataset = await res.dataset();

// Create shape stream.
res = await rdf.fetch("./tests/shacl/point.ttl");
const shapes = await res.dataset();

// Parse input stream using shape stream.
const validator = new Validator(shapes, { factory: rdf });

// Export the report.
const report = await validator.validate({ dataset });
console.log(serializer.transform(report.dataset));
});
// Valid point.
const valid = fs.readFileSync("./tests/data/valid.ttl").toString();
const validReport = fs
.readFileSync("./tests/data/valid.report.ttl")
.toString();

// Invalid point.
const invalid = fs.readFileSync("./tests/data/invalid.ttl").toString();
const invalidReport = fs
.readFileSync("./tests/data/invalid.report.ttl")
.toString();

// SHACL data.
const shapePath = "./tests/shacl/point.ttl";

test("successful", async () => {
expect.assertions(2);

// Function parameters.
const path = "./tests/shacl/point.ttl";
const incoming = new SimpleStream<string>();
const outgoing = new SimpleStream<string>();
const error = new SimpleStream<string>();
const report = new SimpleStream<string>();

outgoing.on("data", (data) => {
expect(data).toEqual(valid);
});

// A valid point shape.
const point = `
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
report.on("data", (data) => {
expect(data).toEqual(validReport);
});

[ ] a <point>;
<x> "1"^^xsd:integer;
<y> "2"^^xsd:integer.
`;
// Initialize and execute the function.
const func = await validate(shapePath, incoming, outgoing, report);
await func();

// An invalid point shape.
const invalid = `
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
// Send point into the pipeline.
await incoming.push(valid);

// Finish testing.
await incoming.end();
await outgoing.end();
await report.end();
});

test("invalid", async () => {
expect.assertions(1);

// Function parameters.
const incoming = new SimpleStream<string>();
const outgoing = new SimpleStream<string>();
const error = new SimpleStream<string>();

[ ] a <point>;
<a> "3"^^xsd:integer;
<b> "4"^^xsd:integer.
`;
outgoing.on("data", () => {
expect(true).toBeFalsy();
});

// Set expected data.
outgoing.on("data", (data) => expect(data).toEqual(point));
error.on("data", (data) => expect(data).toEqual(invalid));
error.on("data", (data) => {
expect(data).toEqual(invalidReport);
});

// Initialize and execute the function.
const func = await validate(path, incoming, outgoing, error);
const func = await validate(shapePath, incoming, outgoing, error);
await func();

// Send point into the pipeline.
await incoming.push(point);
await incoming.push(invalid);

// Finish testing.
Expand Down

0 comments on commit f623f4e

Please sign in to comment.