api-tuner 0.3.0 → 0.3.3
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/index.js +2 -0
- package/bin/merge-curl-output.js +2 -0
- package/bin/merge-curl-output.sh +16 -0
- package/bin/tuner.sh +6 -1
- package/index.js +19 -10
- package/package.json +6 -3
- package/rules/files.n3 +4 -1
- package/lib/merge-curl-output.ts +0 -72
- package/lib/parse-test-case.ts +0 -25
- package/lib/summarise-results.ts +0 -46
package/bin/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
4
|
+
|
|
5
|
+
# find JS entrypoint
|
|
6
|
+
script="$SCRIPT_DIR/merge-curl-output.js"
|
|
7
|
+
|
|
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 "$script" "$@"
|
|
13
|
+
else
|
|
14
|
+
# use plain node
|
|
15
|
+
node "$script" "$@"
|
|
16
|
+
fi
|
package/bin/tuner.sh
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
|
|
3
|
+
WORKING_DIR=$(pwd)
|
|
4
|
+
|
|
3
5
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
6
|
+
cd "$SCRIPT_DIR" || exit
|
|
4
7
|
|
|
5
8
|
# find JS entrypoint
|
|
6
|
-
tuner
|
|
9
|
+
tuner=$(node -e "console.log(require.resolve('api-tuner/bin/index.js'))" 2> /dev/null)
|
|
10
|
+
|
|
11
|
+
cd "$WORKING_DIR" || exit
|
|
7
12
|
|
|
8
13
|
# if tsx exists in path
|
|
9
14
|
if command -v tsx > /dev/null 2>&1
|
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import summariseResults from './lib/summarise-results.js';
|
|
|
10
10
|
const eyePvmPath = url.fileURLToPath(new URL('eye/lib/eye.pvm', import.meta.url));
|
|
11
11
|
program
|
|
12
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.')
|
|
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.', (lib, arr) => [...arr, lib], [])
|
|
14
14
|
.option('--silent', 'Less output', false)
|
|
15
15
|
.option('--debug', 'Enable debug output', false)
|
|
16
16
|
.option('--raw', 'Output raw results from eyes')
|
|
@@ -45,7 +45,7 @@ const rulesPath = url.fileURLToPath(new URL('rules/*.n3', import.meta.url));
|
|
|
45
45
|
const levelIcon = {
|
|
46
46
|
INFO: 'ℹ️',
|
|
47
47
|
DEBUG: '🐞',
|
|
48
|
-
'Failed assertion': '❌',
|
|
48
|
+
'Failed assertion': '❌ Failed assertion',
|
|
49
49
|
};
|
|
50
50
|
async function processPath(path) {
|
|
51
51
|
return new Promise(resolve => {
|
|
@@ -56,6 +56,7 @@ async function processPath(path) {
|
|
|
56
56
|
'--',
|
|
57
57
|
...eyeArgs,
|
|
58
58
|
rulesPath,
|
|
59
|
+
...options.lib,
|
|
59
60
|
'-',
|
|
60
61
|
], {
|
|
61
62
|
shell: true,
|
|
@@ -77,24 +78,32 @@ async function processPath(path) {
|
|
|
77
78
|
const testSuites = program.args.map(async (path) => {
|
|
78
79
|
const absolutePath = resolve(process.cwd(), path);
|
|
79
80
|
const result = await processPath(path);
|
|
80
|
-
const suiteHeader = options.silent ? '' : `\n⚡️ SUITE <file://${absolutePath}>\n`;
|
|
81
81
|
const summaryPassThrough = new PassThrough();
|
|
82
82
|
const rawPassThrough = new PassThrough();
|
|
83
83
|
result.stdout.pipe(summaryPassThrough);
|
|
84
84
|
result.stdout.pipe(rawPassThrough);
|
|
85
85
|
const validationResult = await summariseResults(summaryPassThrough);
|
|
86
86
|
if (options.raw) {
|
|
87
|
-
|
|
87
|
+
const header = options.silent ? '' : `\n⚡️ SUITE <file://${absolutePath}>\n`;
|
|
88
|
+
process.stdout.write(header + await getStream(rawPassThrough));
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
return {
|
|
92
|
+
summary: `\n🔎 SUITE <file://${absolutePath}>\n❌ FAIL Test script failed`,
|
|
93
|
+
success: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
let summary = `\n🔎 SUITE <file://${absolutePath}>\n`;
|
|
97
|
+
if (!validationResult.success) {
|
|
90
98
|
const stderr = await getStream(result.stderr);
|
|
91
|
-
|
|
92
|
-
return `${levelIcon[level]} ${text}`;
|
|
93
|
-
})
|
|
99
|
+
summary += stderr.replace(/"([^"]*)" TRACE ("([^"]*)")?/gm, (_, level, quoted, text) => {
|
|
100
|
+
return `${levelIcon[level]} ${text || ''}`;
|
|
101
|
+
});
|
|
94
102
|
}
|
|
103
|
+
summary += validationResult.summary;
|
|
95
104
|
return {
|
|
96
|
-
summary
|
|
97
|
-
success:
|
|
105
|
+
summary,
|
|
106
|
+
success: validationResult.success,
|
|
98
107
|
};
|
|
99
108
|
});
|
|
100
109
|
Promise.all(testSuites).then((results) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-tuner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
"release": "changeset publish"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"bin
|
|
21
|
+
"bin",
|
|
22
22
|
"logging",
|
|
23
|
-
"lib",
|
|
23
|
+
"lib/*.js",
|
|
24
|
+
"lib/*.txt",
|
|
25
|
+
"lib/*.ttl",
|
|
26
|
+
"lib/*.sh",
|
|
24
27
|
"rules"
|
|
25
28
|
],
|
|
26
29
|
"dependencies": {
|
package/rules/files.n3
CHANGED
|
@@ -13,9 +13,12 @@ prefix file: <http://www.w3.org/2000/10/swap/file#>
|
|
|
13
13
|
( ?suffix ) file:temp ?path .
|
|
14
14
|
} <= {
|
|
15
15
|
?uri log:uri ( "urn:rand:" ( 1000 )!e:random )!string:concatenation .
|
|
16
|
+
# log:shell captures the traling newline
|
|
17
|
+
( "mktemp -d"!log:shell "\n" "" ) string:replace ?tempDir .
|
|
16
18
|
|
|
17
19
|
(
|
|
18
|
-
|
|
20
|
+
?tempDir
|
|
21
|
+
"/"
|
|
19
22
|
?uri!log:uuid
|
|
20
23
|
?suffix
|
|
21
24
|
) string:concatenation ?path .
|
package/lib/merge-curl-output.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
}
|