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 CHANGED
@@ -1,95 +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
- 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
+ });
@@ -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.5",
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 .