api-tuner 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zazuko GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # 🎛️ API Tun3r 🎛️
2
+
3
+ **API** **T**ests **U**sing **n3** **R**ules
4
+
5
+ ## Prerequisites
6
+
7
+ - [SWI Prolog](https://www.swi-prolog.org/Download.html)
8
+
9
+ ## Installation
10
+
11
+ `npm i api-tuner`
12
+
13
+ ## Usage
14
+
15
+ ```sh
16
+ > api-tuner --help
17
+ Usage: api-tuner [options] <path>...
18
+
19
+ Options:
20
+ --silent Less output
21
+ --debug Enable debug output
22
+ --raw Output raw results from eye
23
+ --base-iri <iri> Specify the base IRI for parsing the test case files
24
+ --version Show version information
25
+ --help Show this help message
26
+ ```
27
+
28
+ ## Example
29
+
30
+ Create a test case file `test.n3`:
31
+
32
+ ```turtle
33
+ # test.n3
34
+ PREFIX : <http://example.com/>
35
+ PREFIX earl: <http://www.w3.org/ns/earl#>
36
+ PREFIX tuner: <https://api-tuner.described.at/>
37
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
38
+ PREFIX log: <http://www.w3.org/2000/10/swap/log#>
39
+ PREFIX string: <http://www.w3.org/2000/10/swap/string#>
40
+
41
+ :getExampleDotCom
42
+ a earl:TestCase ;
43
+ rdfs:label "Simple GET test" ;
44
+ .
45
+
46
+ # Configure a request
47
+ :req
48
+ a tuner:Request ;
49
+ tuner:url <http://localhost:1080/example.com> ;
50
+ tuner:method "GET" ;
51
+ .
52
+
53
+ {
54
+ # Execute the request and capture its response
55
+ :req tuner:response ?res .
56
+
57
+ # Check the response status code and content type
58
+ ?res tuner:http_code 200 ;
59
+ tuner:header ( "content-type" "text/html" ) ;
60
+ .
61
+
62
+ # Check the body contains the work "Example"
63
+ ?res!tuner:body string:contains "Example Domain" .
64
+ } => {
65
+ # Use te EARL vocabulary to assert the test passed
66
+ :getExampleDotCom earl:outcome earl:passed .
67
+ } .
68
+ ```
69
+
70
+ Execute the test case:
71
+
72
+ ```sh
73
+ api-tuner test.n3
74
+ ```
75
+
76
+ ## More examples
77
+
78
+ TBD
package/bin/tuner.sh ADDED
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ SCRIPT_PATH=$(dirname "$(readlink -f "$0")")
3
+
4
+ eye="swipl -x ${SCRIPT_PATH}/../eye/lib/eye.pvm --"
5
+
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
+ }
13
+
14
+ function usage() {
15
+ echo "Usage: api-tuner [options] <path>..."
16
+ echo ""
17
+ echo "Options:"
18
+ echo " --silent Less output"
19
+ echo " --debug Enable debug output"
20
+ echo " --raw Output raw results from eye"
21
+ echo " --base-iri <iri> Specify the base IRI for parsing the test case files"
22
+ echo " --version Show version information"
23
+ echo " --help Show this help message"
24
+ }
25
+
26
+ SILENT=false
27
+ BASE_IRI=""
28
+ DEBUG=false
29
+ SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js --summary"
30
+ PATHS=()
31
+ # USAGE: ./tuner.sh --debug --version ...paths
32
+ while [ $# -gt 0 ]; do
33
+ case "$1" in
34
+ --debug)
35
+ DEBUG=true
36
+ shift
37
+ ;;
38
+ --silent)
39
+ SILENT=true
40
+ shift
41
+ ;;
42
+ --raw)
43
+ SUMMARY="node ${SCRIPT_PATH}/../lib/summarise-results.js"
44
+ shift
45
+ ;;
46
+ --base-iri)
47
+ BASE_IRI="$2"
48
+ shift
49
+ shift
50
+ ;;
51
+ --version)
52
+ version
53
+ exit 0
54
+ ;;
55
+ --help)
56
+ usage
57
+ exit 0
58
+ ;;
59
+ *)
60
+ PATHS+=("$1")
61
+ shift
62
+ ;;
63
+ esac
64
+ done
65
+
66
+ # if no paths
67
+ if [ ${#PATHS[@]} -eq 0 ]; then
68
+ usage
69
+ exit 1
70
+ fi
71
+
72
+ ARGS="--quiet --nope --pass"
73
+
74
+ if [ "$DEBUG" = true ]; then
75
+ ARGS="$ARGS ${SCRIPT_PATH}/../logging/debug.n3"
76
+ fi
77
+
78
+ if [ "$SILENT" != true ]; then
79
+ ARGS="$ARGS ${SCRIPT_PATH}/../logging/info.n3"
80
+ fi
81
+
82
+ MERGED="$(mktemp)"
83
+ if [ -n "$BASE_IRI" ]; then
84
+ echo "base <$BASE_IRI>" > "$MERGED"
85
+ fi
86
+
87
+ for path in "${PATHS[@]}"; do
88
+ cat "$path" >> "$MERGED"
89
+ done
90
+
91
+ $eye $ARGS "${SCRIPT_PATH}"/../rules/*.n3 "${MERGED}" | $SUMMARY
@@ -0,0 +1,4 @@
1
+ {
2
+ "response": %{json},
3
+ "headers": %{header_json}
4
+ }
@@ -0,0 +1,6 @@
1
+ curl -sL https://github.com/eyereasoner/eye/archive/refs/tags/v"${EYE_VERSION}".tar.gz -o eye.tar.gz
2
+
3
+ tar -xvzf eye.tar.gz
4
+ rm eye.tar.gz
5
+
6
+ mv eye-"${EYE_VERSION}" eye
@@ -0,0 +1,60 @@
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
+
7
+ const ns = rdf.namespace('https://api-tuner.described.at/');
8
+
9
+ (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))
44
+ }
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)
50
+ }
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
+ })()
@@ -0,0 +1 @@
1
+ sed -e 's/\\"/"/g'
package/lib/shapes.ttl ADDED
@@ -0,0 +1,14 @@
1
+ PREFIX earl: <http://www.w3.org/ns/earl#>
2
+ PREFIX sh: <http://www.w3.org/ns/shacl#>
3
+
4
+ [
5
+ a sh:NodeShape ;
6
+ sh:targetClass earl:TestCase ;
7
+ sh:property
8
+ [
9
+ sh:path earl:outcome ;
10
+ sh:hasValue earl:passed ;
11
+ sh:maxCount 1 ;
12
+ sh:message "Test failed" ;
13
+ ] ;
14
+ ] .
@@ -0,0 +1,44 @@
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 testCases = rdf.clownface({ dataset: data }).has(rdf.ns.rdf.type, rdf.ns.earl.TestCase)
32
+
33
+ for (const testCase of testCases.toArray()) {
34
+ const result = validationReport.results.find(result => result.focusNode.equals(testCase.term))
35
+ if (result?.severity.equals(rdf.ns.sh.Violation)) {
36
+ process.stdout.write(`❌ FAIL <${testCase.value}>\n`)
37
+ } else {
38
+ process.stdout.write(`✅ PASS <${testCase.value}>\n`)
39
+ }
40
+ }
41
+ }
42
+
43
+ process.exit(validationReport.conforms ? 0 : 1)
44
+ })()
@@ -0,0 +1,4 @@
1
+ PREFIX e: <http://eulersharp.sourceforge.net/2003/03swap/log-rules#>
2
+ PREFIX tuner: <https://api-tuner.described.at/>
3
+
4
+ [ tuner:logLevel 'debug' ] .
@@ -0,0 +1,4 @@
1
+ PREFIX e: <http://eulersharp.sourceforge.net/2003/03swap/log-rules#>
2
+ PREFIX tuner: <https://api-tuner.described.at/>
3
+
4
+ [ tuner:logLevel 'info' ] .
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "api-tuner",
3
+ "version": "0.1.0",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "api-tuner": "./bin/tuner.sh"
9
+ },
10
+ "scripts": {
11
+ "download-eye": "EYE_VERSION=11.5.2 ./lib/download-eye.sh",
12
+ "postinstall": "([ -d eye ] || npm run download-eye) && eye/install.sh --prefix=eye",
13
+ "prepare": "husky",
14
+ "lint": "eslint . --quiet --ignore-path .gitignore",
15
+ "test": "./bin/tuner.sh tests/*.n3 tests/**/*.n3",
16
+ "release": "changeset publish"
17
+ },
18
+ "files": [
19
+ "bin/tuner.sh",
20
+ "logging",
21
+ "lib",
22
+ "rules"
23
+ ],
24
+ "dependencies": {
25
+ "@changesets/cli": "^2.27.12",
26
+ "@jeswr/pretty-turtle": "^1.5.0",
27
+ "@zazuko/env-node": "^2.1.4",
28
+ "jsonld": "^8.3.3",
29
+ "rdf-validate-shacl": "^0.5.6",
30
+ "yargs": "^17.7.2"
31
+ },
32
+ "devDependencies": {
33
+ "@rdfjs/types": "^1",
34
+ "@tpluscode/eslint-config": "^0.5.0",
35
+ "@types/jsonld": "^1.5.15",
36
+ "@types/rdf-validate-shacl": "^0.4.9",
37
+ "@types/yargs": "^17.0.33",
38
+ "husky": "^9.1.7",
39
+ "lint-staged": "^15.4.3"
40
+ },
41
+ "lint-staged": {
42
+ "*.{js,ts}": [
43
+ "eslint --fix --quiet"
44
+ ]
45
+ }
46
+ }
@@ -0,0 +1,58 @@
1
+ PREFIX tuner: <https://api-tuner.described.at/>
2
+ prefix string: <http://www.w3.org/2000/10/swap/string#>
3
+ prefix log: <http://www.w3.org/2000/10/swap/log#>
4
+
5
+ {
6
+ ?res tuner:http_code ?value .
7
+ } <= {
8
+ ?res log:includes {
9
+ [] a tuner:Response ;
10
+ tuner:http_code ?actualValue .
11
+ } .
12
+
13
+ (
14
+ { ?actualValue log:equalTo ?value }
15
+ true
16
+ {
17
+ ("Expected status code " ?value " but got " ?actualValue)!string:concatenation^tuner:info .
18
+ true log:equalTo false .
19
+ }
20
+ ) log:ifThenElseIn ?SCOPE .
21
+ } .
22
+
23
+ {
24
+ ?res tuner:body ?body .
25
+ } <= {
26
+ ?res log:includes {
27
+ [] a tuner:Response ; tuner:body ?body .
28
+ } .
29
+ } .
30
+
31
+ {
32
+ ?res tuner:header ( ?name ?value ) .
33
+ } <= {
34
+ ?res tuner:header ( ?name ?value log:equalTo ) .
35
+ }.
36
+
37
+ {
38
+ ?res tuner:header ( ?name ?value ?builtIn ) .
39
+ } <= {
40
+ ?name string:lowerCase ?nameLower .
41
+
42
+ ?res log:includes {
43
+ [] a tuner:Response ;
44
+ tuner:headers [
45
+ tuner:name ?nameLower ;
46
+ tuner:value ?actualValue ;
47
+ ] .
48
+ } .
49
+
50
+ (
51
+ { ?actualValue ?builtIn ?value }
52
+ true
53
+ {
54
+ ("Expected header '" ?name "' '" ?value "' to satisfy '" ?builtIn "' but got '" ?actualValue "'")!string:concatenation^tuner:info .
55
+ true log:equalTo false .
56
+ }
57
+ ) log:ifThenElseIn ?SCOPE .
58
+ } .
@@ -0,0 +1,58 @@
1
+ PREFIX list: <http://www.w3.org/2000/10/swap/list#>
2
+ PREFIX tuner: <https://api-tuner.described.at/>
3
+ PREFIX string: <http://www.w3.org/2000/10/swap/string#>
4
+ PREFIX log: <http://www.w3.org/2000/10/swap/log#>
5
+ prefix file: <http://www.w3.org/2000/10/swap/file#>
6
+ prefix e: <http://eulersharp.sourceforge.net/2003/03swap/log-rules#>
7
+
8
+ {
9
+ ?body </#body> ( ?curlArgs ?requestBodyFile ) .
10
+ } <= {
11
+ ?body log:rawType log:Formula .
12
+ ?body log:n3String ?serialized .
13
+ () file:temp ?requestBodyFile .
14
+
15
+ # Save body to file so that it can be used in the curl command reliably
16
+ (
17
+ "echo '" ?serialized "' | "
18
+ # need to replace quotes around string
19
+ ( "lib/replace-quotes.sh"!file:libPath )!string:concatenation
20
+ " > " ?requestBodyFile
21
+ )!string:concatenation!e:exec .
22
+
23
+ ( " -H 'Content-Type:text/turtle' --data-binary @" ?requestBodyFile ) string:concatenation ?curlArgs .
24
+ } .
25
+
26
+ {
27
+ ?fileUrl </#body> ( ?curlArgs [] ) .
28
+ } <= {
29
+ ( " --data-binary " ?fileUrl!file:curlFileReference ) string:concatenation ?curlArgs .
30
+ } .
31
+
32
+ {
33
+ ?multipartBody </#body> ( ?curlArgs [] ) .
34
+ } <= {
35
+ ?multipartBody!log:rawType list:in ( log:LabeledBlankNode log:UnlabeledBlankNode ) .
36
+
37
+ (
38
+ ?formField
39
+ {
40
+ ?multipartBody tuner:form ( ?name ?value ) .
41
+
42
+ (
43
+ { ?value log:rawType log:Literal }
44
+ {
45
+ ( " -F " ?name "=" ?value ) string:concatenation ?formField .
46
+ }
47
+ {
48
+ ( " -F " ?name "=" ?value!file:curlFileReference ) string:concatenation ?formField .
49
+ }
50
+ ) log:ifThenElseIn [] .
51
+ }
52
+ ?formFields
53
+ ) log:collectAllIn [] .
54
+
55
+ (
56
+ ?formFields!string:concatenation
57
+ ) string:concatenation ?curlArgs .
58
+ } .
package/rules/files.n3 ADDED
@@ -0,0 +1,50 @@
1
+ prefix e: <http://eulersharp.sourceforge.net/2003/03swap/log-rules#>
2
+ prefix string: <http://www.w3.org/2000/10/swap/string#>
3
+ prefix log: <http://www.w3.org/2000/10/swap/log#>
4
+ prefix file: <http://www.w3.org/2000/10/swap/file#>
5
+
6
+ {
7
+ () file:temp ?path .
8
+ } <= {
9
+ ( "" ) file:temp ?path .
10
+ } .
11
+
12
+ {
13
+ ( ?suffix ) file:temp ?path .
14
+ } <= {
15
+ ?uri log:uri ( "urn:rand:" ( 1000 )!e:random )!string:concatenation .
16
+
17
+ (
18
+ #"/tmp/"
19
+ ?uri!log:uuid
20
+ ?suffix
21
+ ) string:concatenation ?path .
22
+ } .
23
+
24
+ {
25
+ ?path file:rm ?iDoNotCare .
26
+ } <= {
27
+ ( "rm " ?path )!string:concatenation!e:exec .
28
+ } .
29
+
30
+ {
31
+ ?relative file:libPath ?absolute .
32
+ } <= {
33
+ (
34
+ (<>!log:uri "/[^/]+$" "/")!string:replace 8
35
+ ) string:substring ?basePath .
36
+
37
+ (
38
+ ?basePath "../" ?relative
39
+ ) string:concatenation ?absolute .
40
+ } .
41
+
42
+ {
43
+ ?fileUrl file:curlFileReference ?ref .
44
+ } <= {
45
+ ?fileUrl log:uri ?fileUri .
46
+ ?fileUri string:startsWith "file:" .
47
+ ( ?fileUri 6 ) string:substring ?relative .
48
+
49
+ ( "@$(pwd)/" ?relative ) string:concatenation ?ref .
50
+ } .
@@ -0,0 +1,24 @@
1
+ PREFIX tuner: <https://api-tuner.described.at/>
2
+ prefix earl: <http://www.w3.org/ns/earl#>
3
+ prefix log: <http://www.w3.org/2000/10/swap/log#>
4
+ prefix string: <http://www.w3.org/2000/10/swap/string#>
5
+
6
+ {
7
+ ?left tuner:trace ?right .
8
+ } <= {
9
+ (
10
+ { [] tuner:logLevel 'debug' }
11
+ { 'DEBUG' log:trace ?right . }
12
+ true
13
+ ) log:ifThenElseIn ?SCOPE .
14
+ } .
15
+
16
+ {
17
+ ?left tuner:info ?right .
18
+ } <= {
19
+ (
20
+ { [] tuner:logLevel 'info' }
21
+ { 'INFO' log:trace ?right . }
22
+ true
23
+ ) log:ifThenElseIn ?SCOPE .
24
+ } .
@@ -0,0 +1,78 @@
1
+ PREFIX list: <http://www.w3.org/2000/10/swap/list#>
2
+ prefix e: <http://eulersharp.sourceforge.net/2003/03swap/log-rules#>
3
+ prefix log: <http://www.w3.org/2000/10/swap/log#>
4
+ prefix string: <http://www.w3.org/2000/10/swap/string#>
5
+ PREFIX tuner: <https://api-tuner.described.at/>
6
+ prefix file: <http://www.w3.org/2000/10/swap/file#>
7
+ prefix earl: <http://www.w3.org/ns/earl#>
8
+
9
+ {
10
+ (?fieldName ?fieldValue) tuner:headerArg ?curlArg .
11
+ } <= {
12
+ ( " -H '" ?fieldName ":" ?fieldValue "'" ) string:concatenation ?curlArg .
13
+ } .
14
+
15
+ {
16
+ ?req tuner:response ?res .
17
+ } <= {
18
+ {
19
+ ?req a tuner:Request .
20
+ ?req tuner:method ?method .
21
+ ?req tuner:url ?endpointUri .
22
+ } log:callWithOptional {
23
+ ?req tuner:body ?body .
24
+ } .
25
+ ({ ?req tuner:done true } false true) log:ifThenElseIn ?SCOPE .
26
+ true log:becomes { ?req tuner:done true } .
27
+
28
+ (
29
+ { ?req tuner:header [] . }
30
+ {
31
+ ( ?header { ?req tuner:header ?header } ?headers ) log:collectAllIn [] .
32
+ ( ?headers tuner:headerArg )!list:map string:concatenation ?headersArgs .
33
+ }
34
+ { ?headersArgs log:equalTo "" }
35
+ ) log:ifThenElseIn [] .
36
+
37
+ ?endpointUri log:uri ?endpoint .
38
+
39
+ ( ?method " " ?endpoint )!string:concatenation^tuner:trace .
40
+
41
+ () file:temp ?responseBodyFile .
42
+ ( ?responseBodyFile ".curl.json" ) string:concatenation ?responseHeadersFile .
43
+ ( ?responseBodyFile ".n3" ) string:concatenation ?responseFile .
44
+
45
+ ( "Calling " ?method " " ?endpoint )!string:concatenation^tuner:info .
46
+
47
+ (
48
+ { ?req tuner:body ?body }
49
+ { ?body </#body> ( ?bodyArgs ?requestBodyFile ) }
50
+ { ?bodyArgs log:equalTo "" }
51
+ ) log:ifThenElseIn [] .
52
+
53
+ (
54
+ "curl -s -X " ?method " '" ?endpoint "'"
55
+ ?headersArgs
56
+ ?bodyArgs
57
+ " -w @" ( "lib/curl-format.txt"!file:libPath )!string:concatenation
58
+ " -o " ?responseBodyFile
59
+ " > " ?responseHeadersFile
60
+ ) string:concatenation ?command .
61
+
62
+ ?command^tuner:trace .
63
+ ?command!e:exec .
64
+
65
+ (
66
+ "node " "lib/merge-curl-output.js"!file:libPath " "
67
+ ?responseBodyFile
68
+ " > " ?responseFile
69
+ )!string:concatenation!e:exec .
70
+
71
+ ("file://" ?responseFile)!string:concatenation^log:uri log:semantics ?res .
72
+ ?res^tuner:trace .
73
+
74
+ ( { ?requestBodyFile log:rawType log:Literal } { ?requestBodyFile!file:rm } true ) log:ifThenElseIn [] .
75
+ ?responseHeadersFile!file:rm .
76
+ ?responseBodyFile!file:rm .
77
+ ?responseFile!file:rm .
78
+ } .