diff --git a/.changeset/lemon-icons-clean.md b/.changeset/lemon-icons-clean.md new file mode 100644 index 00000000..ee9d8656 --- /dev/null +++ b/.changeset/lemon-icons-clean.md @@ -0,0 +1,5 @@ +--- +"barnard59-shacl": patch +--- + +Produce SHACL report on successful validation diff --git a/packages/cube/test/validation.pipeline.test.js b/packages/cube/test/validation.pipeline.test.js index 898d8c0d..38b0e16f 100644 --- a/packages/cube/test/validation.pipeline.test.js +++ b/packages/cube/test/validation.pipeline.test.js @@ -15,7 +15,7 @@ describe('cube validation pipeline', function () { const result = shell.exec(command, { silent: true, cwd }) strictEqual(result.stderr, '') - strictEqual(result.stdout, '') + ok(result.stdout.includes('_:report "true"^^')) strictEqual(result.code, 0) }) @@ -48,7 +48,7 @@ describe('cube validation pipeline', function () { const result = shell.exec(command, { silent: true, cwd }) strictEqual(result.stderr, '') - strictEqual(result.stdout, '') + ok(result.stdout.includes('_:report "true"^^')) strictEqual(result.code, 0) }) diff --git a/packages/shacl/lib/TermCounter.js b/packages/shacl/lib/TermCounter.js new file mode 100644 index 00000000..e1cf2c63 --- /dev/null +++ b/packages/shacl/lib/TermCounter.js @@ -0,0 +1,15 @@ +export default class TermCounter { + /** + * @param {import('@rdfjs/term-map/Factory.js').TermMapFactory} env + */ + constructor(env) { + this.termMap = env.termMap() + } + + /** + * @param {import('@rdfjs/types').Term} term + */ + add(term) { + this.termMap.set(term, (this.termMap.get(term) ?? 0) + 1) + } +} diff --git a/packages/shacl/lib/report.js b/packages/shacl/lib/report.js index 95c1bf0e..f2e05ced 100644 --- a/packages/shacl/lib/report.js +++ b/packages/shacl/lib/report.js @@ -23,6 +23,18 @@ function getMessages(report) { .map(message => message + '\n') } -export function getSummary(dataset) { - return getMessages(new ValidationReport(this.env.clownface({ dataset }))) +export function getSummary() { + return async function * (stream) { + let total = 0 + for await (const dataset of stream) { + const messages = getMessages(new ValidationReport(this.env.clownface({ dataset }))) + total += messages.length + yield messages + } + if (total) { + this.error(new Error(`${total} violations found`)) + } else { + yield 'successful\n' + } + }.bind(this) } diff --git a/packages/shacl/pipeline/report-summary.ttl b/packages/shacl/pipeline/report-summary.ttl index 3989babe..8694f723 100644 --- a/packages/shacl/pipeline/report-summary.ttl +++ b/packages/shacl/pipeline/report-summary.ttl @@ -13,10 +13,10 @@ [ base:stdin () ] [ n3:parse () ] [ rdf:getDataset () ] - [ base:map ([ - a code:EcmaScriptModule ; - code:link - ]) + [ a p:Step ; + code:implementedBy [ a code:EcmaScriptModule ; + code:link + ] ; ] [ base:flatten () ] ) diff --git a/packages/shacl/report.js b/packages/shacl/report.js index 123062b2..bd34419a 100644 --- a/packages/shacl/report.js +++ b/packages/shacl/report.js @@ -1,6 +1,7 @@ import { Duplex } from 'node:stream' import { isReadableStream, isStream } from 'is-stream' import SHACLValidator from 'rdf-validate-shacl' +import TermCounter from './lib/TermCounter.js' /** * @this {import('barnard59-core').Context} @@ -10,6 +11,7 @@ import SHACLValidator from 'rdf-validate-shacl' */ async function * validate(ds, maxViolations, iterable) { let totalViolations = 0 + const counter = new TermCounter(this.env) for await (const chunk of iterable) { if (maxViolations && totalViolations > maxViolations) { @@ -21,14 +23,25 @@ async function * validate(ds, maxViolations, iterable) { const validator = new SHACLValidator(ds, { maxErrors: 0, factory: this.env }) const report = validator.validate(chunk) if (!report.conforms) { - const violations = report.results.filter(r => this.env.ns.sh.Violation.equals(r.severity)).length - totalViolations += violations + for (const result of report.results) { + if (result.severity) counter.add(result.severity) + } + + totalViolations = counter.termMap.get(this.env.ns.sh.Violation) ?? 0 yield report.dataset } } + counter.termMap.forEach((count, term) => this.logger.warn(`${count} results with severity ${term.value}`)) + if (totalViolations) { this.error(new Error(`${totalViolations} violations found`)) + } else { + const report = this.env.dataset() + const blankNode = this.env.blankNode('report') + report.add(this.env.quad(blankNode, this.env.ns.rdf.type, this.env.ns.sh.ValidationReport)) + report.add(this.env.quad(blankNode, this.env.ns.sh.conforms, this.env.literal('true', this.env.ns.xsd.boolean))) + yield report } } diff --git a/packages/shacl/test/TermCounter.test.js b/packages/shacl/test/TermCounter.test.js new file mode 100644 index 00000000..bc5540b8 --- /dev/null +++ b/packages/shacl/test/TermCounter.test.js @@ -0,0 +1,20 @@ +import { strictEqual } from 'assert' +import env from 'barnard59-env' +import TermCounter from '../lib/TermCounter.js' + +describe('TermCounter', () => { + it('should count terms', async () => { + const counter = new TermCounter(env) + counter.add(env.ns.sh.Violation) + counter.add(env.ns.sh.Violation) + counter.add(env.ns.sh.Info) + counter.add(env.ns.sh.Warning) + counter.add(env.ns.sh.Violation) + counter.add(env.ns.sh.Warning) + + strictEqual(counter.termMap.size, 3) + strictEqual(counter.termMap.get(env.ns.sh.Violation), 3) + strictEqual(counter.termMap.get(env.ns.sh.Warning), 2) + strictEqual(counter.termMap.get(env.ns.sh.Info), 1) + }) +})