eyeling 1.26.7 → 1.27.1
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/HANDBOOK.md +32 -1
- package/dist/browser/eyeling.browser.js +519 -3
- package/examples/deck/act-barley-seed-lineage.md +1 -1
- package/examples/deck/faltings-genus2-finiteness.md +1 -1
- package/examples/deck/high-trust-rdf-bloom-envelope.md +1 -1
- package/examples/deck/odrl-dpv-risk-ranked.md +1 -1
- package/examples/deck/rdf-message-cold-chain-recall.md +71 -0
- package/examples/deck/rdf-message-flow.md +1 -1
- package/examples/deck/rdf-message-ldes-incremental.md +94 -0
- package/examples/deck/rdf-message-window-repair.md +1 -1
- package/examples/deck/schema-foaf-mapping.md +2 -0
- package/examples/input/rdf-message-cold-chain-recall.trig +668 -0
- package/examples/input/rdf-message-flow.trig +3 -0
- package/examples/input/rdf-message-ldes-incremental.trig +564 -0
- package/examples/input/rdf-message-microgrid.trig +3 -0
- package/examples/input/rdf-message-window-repair.trig +3 -0
- package/examples/input/rdf-messages.trig +3 -0
- package/examples/output/rdf-message-cold-chain-recall.md +13 -0
- package/examples/output/rdf-message-ldes-incremental.md +13 -0
- package/examples/rdf-message-cold-chain-recall.n3 +229 -0
- package/examples/rdf-message-flow.n3 +3 -0
- package/examples/rdf-message-ldes-incremental.n3 +231 -0
- package/examples/rdf-message-microgrid.n3 +3 -0
- package/examples/rdf-message-window-repair.n3 +3 -0
- package/examples/rdf-messages.n3 +3 -0
- package/eyeling.js +519 -3
- package/lib/cli.js +519 -3
- package/package.json +5 -3
- package/test/examples.test.js +25 -14
- package/test/fixtures/marc-rules-stream-messages.n3 +192 -0
- package/test/stream_messages.test.js +284 -0
package/test/examples.test.js
CHANGED
|
@@ -212,6 +212,8 @@ function runExampleToFile({ root, examplesDir, eyelingJsPath, nodePath, file, ge
|
|
|
212
212
|
|
|
213
213
|
function main() {
|
|
214
214
|
const suiteStart = Date.now();
|
|
215
|
+
const proofOnly = process.argv.includes('--proof-only');
|
|
216
|
+
|
|
215
217
|
|
|
216
218
|
// test/examples.test.js -> repo root is one level up
|
|
217
219
|
const root = path.resolve(__dirname, '..');
|
|
@@ -230,11 +232,13 @@ function main() {
|
|
|
230
232
|
process.exit(1);
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
const files =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
const files = proofOnly
|
|
236
|
+
? []
|
|
237
|
+
: fs
|
|
238
|
+
.readdirSync(examplesDir)
|
|
239
|
+
.filter((f) => f.endsWith('.n3'))
|
|
240
|
+
.sort((a, b) => a.localeCompare(b));
|
|
241
|
+
const proofFiles = proofOnly && fs.existsSync(proofDir)
|
|
238
242
|
? fs
|
|
239
243
|
.readdirSync(proofDir)
|
|
240
244
|
.filter((f) => f.endsWith('.n3') && fs.existsSync(path.join(examplesDir, f)))
|
|
@@ -242,11 +246,13 @@ function main() {
|
|
|
242
246
|
: [];
|
|
243
247
|
const totalTests = files.length + proofFiles.length;
|
|
244
248
|
|
|
245
|
-
info(
|
|
249
|
+
info(proofOnly
|
|
250
|
+
? `Running ${proofFiles.length} proof golden tests`
|
|
251
|
+
: `Running ${files.length} examples tests`);
|
|
246
252
|
console.log(`${C.dim}${getEyelingVersion(nodePath, eyelingJsPath, root)}; node ${process.version}${C.n}`);
|
|
247
253
|
|
|
248
|
-
if (
|
|
249
|
-
ok('No .n3 files found in examples/');
|
|
254
|
+
if (totalTests === 0) {
|
|
255
|
+
ok(proofOnly ? 'No proof goldens found in examples/proof/' : 'No .n3 files found in examples/');
|
|
250
256
|
process.exit(0);
|
|
251
257
|
}
|
|
252
258
|
|
|
@@ -256,6 +262,7 @@ function main() {
|
|
|
256
262
|
// Pretty, stable numbering (e.g., 001..100 when running 100 tests)
|
|
257
263
|
const idxWidth = String(files.length).length;
|
|
258
264
|
const proofIdxWidth = String(Math.max(proofFiles.length, 1)).length;
|
|
265
|
+
const proofLabel = proofOnly ? '' : 'proof ';
|
|
259
266
|
|
|
260
267
|
for (let i = 0; i < files.length; i++) {
|
|
261
268
|
const idx = String(i + 1).padStart(idxWidth, '0');
|
|
@@ -357,7 +364,7 @@ function main() {
|
|
|
357
364
|
n3Text = fs.readFileSync(filePath, 'utf8');
|
|
358
365
|
} catch (e) {
|
|
359
366
|
const ms = Date.now() - start;
|
|
360
|
-
fail(
|
|
367
|
+
fail(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
|
|
361
368
|
fail(`Cannot read proof input: ${e.message}`);
|
|
362
369
|
failed++;
|
|
363
370
|
continue;
|
|
@@ -381,13 +388,13 @@ function main() {
|
|
|
381
388
|
|
|
382
389
|
if (diffOk && rcOk) {
|
|
383
390
|
if (expectedRc === 0) {
|
|
384
|
-
ok(
|
|
391
|
+
ok(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
|
|
385
392
|
} else {
|
|
386
|
-
ok(
|
|
393
|
+
ok(`${proofLabel}${idx} ${file} (expected exit ${expectedRc}) ${msTag(ms)}`);
|
|
387
394
|
}
|
|
388
395
|
passed++;
|
|
389
396
|
} else {
|
|
390
|
-
fail(
|
|
397
|
+
fail(`${proofLabel}${idx} ${file} ${msTag(ms)}`);
|
|
391
398
|
if (!rcOk) {
|
|
392
399
|
fail(`Exit code ${rc}, expected ${expectedRc}`);
|
|
393
400
|
}
|
|
@@ -410,10 +417,14 @@ function main() {
|
|
|
410
417
|
info(`Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
|
|
411
418
|
|
|
412
419
|
if (failed === 0) {
|
|
413
|
-
ok(
|
|
420
|
+
ok(proofOnly
|
|
421
|
+
? `All proof golden tests passed (${passed}/${totalTests})`
|
|
422
|
+
: `All examples tests passed (${passed}/${totalTests})`);
|
|
414
423
|
process.exit(0);
|
|
415
424
|
} else {
|
|
416
|
-
fail(
|
|
425
|
+
fail(proofOnly
|
|
426
|
+
? `Some proof golden tests failed (${passed}/${totalTests})`
|
|
427
|
+
: `Some examples tests failed (${passed}/${totalTests})`);
|
|
417
428
|
// keep exit code 2 (matches historical behavior of examples/test)
|
|
418
429
|
process.exit(2);
|
|
419
430
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# MARC extraction rules for the RDF Messages demo (see marc-sample.messages.nt).
|
|
2
|
+
#
|
|
3
|
+
# Each message is one bibliographic record shaped as a nested list of MARC
|
|
4
|
+
# fields: <record> :record (("245" "1" "0" "a" "Title…") ("650" …) …), where an
|
|
5
|
+
# inner list is (tag ind1 ind2 code value code value …). These backward helper
|
|
6
|
+
# rules (marc:field / marc:subf / marc:map / marc:join / marc:id) walk that
|
|
7
|
+
# structure to pull a subfield's value by (tag, subfield-code); the forward
|
|
8
|
+
# rules at the bottom emit :title / :subject / :type per record.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
@prefix : <http://example.org/ns#> .
|
|
12
|
+
@prefix list: <http://www.w3.org/2000/10/swap/list#> .
|
|
13
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
14
|
+
@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#> .
|
|
15
|
+
@prefix string: <http://www.w3.org/2000/10/swap/string#> .
|
|
16
|
+
@prefix math: <http://www.w3.org/2000/10/swap/math#> .
|
|
17
|
+
@prefix marc: <https://codeberg.org/phochste/marcattacks#> .
|
|
18
|
+
|
|
19
|
+
## ---- Helper functions (backward rules) ----
|
|
20
|
+
|
|
21
|
+
# marc:splice - drop the first ?Idx members of a list (here: the tag + indicators)
|
|
22
|
+
{ (?List ?Idx) marc:splice ?Result }
|
|
23
|
+
<=
|
|
24
|
+
{
|
|
25
|
+
( ?X {
|
|
26
|
+
?List list:iterate (?Num ?X).
|
|
27
|
+
?Num math:notLessThan ?Idx.
|
|
28
|
+
} ?Result ) log:collectAllIn _:x.
|
|
29
|
+
}.
|
|
30
|
+
|
|
31
|
+
# marc:join - join a list with a separator
|
|
32
|
+
{ (?List ?Sep) marc:join ?Result }
|
|
33
|
+
<=
|
|
34
|
+
{
|
|
35
|
+
(?List ?Sep "") marc:join ?Result.
|
|
36
|
+
}.
|
|
37
|
+
|
|
38
|
+
{ ( () ?Sep ?Acc ) marc:join ?Acc }
|
|
39
|
+
<= true.
|
|
40
|
+
|
|
41
|
+
{ ( ?List ?Sep ?Acc ) marc:join ?Result }
|
|
42
|
+
<=
|
|
43
|
+
{
|
|
44
|
+
?List list:firstRest (?H ?T).
|
|
45
|
+
?Acc log:equalTo "".
|
|
46
|
+
( ?T ?Sep ?H ) marc:join ?Result .
|
|
47
|
+
}.
|
|
48
|
+
|
|
49
|
+
{ ( ?List ?Sep ?Acc ) marc:join ?Result }
|
|
50
|
+
<=
|
|
51
|
+
{
|
|
52
|
+
?List list:firstRest (?H ?T).
|
|
53
|
+
?Acc log:notEqualTo "".
|
|
54
|
+
( ?Acc ?Sep ?H) string:concatenation ?AccNew.
|
|
55
|
+
( ?T ?Sep ?AccNew ) marc:join ?Result .
|
|
56
|
+
}.
|
|
57
|
+
|
|
58
|
+
# marc:id - return the record id as an IRI (from the 001 control field)
|
|
59
|
+
{ ?Record marc:id ?Result }
|
|
60
|
+
<=
|
|
61
|
+
{
|
|
62
|
+
(?Record "001") marc:field0 ?F001.
|
|
63
|
+
?F001 marc:ctrl ?ID.
|
|
64
|
+
( "http://lib.ugent.be/record/" ?ID ) string:concatenation ?IRI_ID.
|
|
65
|
+
?Result log:uri ?IRI_ID.
|
|
66
|
+
}.
|
|
67
|
+
|
|
68
|
+
# marc:ctrl - return the control value of a field
|
|
69
|
+
{ ?Field marc:ctrl ?Result }
|
|
70
|
+
<=
|
|
71
|
+
{
|
|
72
|
+
(?Field 3) list:memberAt "_" .
|
|
73
|
+
(?Field 4) list:memberAt ?Result.
|
|
74
|
+
}.
|
|
75
|
+
|
|
76
|
+
# marc:subf - return all values matching a subfield regex
|
|
77
|
+
{ ( ?Field ?Regex) marc:subf ?Result }
|
|
78
|
+
<=
|
|
79
|
+
{
|
|
80
|
+
( ?Field 3) marc:splice ?FieldData.
|
|
81
|
+
( ?FieldData ?Regex ()) marc:subf ?Result.
|
|
82
|
+
} .
|
|
83
|
+
|
|
84
|
+
{ ( () ?Regex ?Acc ) marc:subf ?Acc }
|
|
85
|
+
<= true.
|
|
86
|
+
|
|
87
|
+
{ ( ?FieldData ?Regex ?Acc ) marc:subf ?Result }
|
|
88
|
+
<=
|
|
89
|
+
{
|
|
90
|
+
?FieldData list:firstRest (?Subf ?Rest).
|
|
91
|
+
?Rest list:firstRest (?Value ?Tail).
|
|
92
|
+
?Subf string:matches ?Regex.
|
|
93
|
+
( ?Acc (?Value)) list:append ?Acc2.
|
|
94
|
+
( ?Tail ?Regex ?Acc2 ) marc:subf ?Result.
|
|
95
|
+
}.
|
|
96
|
+
|
|
97
|
+
{ ( ?FieldData ?Regex ?Acc ) marc:subf ?Result }
|
|
98
|
+
<=
|
|
99
|
+
{
|
|
100
|
+
?FieldData list:firstRest (?Subf ?Rest).
|
|
101
|
+
?Rest list:firstRest (?Value ?Tail).
|
|
102
|
+
?Subf string:notMatches ?Regex.
|
|
103
|
+
( ?Tail ?Regex ?Acc ) marc:subf ?Result.
|
|
104
|
+
}.
|
|
105
|
+
|
|
106
|
+
# marc:field0 - collect the first row of a marc field
|
|
107
|
+
{ ( ?Record ?Field) marc:field0 ?Result }
|
|
108
|
+
<=
|
|
109
|
+
{
|
|
110
|
+
( ?Record ?Field) marc:field ?F.
|
|
111
|
+
?F list:first ?Result.
|
|
112
|
+
}.
|
|
113
|
+
|
|
114
|
+
# marc:field - collect all data for a marc field
|
|
115
|
+
{ ( ?Record ?Field) marc:field ?Result}
|
|
116
|
+
<=
|
|
117
|
+
{
|
|
118
|
+
( ?Record ?Field ()) marc:field ?Result.
|
|
119
|
+
}.
|
|
120
|
+
|
|
121
|
+
{ ( () ?Field ?Acc ) marc:field ?Acc}
|
|
122
|
+
<= true.
|
|
123
|
+
|
|
124
|
+
{ ( ?L ?Field ?Acc ) marc:field ?Result }
|
|
125
|
+
<=
|
|
126
|
+
{
|
|
127
|
+
?L list:firstRest (?H ?T).
|
|
128
|
+
( ?H 0 ) list:memberAt ?Field.
|
|
129
|
+
( ?Acc (?H) ) list:append ?AccNew.
|
|
130
|
+
(?T ?Field ?AccNew) marc:field ?Result.
|
|
131
|
+
}.
|
|
132
|
+
|
|
133
|
+
{ (?L ?Field ?Acc) marc:field ?Result }
|
|
134
|
+
<=
|
|
135
|
+
{
|
|
136
|
+
?L list:firstRest (?H ?T).
|
|
137
|
+
( ?H 0 ) list:memberAt ?X.
|
|
138
|
+
?Field log:notEqualTo ?X.
|
|
139
|
+
(?T ?Field ?Acc) marc:field ?Result.
|
|
140
|
+
}.
|
|
141
|
+
|
|
142
|
+
# marc:map - join all values of (tag, subfield) into one string
|
|
143
|
+
{ ( ?Record ?Tag ?Subfield ) marc:map ?Result }
|
|
144
|
+
<=
|
|
145
|
+
{
|
|
146
|
+
(?Record ?Tag) marc:field ?FL.
|
|
147
|
+
?FL list:member ?F.
|
|
148
|
+
(?F ?Subfield) marc:subf ?T .
|
|
149
|
+
(?T " ") marc:join ?Result.
|
|
150
|
+
}.
|
|
151
|
+
|
|
152
|
+
## ---- Extraction rules (forward) ----
|
|
153
|
+
|
|
154
|
+
# In --stream-messages mode the current RDF Message payload is exposed as a
|
|
155
|
+
# quoted graph via eymsg:payloadGraph/log:nameOf. Match :record inside that
|
|
156
|
+
# payload graph, then reuse the ordinary MARC helper rules on the bound list.
|
|
157
|
+
|
|
158
|
+
{
|
|
159
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
160
|
+
?Payload log:nameOf ?PayloadContext.
|
|
161
|
+
?PayloadContext log:includes { ?X :record ?Record. }.
|
|
162
|
+
?Record marc:id ?ID.
|
|
163
|
+
(?Record "245" "a") marc:map ?Val.
|
|
164
|
+
}
|
|
165
|
+
=>
|
|
166
|
+
{
|
|
167
|
+
?ID :title ?Val.
|
|
168
|
+
}.
|
|
169
|
+
|
|
170
|
+
{
|
|
171
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
172
|
+
?Payload log:nameOf ?PayloadContext.
|
|
173
|
+
?PayloadContext log:includes { ?X :record ?Record. }.
|
|
174
|
+
?Record marc:id ?ID.
|
|
175
|
+
(?Record "650" "a") marc:map ?Val.
|
|
176
|
+
}
|
|
177
|
+
=>
|
|
178
|
+
{
|
|
179
|
+
?ID :subject ?Val.
|
|
180
|
+
}.
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
?Envelope eymsg:payloadGraph ?Payload.
|
|
184
|
+
?Payload log:nameOf ?PayloadContext.
|
|
185
|
+
?PayloadContext log:includes { ?X :record ?Record. }.
|
|
186
|
+
?Record marc:id ?ID.
|
|
187
|
+
(?Record "920" "a") marc:map ?Val.
|
|
188
|
+
}
|
|
189
|
+
=>
|
|
190
|
+
{
|
|
191
|
+
?ID :type ?Val.
|
|
192
|
+
}.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const cp = require('node:child_process');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const eyelingJsPath = path.join(root, 'eyeling.js');
|
|
11
|
+
|
|
12
|
+
const TTY = process.stdout.isTTY;
|
|
13
|
+
const C = TTY
|
|
14
|
+
? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
|
|
15
|
+
: { g: '', r: '', y: '', dim: '', n: '' };
|
|
16
|
+
|
|
17
|
+
function ok(msg) {
|
|
18
|
+
console.log(`${C.g}OK ${C.n} ${msg}`);
|
|
19
|
+
}
|
|
20
|
+
function info(msg) {
|
|
21
|
+
console.log(`${C.y}==${C.n} ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
function fail(msg) {
|
|
24
|
+
console.error(`${C.r}FAIL${C.n} ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
function numberedName(index, name) {
|
|
27
|
+
return `${String(index + 1).padStart(3, '0')} ${name}`;
|
|
28
|
+
}
|
|
29
|
+
function msNow() {
|
|
30
|
+
return Date.now();
|
|
31
|
+
}
|
|
32
|
+
function mkTmpDir() {
|
|
33
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-stream-messages-'));
|
|
34
|
+
}
|
|
35
|
+
function rmrf(p) {
|
|
36
|
+
try {
|
|
37
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runEyeling(args, opts = {}) {
|
|
42
|
+
return cp.spawnSync(process.execPath, [eyelingJsPath, ...args], {
|
|
43
|
+
cwd: root,
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
maxBuffer: opts.maxBuffer || 20 * 1024 * 1024,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function expectEyelingOk(args, opts = {}) {
|
|
49
|
+
const r = runEyeling(args, opts);
|
|
50
|
+
if (r.status === 0) return r.stdout;
|
|
51
|
+
throw new Error(
|
|
52
|
+
`eyeling failed with exit ${r.status}\n` +
|
|
53
|
+
`STDOUT:\n${r.stdout || ''}\n` +
|
|
54
|
+
`STDERR:\n${r.stderr || ''}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeScopedPayloadRules(file) {
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
file,
|
|
61
|
+
`@prefix : <urn:test#>.\n` +
|
|
62
|
+
`@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#>.\n` +
|
|
63
|
+
`@prefix log: <http://www.w3.org/2000/10/swap/log#>.\n` +
|
|
64
|
+
`{\n` +
|
|
65
|
+
` ?Envelope eymsg:payloadGraph ?Payload.\n` +
|
|
66
|
+
` ?Payload log:nameOf ?PayloadContext.\n` +
|
|
67
|
+
` ?PayloadContext log:includes { ?Subject :line ?Line. }.\n` +
|
|
68
|
+
`} => {\n` +
|
|
69
|
+
` ?Envelope log:outputString ?Line.\n` +
|
|
70
|
+
`}.\n`,
|
|
71
|
+
'utf8',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
function writeBasicMessageLog(file) {
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
file,
|
|
77
|
+
`VERSION "1.2-messages"\n` +
|
|
78
|
+
`PREFIX : <urn:test#>\n` +
|
|
79
|
+
`\n` +
|
|
80
|
+
`:a :line "one\\n".\n` +
|
|
81
|
+
`MESSAGE\n` +
|
|
82
|
+
`\n` +
|
|
83
|
+
`:b :line "two\\n".\n` +
|
|
84
|
+
`MESSAGE\n` +
|
|
85
|
+
`# empty heartbeat\n` +
|
|
86
|
+
`MESSAGE\n` +
|
|
87
|
+
`:c :line "three\\n".\n`,
|
|
88
|
+
'utf8',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function writeLargeMessageLog(file, count) {
|
|
92
|
+
let text = 'VERSION "1.2-messages"\nPREFIX : <urn:test#>\n';
|
|
93
|
+
for (let i = 1; i <= count; i += 1) {
|
|
94
|
+
text += `:m${i} :line "${i}\\n".\n`;
|
|
95
|
+
if (i < count) text += 'MESSAGE\n';
|
|
96
|
+
}
|
|
97
|
+
fs.writeFileSync(file, text, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
function writeMarcMessageLog(file) {
|
|
100
|
+
fs.writeFileSync(
|
|
101
|
+
file,
|
|
102
|
+
`VERSION "1.2-messages"\n` +
|
|
103
|
+
`PREFIX : <http://example.org/ns#>\n` +
|
|
104
|
+
`\n` +
|
|
105
|
+
`:record1 :record (\n` +
|
|
106
|
+
` ("001" "_" "_" "_" "42")\n` +
|
|
107
|
+
` ("245" "1" "0" "a" "Streaming RDF Messages")\n` +
|
|
108
|
+
` ("650" "_" "0" "a" "Semantic Web")\n` +
|
|
109
|
+
` ("920" "_" "_" "a" "book")\n` +
|
|
110
|
+
`).\n` +
|
|
111
|
+
`MESSAGE\n` +
|
|
112
|
+
`:record2 :record (\n` +
|
|
113
|
+
` ("001" "_" "_" "_" "43")\n` +
|
|
114
|
+
` ("245" "1" "0" "a" "Incremental MARC Extraction")\n` +
|
|
115
|
+
` ("650" "_" "0" "a" "Linked data")\n` +
|
|
116
|
+
` ("920" "_" "_" "a" "article")\n` +
|
|
117
|
+
`).\n`,
|
|
118
|
+
'utf8',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function startFileServer(file) {
|
|
123
|
+
const dir = path.dirname(file);
|
|
124
|
+
const script = path.join(dir, 'server.js');
|
|
125
|
+
const portFile = path.join(dir, 'server.port');
|
|
126
|
+
fs.writeFileSync(
|
|
127
|
+
script,
|
|
128
|
+
`const fs = require('node:fs');\n` +
|
|
129
|
+
`const http = require('node:http');\n` +
|
|
130
|
+
`const file = process.argv[2];\n` +
|
|
131
|
+
`const portFile = process.argv[3];\n` +
|
|
132
|
+
`const size = fs.statSync(file).size;\n` +
|
|
133
|
+
`const server = http.createServer((req, res) => {\n` +
|
|
134
|
+
` const headers = { 'content-type': 'text/plain', 'accept-ranges': 'bytes' };\n` +
|
|
135
|
+
` if (req.method === 'HEAD') { res.writeHead(200, { ...headers, 'content-length': size }); res.end(); return; }\n` +
|
|
136
|
+
` const range = req.headers.range;\n` +
|
|
137
|
+
` if (range) {\n` +
|
|
138
|
+
` const m = /^bytes=(\\d+)-(\\d*)$/.exec(range);\n` +
|
|
139
|
+
` const start = m ? Number(m[1]) : 0;\n` +
|
|
140
|
+
` const end = m && m[2] ? Math.min(Number(m[2]), size - 1) : size - 1;\n` +
|
|
141
|
+
` res.writeHead(206, { ...headers, 'content-range': 'bytes ' + start + '-' + end + '/' + size, 'content-length': end - start + 1 });\n` +
|
|
142
|
+
` fs.createReadStream(file, { start, end }).pipe(res);\n` +
|
|
143
|
+
` return;\n` +
|
|
144
|
+
` }\n` +
|
|
145
|
+
` res.writeHead(200, { ...headers, 'content-length': size });\n` +
|
|
146
|
+
` fs.createReadStream(file).pipe(res);\n` +
|
|
147
|
+
`});\n` +
|
|
148
|
+
`server.listen(0, '127.0.0.1', () => fs.writeFileSync(portFile, String(server.address().port)));\n`,
|
|
149
|
+
'utf8',
|
|
150
|
+
);
|
|
151
|
+
const child = cp.spawn(process.execPath, [script, file, portFile], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
152
|
+
const deadline = Date.now() + 5000;
|
|
153
|
+
while (Date.now() < deadline) {
|
|
154
|
+
if (fs.existsSync(portFile)) {
|
|
155
|
+
const port = fs.readFileSync(portFile, 'utf8').trim();
|
|
156
|
+
return { url: `http://127.0.0.1:${port}/messages.txt`, stop: () => child.kill() };
|
|
157
|
+
}
|
|
158
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
159
|
+
}
|
|
160
|
+
child.kill();
|
|
161
|
+
throw new Error('test HTTP server did not start');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const cases = [
|
|
165
|
+
{
|
|
166
|
+
name: 'scoped payload rules run once per RDF Message',
|
|
167
|
+
run(tmp) {
|
|
168
|
+
const rules = path.join(tmp, 'rules.n3');
|
|
169
|
+
const log = path.join(tmp, 'messages.trig');
|
|
170
|
+
writeScopedPayloadRules(rules);
|
|
171
|
+
writeBasicMessageLog(log);
|
|
172
|
+
const out = expectEyelingOk(['-r', '--stream-messages', rules, log]);
|
|
173
|
+
assert.equal(out, 'one\ntwo\nthree\n');
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'empty heartbeat messages do not leak previous payloads',
|
|
178
|
+
run(tmp) {
|
|
179
|
+
const rules = path.join(tmp, 'rules.n3');
|
|
180
|
+
const log = path.join(tmp, 'messages.trig');
|
|
181
|
+
writeScopedPayloadRules(rules);
|
|
182
|
+
writeBasicMessageLog(log);
|
|
183
|
+
const out = expectEyelingOk(['-r', '--stream-messages', rules, log]);
|
|
184
|
+
assert.equal(out.trim().split('\n').length, 3);
|
|
185
|
+
assert.ok(!out.includes('one\none'));
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'large message logs stream without requiring one monolithic dataset',
|
|
190
|
+
run(tmp) {
|
|
191
|
+
const rules = path.join(tmp, 'rules.n3');
|
|
192
|
+
const log = path.join(tmp, 'large.trig');
|
|
193
|
+
writeScopedPayloadRules(rules);
|
|
194
|
+
writeLargeMessageLog(log, 1000);
|
|
195
|
+
const out = expectEyelingOk(['-r', '--stream-messages', rules, log]);
|
|
196
|
+
const lines = out.trim().split('\n');
|
|
197
|
+
assert.equal(lines.length, 1000);
|
|
198
|
+
assert.equal(new Set(lines).size, 1000);
|
|
199
|
+
assert.ok(lines.includes('1'));
|
|
200
|
+
assert.ok(lines.includes('999'));
|
|
201
|
+
assert.ok(lines.includes('1000'));
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'remote text/plain RDF Message Logs are streamed via HTTP',
|
|
206
|
+
run(tmp) {
|
|
207
|
+
const rules = path.join(tmp, 'rules.n3');
|
|
208
|
+
const log = path.join(tmp, 'remote.trig');
|
|
209
|
+
writeScopedPayloadRules(rules);
|
|
210
|
+
writeLargeMessageLog(log, 25);
|
|
211
|
+
const server = startFileServer(log);
|
|
212
|
+
try {
|
|
213
|
+
const out = expectEyelingOk(['-r', '--stream-messages', server.url, rules]);
|
|
214
|
+
const lines = out.trim().split('\n');
|
|
215
|
+
assert.equal(lines.length, 25);
|
|
216
|
+
assert.equal(lines[0], '1');
|
|
217
|
+
assert.equal(lines[24], '25');
|
|
218
|
+
} finally {
|
|
219
|
+
server.stop();
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'MARC extraction rules fire over each streamed payload graph',
|
|
225
|
+
run(tmp) {
|
|
226
|
+
const log = path.join(tmp, 'marc.messages.txt');
|
|
227
|
+
writeMarcMessageLog(log);
|
|
228
|
+
const fixture = path.join(root, 'test', 'fixtures', 'marc-rules-stream-messages.n3');
|
|
229
|
+
const out = expectEyelingOk(['-r', '--stream-messages', fixture, log]);
|
|
230
|
+
assert.deepEqual(out.trim().split('\n').sort(), [
|
|
231
|
+
'<http://lib.ugent.be/record/42> <http://example.org/ns#subject> "Semantic Web" .',
|
|
232
|
+
'<http://lib.ugent.be/record/42> <http://example.org/ns#title> "Streaming RDF Messages" .',
|
|
233
|
+
'<http://lib.ugent.be/record/42> <http://example.org/ns#type> "book" .',
|
|
234
|
+
'<http://lib.ugent.be/record/43> <http://example.org/ns#subject> "Linked data" .',
|
|
235
|
+
'<http://lib.ugent.be/record/43> <http://example.org/ns#title> "Incremental MARC Extraction" .',
|
|
236
|
+
'<http://lib.ugent.be/record/43> <http://example.org/ns#type> "article" .',
|
|
237
|
+
]);
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: '--stream-messages requires RDF mode',
|
|
242
|
+
run(tmp) {
|
|
243
|
+
const rules = path.join(tmp, 'rules.n3');
|
|
244
|
+
const log = path.join(tmp, 'messages.trig');
|
|
245
|
+
writeScopedPayloadRules(rules);
|
|
246
|
+
writeBasicMessageLog(log);
|
|
247
|
+
const r = runEyeling(['--stream-messages', rules, log]);
|
|
248
|
+
assert.notEqual(r.status, 0);
|
|
249
|
+
assert.match(r.stderr, /requires -r\/--rdf/);
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
(function main() {
|
|
255
|
+
const suiteStart = msNow();
|
|
256
|
+
info(`Running ${cases.length} stream-message tests`);
|
|
257
|
+
let passed = 0;
|
|
258
|
+
let failed = 0;
|
|
259
|
+
for (const [index, tc] of cases.entries()) {
|
|
260
|
+
const tmp = mkTmpDir();
|
|
261
|
+
const testName = numberedName(index, tc.name);
|
|
262
|
+
const start = msNow();
|
|
263
|
+
try {
|
|
264
|
+
tc.run(tmp);
|
|
265
|
+
ok(`${testName} ${C.dim}(${msNow() - start} ms)${C.n}`);
|
|
266
|
+
passed++;
|
|
267
|
+
} catch (e) {
|
|
268
|
+
fail(`${testName} ${C.dim}(${msNow() - start} ms)${C.n}`);
|
|
269
|
+
fail(e && e.stack ? e.stack : String(e));
|
|
270
|
+
failed++;
|
|
271
|
+
} finally {
|
|
272
|
+
rmrf(tmp);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
console.log('');
|
|
276
|
+
const suiteMs = msNow() - suiteStart;
|
|
277
|
+
console.log(`${C.y}==${C.n} Total elapsed: ${suiteMs} ms (${(suiteMs / 1000).toFixed(2)} s)`);
|
|
278
|
+
if (failed === 0) {
|
|
279
|
+
ok(`All stream-message tests passed (${passed}/${cases.length})`);
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
282
|
+
fail(`Some stream-message tests failed (${passed}/${cases.length})`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
})();
|