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 ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../index.js'
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../lib/merge-curl-output.js'
@@ -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="$SCRIPT_DIR/index.js"
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
- process.stdout.write(suiteHeader + await getStream(rawPassThrough));
87
+ const header = options.silent ? '' : `\n⚡️ SUITE <file://${absolutePath}>\n`;
88
+ process.stdout.write(header + await getStream(rawPassThrough));
88
89
  }
89
- else {
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
- process.stderr.write(suiteHeader + stderr.replace(/"(\w+)" TRACE "(.*)"/g, (_, level, text) => {
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: `\n🔎 SUITE <file://${absolutePath}>\n${validationResult.summary}`,
97
- success: result.success && validationResult.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.0",
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/tuner.sh",
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
- #"/tmp/"
20
+ ?tempDir
21
+ "/"
19
22
  ?uri!log:uuid
20
23
  ?suffix
21
24
  ) string:concatenation ?path .
@@ -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
- })()
@@ -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
- }
@@ -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
- }