api-tuner 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/tuner.sh CHANGED
@@ -1,121 +1,16 @@
1
- #!/bin/bash
2
- SCRIPT_PATH=$(dirname "$(readlink -f "$0")")
1
+ #!/usr/bin/env bash
3
2
 
4
- eye="swipl -x ${SCRIPT_PATH}/../eye/lib/eye.pvm --"
3
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5
4
 
6
- # function prints version
7
- function version() {
8
- # read from ./package.json
9
- API_TUNER_VERSION=$(cat "${SCRIPT_PATH}"/../package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
10
- echo "API-TUNER v${API_TUNER_VERSION}"
11
- $eye --version
12
- }
5
+ # find JS entrypoint
6
+ tuner="$SCRIPT_DIR/index.js"
13
7
 
14
- function usage() {
15
- echo "Usage: api-tuner [options] <path>..."
16
- echo ""
17
- echo "Options:"
18
- echo " --lib <path> Specify rules to include in all tests. Can be used multiple times. Make sure to surround globs in quotes to prevent expansion."
19
- echo " --silent Less output"
20
- echo " --debug Enable debug output"
21
- echo " --raw Output raw results from eye"
22
- echo " --base-iri <iri> Specify the base IRI for parsing the test case files"
23
- echo " --version Show version information"
24
- echo " --help Show this help message"
25
- }
26
-
27
- PARALLEL=true
28
- SILENT=false
29
- BASE_IRI=""
30
- DEBUG=false
31
- SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js --summary"
32
- PATHS=()
33
- LIBS=()
34
- # USAGE: ./tuner.sh --debug --version ...paths
35
- while [ $# -gt 0 ]; do
36
- case "$1" in
37
- --debug)
38
- DEBUG=true
39
- shift
40
- ;;
41
- --silent)
42
- SILENT=true
43
- shift
44
- ;;
45
- --raw)
46
- SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js"
47
- shift
48
- ;;
49
- --base-iri)
50
- BASE_IRI="$2"
51
- shift
52
- shift
53
- ;;
54
- --no-parallel)
55
- PARALLEL=false
56
- shift
57
- ;;
58
- --version)
59
- version
60
- exit 0
61
- ;;
62
- --help)
63
- usage
64
- exit 0
65
- ;;
66
- --lib)
67
- LIBS+=("$2")
68
- shift
69
- shift
70
- ;;
71
- *)
72
- PATHS+=("$1")
73
- shift
74
- ;;
75
- esac
76
- done
77
-
78
- # if no paths
79
- if [ ${#PATHS[@]} -eq 0 ]; then
80
- usage
81
- exit 1
82
- fi
83
-
84
- ARGS="--quiet --nope --pass"
85
-
86
- if [ "$DEBUG" = true ]; then
87
- ARGS="$ARGS ${SCRIPT_PATH}/../logging/debug.n3"
88
- fi
89
-
90
- if [ "$SILENT" != true ]; then
91
- ARGS="$ARGS ${SCRIPT_PATH}/../logging/info.n3"
92
- fi
93
-
94
- set -o pipefail
95
- process_path() {
96
- local path="$1"
97
- if [ "$SILENT" != true ] & [ $PARALLEL == false ]; then
98
- echo "" >&2
99
- echo "⚡️ RUNNING <file://$(realpath "$path")>" >&2
100
- fi
101
- node "${SCRIPT_PATH}/../lib/parse-test-case.js" --base-iri "$BASE_IRI" -- "${path}" \
102
- | $eye $ARGS "${SCRIPT_PATH}"/../rules/*.n3 ${LIBS[@]:+${LIBS[*]}} - \
103
- 2> >(while read -r line; do
104
- echo "$line" | sed -E 's/^"INFO" TRACE "(.*)"/ℹ️ \1/; s/^"DEBUG" TRACE "(.*)"/🐞 \1/' >&2
105
- done)
106
- }
107
-
108
-
109
- # if parallel
110
- if [ "$PARALLEL" = true ]; then
111
- # run in parallel
112
- for path in "${PATHS[@]}"; do
113
- process_path "$path" &
114
- done
115
- wait
8
+ # if tsx exists in path
9
+ if command -v tsx > /dev/null 2>&1
10
+ then
11
+ # use tsx
12
+ node --import tsx --no-warnings "$tuner" "$@"
116
13
  else
117
- # run sequentially
118
- for path in "${PATHS[@]}"; do
119
- process_path "$path"
120
- done
121
- fi | $SUMMARY
14
+ # use plain node
15
+ node "$tuner" "$@"
16
+ fi
package/index.js ADDED
@@ -0,0 +1,107 @@
1
+ import * as url from 'node:url';
2
+ import * as childProcess from 'node:child_process';
3
+ import { PassThrough } from 'node:stream';
4
+ import { resolve } from 'node:path';
5
+ import { program } from 'commander';
6
+ import getStream from 'get-stream';
7
+ import packageJson from './package.json' with { type: 'json' };
8
+ import parseTestCase from './lib/parse-test-case.js';
9
+ import summariseResults from './lib/summarise-results.js';
10
+ const eyePvmPath = url.fileURLToPath(new URL('eye/lib/eye.pvm', import.meta.url));
11
+ program
12
+ .name('api-tuner')
13
+ .option('--lib <lib>', 'Specify rules to include in all tests. Can be used multiple times. Make sure to surround globs in quotes to prevent expansion.')
14
+ .option('--silent', 'Less output', false)
15
+ .option('--debug', 'Enable debug output', false)
16
+ .option('--raw', 'Output raw results from eyes')
17
+ .requiredOption('--base-iri <baseIri>', 'Specify the base IRI for parsing the test case files')
18
+ .option('--version', 'Show version information')
19
+ .argument('[paths...]', 'Paths to test files')
20
+ .parse();
21
+ const options = program.opts();
22
+ if (options.version) {
23
+ process.stdout.write(`API-TUNER ${packageJson.version}\n`);
24
+ childProcess.execSync(`swipl -x ${eyePvmPath} -- --version`, { stdio: 'inherit' });
25
+ process.exit();
26
+ }
27
+ if (!program.args.length) {
28
+ program.help();
29
+ process.exit();
30
+ }
31
+ const eyeArgs = [
32
+ '--quiet',
33
+ '--nope',
34
+ '--pass',
35
+ ];
36
+ if (options.debug) {
37
+ const debugN3Path = url.fileURLToPath(new URL('logging/debug.n3', import.meta.url));
38
+ eyeArgs.push(debugN3Path);
39
+ }
40
+ if (!options.silent) {
41
+ const infoN3Path = url.fileURLToPath(new URL('logging/info.n3', import.meta.url));
42
+ eyeArgs.push(infoN3Path);
43
+ }
44
+ const rulesPath = url.fileURLToPath(new URL('rules/*.n3', import.meta.url));
45
+ const levelIcon = {
46
+ INFO: 'ℹ️',
47
+ DEBUG: '🐞',
48
+ 'Failed assertion': '❌',
49
+ };
50
+ async function processPath(path) {
51
+ return new Promise(resolve => {
52
+ const testCaseStream = parseTestCase(path, options.baseIri);
53
+ const eyeProc = childProcess.spawn('swipl', [
54
+ '-x',
55
+ eyePvmPath,
56
+ '--',
57
+ ...eyeArgs,
58
+ rulesPath,
59
+ '-',
60
+ ], {
61
+ shell: true,
62
+ });
63
+ testCaseStream.pipe(eyeProc.stdin);
64
+ const stdout = new PassThrough();
65
+ const stderr = new PassThrough();
66
+ eyeProc.on('exit', (code) => {
67
+ resolve({
68
+ stdout,
69
+ stderr,
70
+ success: code === 0,
71
+ });
72
+ });
73
+ eyeProc.stdout.pipe(stdout);
74
+ eyeProc.stderr.pipe(stderr);
75
+ });
76
+ }
77
+ const testSuites = program.args.map(async (path) => {
78
+ const absolutePath = resolve(process.cwd(), path);
79
+ const result = await processPath(path);
80
+ const suiteHeader = options.silent ? '' : `\n⚡️ SUITE <file://${absolutePath}>\n`;
81
+ const summaryPassThrough = new PassThrough();
82
+ const rawPassThrough = new PassThrough();
83
+ result.stdout.pipe(summaryPassThrough);
84
+ result.stdout.pipe(rawPassThrough);
85
+ const validationResult = await summariseResults(summaryPassThrough);
86
+ if (options.raw) {
87
+ process.stdout.write(suiteHeader + await getStream(rawPassThrough));
88
+ }
89
+ else {
90
+ const stderr = await getStream(result.stderr);
91
+ process.stderr.write(suiteHeader + stderr.replace(/"(\w+)" TRACE "(.*)"/g, (_, level, text) => {
92
+ return `${levelIcon[level]} ${text}`;
93
+ }));
94
+ }
95
+ return {
96
+ summary: `\n🔎 SUITE <file://${absolutePath}>\n${validationResult.summary}`,
97
+ success: result.success && validationResult.success,
98
+ };
99
+ });
100
+ Promise.all(testSuites).then((results) => {
101
+ const summary = results.map(result => result.summary).join('\n');
102
+ if (!options.raw) {
103
+ process.stdout.write(summary + '\n');
104
+ }
105
+ // exit code equals number of failed tests
106
+ process.exit(results.filter(result => !result.success).length);
107
+ });
@@ -1,60 +1,54 @@
1
- import * as fs from 'node:fs/promises'
2
- import { createReadStream } from 'node:fs'
3
- import jsonld from 'jsonld'
4
- import rdf from '@zazuko/env-node'
5
- import { write } from '@jeswr/pretty-turtle'
6
-
1
+ import * as fs from 'node:fs/promises';
2
+ import { createReadStream } from 'node:fs';
3
+ // eslint-disable-next-line import/default
4
+ import jsonld from 'jsonld';
5
+ import rdf from '@zazuko/env-node';
6
+ import { write } from '@jeswr/pretty-turtle';
7
7
  const ns = rdf.namespace('https://api-tuner.described.at/');
8
-
9
8
  (async () => {
10
- const bodyPath = process.argv[2]
11
-
12
- const curlJsonPath = `${bodyPath}.curl.json`
13
- const { response: responseJson, headers: headersJson } = JSON.parse((await fs.readFile(curlJsonPath)).toString())
14
- /**
15
- * @type {Record<string, string | number | null>}
16
- */
17
- const curlJsonLd = Object.assign({
18
- '@context': {
19
- '@vocab': ns().value,
20
- },
21
- }, responseJson)
22
-
23
- /**
24
- * @type {import('@rdfjs/types').Quad[]}
25
- */
26
- const responseQuads = await jsonld.toRDF(curlJsonLd)
27
- const response = rdf.clownface({
28
- dataset: rdf.dataset(responseQuads),
29
- }).has(ns.exitcode).addOut(rdf.ns.rdf.type, ns.Response)
30
-
31
- let contentType
32
- if (typeof responseJson.content_type === 'string') {
33
- contentType = responseJson.content_type.substring(0, responseJson.content_type.indexOf(';')) || responseJson.content_type
34
- }
35
- let parser
36
- if (contentType) {
37
- parser = rdf.formats.parsers.get(contentType)
38
- }
39
- if (parser) {
40
- const bodyGraph = rdf.blankNode()
41
- const bodyStream = parser.import(createReadStream(bodyPath))
42
- for await (const quad of bodyStream) {
43
- response.dataset.add(rdf.quad(quad.subject, quad.predicate, quad.object, bodyGraph))
9
+ const bodyPath = process.argv[2];
10
+ const curlJsonPath = `${bodyPath}.curl.json`;
11
+ const { response: responseJson, headers: headersJson } = JSON.parse((await fs.readFile(curlJsonPath)).toString());
12
+ /**
13
+ * @type {Record<string, string | number | null>}
14
+ */
15
+ const curlJsonLd = Object.assign({
16
+ '@context': {
17
+ '@vocab': ns().value,
18
+ },
19
+ }, responseJson);
20
+ /**
21
+ * @type {import('@rdfjs/types').Quad[]}
22
+ */
23
+ const responseQuads = await jsonld.toRDF(curlJsonLd);
24
+ const response = rdf.clownface({
25
+ dataset: rdf.dataset(responseQuads),
26
+ }).has(ns.exitcode).addOut(rdf.ns.rdf.type, ns.Response);
27
+ let contentType;
28
+ if (typeof responseJson.content_type === 'string') {
29
+ contentType = responseJson.content_type.substring(0, responseJson.content_type.indexOf(';')) || responseJson.content_type;
44
30
  }
45
- response.addOut(ns.body, bodyGraph)
46
- } else {
47
- const body = await fs.readFile(bodyPath, 'utf-8')
48
- if (body) {
49
- response.addOut(ns.body, body)
31
+ let parser;
32
+ if (contentType) {
33
+ parser = rdf.formats.parsers.get(contentType);
50
34
  }
51
- }
52
-
53
- const headers = Object.entries(headersJson).flatMap(([header, values]) =>
54
- values.map(value => response.blankNode().addOut(ns.name, header).addOut(ns.value, value)))
55
- response.addOut(ns.headers, headers)
56
-
57
- process.stdout.write(await write([...response.dataset], {
58
- format: 'text/n3',
59
- }))
60
- })()
35
+ if (parser) {
36
+ const bodyGraph = rdf.blankNode();
37
+ const bodyStream = parser.import(createReadStream(bodyPath));
38
+ for await (const quad of bodyStream) {
39
+ response.dataset.add(rdf.quad(quad.subject, quad.predicate, quad.object, bodyGraph));
40
+ }
41
+ response.addOut(ns.body, bodyGraph);
42
+ }
43
+ else {
44
+ const body = await fs.readFile(bodyPath, 'utf-8');
45
+ if (body) {
46
+ response.addOut(ns.body, body);
47
+ }
48
+ }
49
+ const headers = Object.entries(headersJson).flatMap(([header, values]) => values.map(value => response.blankNode().addOut(ns('name'), header).addOut(ns.value, value)));
50
+ response.addOut(ns.headers, headers);
51
+ process.stdout.write(await write([...response.dataset], {
52
+ format: 'text/n3',
53
+ }));
54
+ })();
@@ -0,0 +1,72 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import { createReadStream } from 'node:fs'
3
+ // eslint-disable-next-line import/default
4
+ import jsonld from 'jsonld'
5
+ import rdf from '@zazuko/env-node'
6
+ import { write } from '@jeswr/pretty-turtle'
7
+ import type { DatasetCore, Quad } from '@rdfjs/types'
8
+
9
+ declare module '@rdfjs/types' {
10
+ interface Stream extends AsyncIterable<Quad>{
11
+ }
12
+ }
13
+
14
+ interface CurlFile {
15
+ response: Record<string, string | number | null>
16
+ headers: Record<string, string[]>
17
+ }
18
+
19
+ const ns = rdf.namespace('https://api-tuner.described.at/');
20
+
21
+ (async () => {
22
+ const bodyPath = process.argv[2]
23
+
24
+ const curlJsonPath = `${bodyPath}.curl.json`
25
+ const { response: responseJson, headers: headersJson } = JSON.parse((await fs.readFile(curlJsonPath)).toString()) as CurlFile
26
+ /**
27
+ * @type {Record<string, string | number | null>}
28
+ */
29
+ const curlJsonLd = Object.assign({
30
+ '@context': {
31
+ '@vocab': ns().value,
32
+ },
33
+ }, responseJson)
34
+
35
+ /**
36
+ * @type {import('@rdfjs/types').Quad[]}
37
+ */
38
+ const responseQuads = await jsonld.toRDF(curlJsonLd) as DatasetCore
39
+ const response = rdf.clownface({
40
+ dataset: rdf.dataset(responseQuads),
41
+ }).has(ns.exitcode).addOut(rdf.ns.rdf.type, ns.Response)
42
+
43
+ let contentType
44
+ if (typeof responseJson.content_type === 'string') {
45
+ contentType = responseJson.content_type.substring(0, responseJson.content_type.indexOf(';')) || responseJson.content_type
46
+ }
47
+ let parser
48
+ if (contentType) {
49
+ parser = rdf.formats.parsers.get(contentType)
50
+ }
51
+ if (parser) {
52
+ const bodyGraph = rdf.blankNode()
53
+ const bodyStream = parser.import(createReadStream(bodyPath))
54
+ for await (const quad of bodyStream) {
55
+ response.dataset.add(rdf.quad(quad.subject, quad.predicate, quad.object, bodyGraph))
56
+ }
57
+ response.addOut(ns.body, bodyGraph)
58
+ } else {
59
+ const body = await fs.readFile(bodyPath, 'utf-8')
60
+ if (body) {
61
+ response.addOut(ns.body, body)
62
+ }
63
+ }
64
+
65
+ const headers = Object.entries(headersJson).flatMap(([header, values]) =>
66
+ values.map(value => response.blankNode().addOut(ns('name'), header).addOut(ns.value, value)))
67
+ response.addOut(ns.headers, headers)
68
+
69
+ process.stdout.write(await write([...response.dataset], {
70
+ format: 'text/n3',
71
+ }))
72
+ })()
@@ -1,39 +1,20 @@
1
- import * as url from 'node:url'
2
- import { createReadStream } from 'node:fs'
3
- import yargs from 'yargs'
4
- import { hideBin } from 'yargs/helpers'
5
- import replaceStream from 'replacestream'
6
- import isAbsoluteUrl from 'is-absolute-url'
7
- import StreamConcat from 'stream-concat'
8
-
9
- const argv = yargs(hideBin(process.argv)).argv
10
-
11
- const baseIri = argv['base-iri'] || 'http://example.org/'
12
- const testCases = argv._
13
-
14
- function replacer(fileUrl) {
15
- return (_, match) => {
16
- if (match.startsWith('#')) {
17
- return `<${fileUrl}${match}>`
1
+ import * as url from 'node:url';
2
+ import { createReadStream } from 'node:fs';
3
+ import replaceStream from 'replacestream';
4
+ import isAbsoluteUrl from 'is-absolute-url';
5
+ export default function (testCase, baseIri) {
6
+ function replacer(fileUrl) {
7
+ return (_, match) => {
8
+ if (match.startsWith('#')) {
9
+ return `<${fileUrl}${match}>`;
10
+ }
11
+ if (isAbsoluteUrl(match)) {
12
+ return `<${match}>`;
13
+ }
14
+ return `<${baseIri}${match}>`;
15
+ };
18
16
  }
19
-
20
- if (isAbsoluteUrl(match)) {
21
- return `<${match}>`
22
- }
23
-
24
- return `<${baseIri}${match}>`
25
- }
26
- }
27
-
28
- let fileIndex = 0
29
- function nextStream() {
30
- if (fileIndex === testCases.length) {
31
- return null
32
- }
33
- const testCase = testCases[fileIndex++]
34
- const testCaseUrl = url.pathToFileURL(testCase).toString()
35
- return createReadStream(testCase)
36
- .pipe(replaceStream(/<([^>]*)>(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/g, replacer(testCaseUrl)))
17
+ const testCaseUrl = url.pathToFileURL(testCase).toString();
18
+ return createReadStream(testCase)
19
+ .pipe(replaceStream(/<([^>]*)>(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/g, replacer(testCaseUrl)));
37
20
  }
38
-
39
- new StreamConcat(nextStream).pipe(process.stdout)
@@ -0,0 +1,25 @@
1
+ import * as url from 'node:url'
2
+ import { createReadStream } from 'node:fs'
3
+ import type { Readable } from 'node:stream'
4
+ import replaceStream from 'replacestream'
5
+ import isAbsoluteUrl from 'is-absolute-url'
6
+
7
+ export default function (testCase: string, baseIri: string): Readable {
8
+ function replacer(fileUrl: string) {
9
+ return (_: unknown, match: string) => {
10
+ if (match.startsWith('#')) {
11
+ return `<${fileUrl}${match}>`
12
+ }
13
+
14
+ if (isAbsoluteUrl(match)) {
15
+ return `<${match}>`
16
+ }
17
+
18
+ return `<${baseIri}${match}>`
19
+ }
20
+ }
21
+
22
+ const testCaseUrl = url.pathToFileURL(testCase).toString()
23
+ return createReadStream(testCase)
24
+ .pipe(replaceStream(/<([^>]*)>(?=([^"\\]*(\\.|"([^"\\]*\\.)*[^"\\]*"))*[^"]*$)/g, replacer(testCaseUrl)))
25
+ }
@@ -1,65 +1,33 @@
1
- import { PassThrough } from 'node:stream'
2
- import SHACLValidator from 'rdf-validate-shacl'
3
- import rdf from '@zazuko/env-node'
4
- import yargs from 'yargs'
5
- import { hideBin } from 'yargs/helpers'
6
-
7
- const argv = yargs(hideBin(process.argv)).argv
8
-
9
- const shapesTtl = new URL('./shapes.ttl', import.meta.url)
10
-
11
- ;(async () => {
12
- const shapes = await rdf.dataset().import(rdf.fromFile(shapesTtl))
13
- const validator = new SHACLValidator(shapes, {
14
- factory: rdf,
15
- })
16
-
17
- const dataPassThrough = new PassThrough()
18
- process.stdin.pipe(dataPassThrough)
19
-
20
- if (!argv.summary) {
21
- process.stdin.pipe(process.stdout)
22
- }
23
-
24
- const data = await rdf.dataset().import(rdf.formats.parsers.import('text/n3', dataPassThrough, {
25
- format: 'n3',
26
- }))
27
-
28
- const validationReport = validator.validate(data)
29
-
30
- if (argv.summary) {
31
- const resultMap = rdf.clownface({ dataset: data })
32
- .has(rdf.ns.rdf.type, rdf.ns.earl.TestCase)
33
- .toArray()
34
- .reduce((map, testCase) => {
35
- const { pathname } = new URL(testCase)
36
-
37
- const testCases = map.get(pathname) || []
38
- testCases.push(testCase)
39
-
40
- return map.set(pathname, testCases)
41
- }, new Map())
42
-
43
- for (const [pathname, testCases] of resultMap.entries()) {
44
- const summary = ['']
45
- summary.push(`🔎 SUITE <file://${pathname}>`)
46
-
47
- for (const testCase of testCases) {
48
- const { hash } = new URL(testCase.value)
49
- const result = validationReport.results.find(result => result.focusNode.equals(testCase.term))
50
- const label = testCase.out(rdf.ns.rdfs.label).value
51
- const resultLine = label ? `${label} (<${hash}>)` : `<${hash}>`
52
-
53
- if (result?.severity.equals(rdf.ns.sh.Violation)) {
54
- summary.push(`❌ FAIL ${resultLine}`)
55
- } else {
56
- summary.push(`✅ PASS ${resultLine}`)
1
+ import SHACLValidator from 'rdf-validate-shacl';
2
+ import rdf from '@zazuko/env-node';
3
+ const shapesTtl = new URL('./shapes.ttl', import.meta.url);
4
+ export default async (resultStream) => {
5
+ const shapes = await rdf.dataset().import(rdf.fromFile(shapesTtl));
6
+ const validator = new SHACLValidator(shapes, {
7
+ factory: rdf,
8
+ });
9
+ const data = await rdf.dataset().import(rdf.formats.parsers.import('text/n3', resultStream, {
10
+ format: 'n3',
11
+ }));
12
+ const validationReport = validator.validate(data);
13
+ const testCases = rdf.clownface({ dataset: data })
14
+ .has(rdf.ns.rdf.type, rdf.ns.earl.TestCase)
15
+ .toArray();
16
+ const summary = [];
17
+ for (const testCase of testCases) {
18
+ const { hash } = new URL(testCase.value);
19
+ const result = validationReport.results.find(result => testCase.term.equals(result.focusNode));
20
+ const label = testCase.out(rdf.ns.rdfs.label).value;
21
+ const resultLine = label ? `${label} (<${hash}>)` : `<${hash}>`;
22
+ if (rdf.ns.sh.Violation.equals(result?.severity)) {
23
+ summary.push(`❌ FAIL ${resultLine}`);
24
+ }
25
+ else {
26
+ summary.push(`✅ PASS ${resultLine}`);
57
27
  }
58
- }
59
-
60
- process.stderr.write(summary.join('\n') + '\n')
61
28
  }
62
- }
63
-
64
- process.exit(validationReport.conforms ? 0 : 1)
65
- })()
29
+ return {
30
+ success: validationReport.conforms,
31
+ summary: summary.join('\n'),
32
+ };
33
+ };
@@ -0,0 +1,46 @@
1
+ import type { Readable } from 'node:stream'
2
+ import SHACLValidator from 'rdf-validate-shacl'
3
+ import rdf from '@zazuko/env-node'
4
+
5
+ const shapesTtl = new URL('./shapes.ttl', import.meta.url)
6
+
7
+ interface Result {
8
+ summary: string
9
+ success: boolean
10
+ }
11
+
12
+ export default async (resultStream: Readable): Promise<Result> => {
13
+ const shapes = await rdf.dataset().import(rdf.fromFile(shapesTtl))
14
+ const validator = new SHACLValidator(shapes, {
15
+ factory: rdf,
16
+ })
17
+
18
+ const data = await rdf.dataset().import(rdf.formats.parsers.import('text/n3', resultStream, {
19
+ format: 'n3',
20
+ })!)
21
+
22
+ const validationReport = validator.validate(data)
23
+
24
+ const testCases = rdf.clownface({ dataset: data })
25
+ .has(rdf.ns.rdf.type, rdf.ns.earl.TestCase)
26
+ .toArray()
27
+
28
+ const summary = []
29
+ for (const testCase of testCases) {
30
+ const { hash } = new URL(testCase.value)
31
+ const result = validationReport.results.find(result => testCase.term.equals(result.focusNode))
32
+ const label = testCase.out(rdf.ns.rdfs.label).value
33
+ const resultLine = label ? `${label} (<${hash}>)` : `<${hash}>`
34
+
35
+ if (rdf.ns.sh.Violation.equals(result?.severity)) {
36
+ summary.push(`❌ FAIL ${resultLine}`)
37
+ } else {
38
+ summary.push(`✅ PASS ${resultLine}`)
39
+ }
40
+ }
41
+
42
+ return {
43
+ success: validationReport.conforms,
44
+ summary: summary.join('\n'),
45
+ }
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-tuner",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,6 +14,7 @@
14
14
  "lint": "eslint . --quiet --ignore-path .gitignore",
15
15
  "pretest": "docker compose up -d",
16
16
  "test": "./bin/tuner.sh tests/*.n3 tests/**/*.n3 --base-iri http://localhost:1080/",
17
+ "prepack": "tsc",
17
18
  "release": "changeset publish"
18
19
  },
19
20
  "files": [
@@ -26,21 +27,29 @@
26
27
  "@changesets/cli": "^2.27.12",
27
28
  "@jeswr/pretty-turtle": "^1.5.0",
28
29
  "@zazuko/env-node": "^2.1.4",
30
+ "commander": "^13.1.0",
31
+ "get-stream": "^9.0.1",
29
32
  "is-absolute-url": "^4.0.1",
30
33
  "jsonld": "^8.3.3",
31
34
  "rdf-validate-shacl": "^0.5.6",
32
- "replacestream": "^4.0.3",
33
- "stream-concat": "^2.0.0",
34
- "yargs": "^17.7.2"
35
+ "replacestream": "^4.0.3"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@rdfjs/types": "^1",
38
39
  "@tpluscode/eslint-config": "^0.5.0",
39
40
  "@types/jsonld": "^1.5.15",
41
+ "@types/n3": "^1.24.2",
40
42
  "@types/rdf-validate-shacl": "^0.4.9",
43
+ "@types/replacestream": "^4.0.4",
41
44
  "@types/yargs": "^17.0.33",
45
+ "@typescript-eslint/eslint-plugin": "^7",
46
+ "@typescript-eslint/parser": "^7",
47
+ "eslint": "^8.57.1",
48
+ "eslint-import-resolver-typescript": "^4.3.4",
42
49
  "husky": "^9.1.7",
43
- "lint-staged": "^15.4.3"
50
+ "lint-staged": "^15.4.3",
51
+ "tsx": "^4.19.3",
52
+ "typescript": "^5.8.3"
44
53
  },
45
54
  "lint-staged": {
46
55
  "*.{js,ts}": [
package/rules/requests.n3 CHANGED
@@ -70,7 +70,7 @@ prefix earl: <http://www.w3.org/ns/earl#>
70
70
  } .
71
71
 
72
72
  (
73
- "node " "lib/merge-curl-output.js"!file:libPath " "
73
+ "bin/merge-curl-output.sh"!file:libPath " "
74
74
  ?responseBodyFile
75
75
  " > " ?responseFile
76
76
  )!string:concatenation!e:exec .