api-tuner 0.2.5 → 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 +12 -91
- package/index.js +107 -0
- package/lib/merge-curl-output.js +50 -56
- package/lib/merge-curl-output.ts +72 -0
- package/lib/parse-test-case.js +18 -37
- package/lib/parse-test-case.ts +25 -0
- package/lib/summarise-results.js +31 -63
- package/lib/summarise-results.ts +46 -0
- package/package.json +14 -5
- package/rules/requests.n3 +1 -1
package/bin/tuner.sh
CHANGED
|
@@ -1,95 +1,16 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
SCRIPT_PATH=$(dirname "$(readlink -f "$0")")
|
|
1
|
+
#!/usr/bin/env bash
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
5
4
|
|
|
6
|
-
#
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
SILENT=false
|
|
28
|
-
BASE_IRI=""
|
|
29
|
-
DEBUG=false
|
|
30
|
-
SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js --summary"
|
|
31
|
-
PATHS=()
|
|
32
|
-
LIBS=()
|
|
33
|
-
# USAGE: ./tuner.sh --debug --version ...paths
|
|
34
|
-
while [ $# -gt 0 ]; do
|
|
35
|
-
case "$1" in
|
|
36
|
-
--debug)
|
|
37
|
-
DEBUG=true
|
|
38
|
-
shift
|
|
39
|
-
;;
|
|
40
|
-
--silent)
|
|
41
|
-
SILENT=true
|
|
42
|
-
shift
|
|
43
|
-
;;
|
|
44
|
-
--raw)
|
|
45
|
-
SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js"
|
|
46
|
-
shift
|
|
47
|
-
;;
|
|
48
|
-
--base-iri)
|
|
49
|
-
BASE_IRI="$2"
|
|
50
|
-
shift
|
|
51
|
-
shift
|
|
52
|
-
;;
|
|
53
|
-
--version)
|
|
54
|
-
version
|
|
55
|
-
exit 0
|
|
56
|
-
;;
|
|
57
|
-
--help)
|
|
58
|
-
usage
|
|
59
|
-
exit 0
|
|
60
|
-
;;
|
|
61
|
-
--lib)
|
|
62
|
-
LIBS+=("$2")
|
|
63
|
-
shift
|
|
64
|
-
shift
|
|
65
|
-
;;
|
|
66
|
-
*)
|
|
67
|
-
PATHS+=("$1")
|
|
68
|
-
shift
|
|
69
|
-
;;
|
|
70
|
-
esac
|
|
71
|
-
done
|
|
72
|
-
|
|
73
|
-
# if no paths
|
|
74
|
-
if [ ${#PATHS[@]} -eq 0 ]; then
|
|
75
|
-
usage
|
|
76
|
-
exit 1
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
ARGS="--quiet --nope --pass"
|
|
80
|
-
|
|
81
|
-
if [ "$DEBUG" = true ]; then
|
|
82
|
-
ARGS="$ARGS ${SCRIPT_PATH}/../logging/debug.n3"
|
|
83
|
-
fi
|
|
84
|
-
|
|
85
|
-
if [ "$SILENT" != true ]; then
|
|
86
|
-
ARGS="$ARGS ${SCRIPT_PATH}/../logging/info.n3"
|
|
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" "$@"
|
|
13
|
+
else
|
|
14
|
+
# use plain node
|
|
15
|
+
node "$tuner" "$@"
|
|
87
16
|
fi
|
|
88
|
-
|
|
89
|
-
set -o pipefail
|
|
90
|
-
for path in "${PATHS[@]}"; do
|
|
91
|
-
(
|
|
92
|
-
node "${SCRIPT_PATH}/../lib/parse-test-case.js" --base-iri "$BASE_IRI" -- "${path}" \
|
|
93
|
-
| $eye $ARGS "${SCRIPT_PATH}"/../rules/*.n3 ${LIBS[@]:+${LIBS[*]}} -
|
|
94
|
-
) ;
|
|
95
|
-
done | $SUMMARY
|
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
|
+
});
|
package/lib/merge-curl-output.js
CHANGED
|
@@ -1,60 +1,54 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises'
|
|
2
|
-
import { createReadStream } from 'node:fs'
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
})()
|
package/lib/parse-test-case.js
CHANGED
|
@@ -1,39 +1,20 @@
|
|
|
1
|
-
import * as url from 'node:url'
|
|
2
|
-
import { createReadStream } from 'node:fs'
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
+
}
|
package/lib/summarise-results.js
CHANGED
|
@@ -1,65 +1,33 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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.
|
|
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