1 | #!/usr/bin/env node
|
2 |
|
3 |
|
4 | const gobble = (s_text, s_indent='') => {
|
5 | let m_pad = /^(\s+)/.exec(s_text.replace(/^([ \t]*\n)/, ''));
|
6 | if(m_pad) {
|
7 | return s_indent+s_text.replace(new RegExp(`\\n${m_pad[1]}`, 'g'), '\n'+s_indent.trim()).trim();
|
8 | }
|
9 | else {
|
10 | return s_indent+s_text.trim();
|
11 | }
|
12 | };
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const ttl = {
|
18 |
|
19 | get read() {
|
20 |
|
21 | delete ttl.read;
|
22 | return ttl.read = require('@graphy-dev/content.ttl.read');
|
23 | },
|
24 |
|
25 | get scan() {
|
26 |
|
27 | delete ttl.scan;
|
28 | return ttl.scan = require('@graphy-dev/content.ttl.scan');
|
29 | },
|
30 |
|
31 | get write() {
|
32 |
|
33 | delete ttl.write;
|
34 | return ttl.write = require('@graphy-dev/content.ttl.write');
|
35 | },
|
36 | };
|
37 |
|
38 | const trig = {
|
39 |
|
40 | get read() {
|
41 |
|
42 | delete trig.read;
|
43 | return trig.read = require('@graphy-dev/content.trig.read');
|
44 | },
|
45 |
|
46 | get scan() {
|
47 |
|
48 | delete trig.scan;
|
49 | return trig.scan = require('@graphy-dev/content.trig.scan');
|
50 | },
|
51 |
|
52 | get write() {
|
53 |
|
54 | delete trig.write;
|
55 | return trig.write = require('@graphy-dev/content.trig.write');
|
56 | },
|
57 | };
|
58 |
|
59 | const nt = {
|
60 |
|
61 | get read() {
|
62 |
|
63 | delete nt.read;
|
64 | return nt.read = require('@graphy-dev/content.nt.read');
|
65 | },
|
66 |
|
67 | get scan() {
|
68 |
|
69 | delete nt.scan;
|
70 | return nt.scan = require('@graphy-dev/content.nt.scan');
|
71 | },
|
72 |
|
73 | get write() {
|
74 |
|
75 | delete nt.write;
|
76 | return nt.write = require('@graphy-dev/content.nt.write');
|
77 | },
|
78 | };
|
79 |
|
80 | const nq = {
|
81 |
|
82 | get read() {
|
83 |
|
84 | delete nq.read;
|
85 | return nq.read = require('@graphy-dev/content.nq.read');
|
86 | },
|
87 |
|
88 | get scan() {
|
89 |
|
90 | delete nq.scan;
|
91 | return nq.scan = require('@graphy-dev/content.nq.scan');
|
92 | },
|
93 |
|
94 | get write() {
|
95 |
|
96 | delete nq.write;
|
97 | return nq.write = require('@graphy-dev/content.nq.write');
|
98 | },
|
99 | };
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 | const H_CONTENT_MIMES = {
|
114 | 'text/turtle': ttl,
|
115 | 'application/trig': trig,
|
116 | 'application/n-triples': nt,
|
117 | 'application/n-quads': nq,
|
118 |
|
119 | };
|
120 |
|
121 | const H_CONTENT_TAGS = {
|
122 | ttl,
|
123 | trig,
|
124 | nt,
|
125 | nq,
|
126 |
|
127 | };
|
128 |
|
129 |
|
130 |
|
131 | const R_CONTENT_TYPE = /^((?:application|text)\/[^\0-\x20()<>@,;:\\"\/[\]?.=]+)(;.+)*$/i;
|
132 |
|
133 | const graphy = module.exports = Object.assign({
|
134 |
|
135 | content: Object.assign((s_query) => {
|
136 | if(s_query in H_CONTENT_TAGS) {
|
137 | return H_CONTENT_TAGS[s_query];
|
138 | }
|
139 |
|
140 | let m_content_type = R_CONTENT_TYPE.exec(s_query);
|
141 | if(!m_content_type) throw new Error(`invalid content-type string: "${s_query}"`);
|
142 | let [, s_content_type, s_parameters] = m_content_type;
|
143 | let s_content_type_normal = s_content_type.toLowerCase();
|
144 |
|
145 | if(s_content_type_normal in H_CONTENT_MIMES) {
|
146 | return H_CONTENT_MIMES[s_content_type_normal];
|
147 | }
|
148 | else {
|
149 | throw new Error(`no content handlers matched query for "${s_content_type_normal}"`);
|
150 | }
|
151 | }, {
|
152 | ttl,
|
153 | trig,
|
154 | nt,
|
155 | nq,
|
156 |
|
157 | }),
|
158 |
|
159 | core: {
|
160 | data: {
|
161 | get factory() {
|
162 |
|
163 | delete graphy.core.data.factory;
|
164 | return (graphy.core.data.factory = require('@graphy-dev/core.data.factory'));
|
165 | },
|
166 | },
|
167 | },
|
168 |
|
169 | get 'core.data.factory'() {
|
170 | delete graphy['core.data.factory'];
|
171 | return (graphy['core.data.factory'] = require('@graphy-dev/core.data.factory'));
|
172 | },
|
173 |
|
174 | get 'util.dataset.tree'() {
|
175 | delete graphy['util.dataset.tree'];
|
176 | return (graphy['util.dataset.tree'] = require('@graphy-dev/util.dataset.tree'));
|
177 | },
|
178 |
|
179 | util: {
|
180 | dataset: {
|
181 | get tree() {
|
182 |
|
183 | delete graphy.util.dataset.tree;
|
184 | return (graphy.util.dataset.tree = require('@graphy-dev/util.dataset.tree'));
|
185 | },
|
186 | },
|
187 | },
|
188 |
|
189 |
|
190 | get 'content.ttl.read'() {
|
191 |
|
192 | delete graphy['content.ttl.read'];
|
193 | return graphy['content.ttl.read'] = require('@graphy-dev/content.ttl.read');
|
194 | },
|
195 |
|
196 | get 'content.ttl.write'() {
|
197 |
|
198 | delete graphy['content.ttl.write'];
|
199 | return graphy['content.ttl.write'] = require('@graphy-dev/content.ttl.write');
|
200 | },
|
201 |
|
202 | get 'content.trig.read'() {
|
203 |
|
204 | delete graphy['content.trig.read'];
|
205 | return graphy['content.trig.read'] = require('@graphy-dev/content.trig.read');
|
206 | },
|
207 |
|
208 | get 'content.trig.write'() {
|
209 |
|
210 | delete graphy['content.trig.write'];
|
211 | return graphy['content.trig.write'] = require('@graphy-dev/content.trig.write');
|
212 | },
|
213 |
|
214 | get 'content.nt.read'() {
|
215 |
|
216 | delete graphy['content.nt.read'];
|
217 | return graphy['content.nt.read'] = require('@graphy-dev/content.nt.read');
|
218 | },
|
219 |
|
220 | get 'content.nt.write'() {
|
221 |
|
222 | delete graphy['content.nt.write'];
|
223 | return graphy['content.nt.write'] = require('@graphy-dev/content.nt.write');
|
224 | },
|
225 |
|
226 | get 'content.nq.read'() {
|
227 |
|
228 | delete graphy['content.nq.read'];
|
229 | return graphy['content.nq.read'] = require('@graphy-dev/content.nq.read');
|
230 | },
|
231 |
|
232 | get 'content.nq.write'() {
|
233 |
|
234 | delete graphy['content.nq.write'];
|
235 | return graphy['content.nq.write'] = require('@graphy-dev/content.nq.write');
|
236 | },
|
237 |
|
238 |
|
239 |
|
240 | }, require('@graphy-dev/core.data.factory'));
|
241 |
|
242 |
|
243 |
|
244 | if('undefined' !== typeof window) window.graphy = graphy;
|
245 |
|
246 |
|
247 | if(module === require.main) {
|
248 | const fs =require('fs');
|
249 | const path = require('path');
|
250 | const mk_yargs = require('yargs/yargs');
|
251 | const stream = require('@graphy-dev/core.iso.stream');
|
252 |
|
253 | class answer_source extends require('stream').Readable {
|
254 | constructor(w_datum) {
|
255 | super({
|
256 | objectMode: true,
|
257 | });
|
258 |
|
259 | this.datum = w_datum;
|
260 | }
|
261 |
|
262 |
|
263 | pipe(ds_dst) {
|
264 |
|
265 | if(!ds_dst._writableState.objectMode) {
|
266 |
|
267 | this._read = () => {
|
268 | this.push(JSON.stringify(this.datum)+'\n', 'utf8');
|
269 | this.push(null);
|
270 | };
|
271 | }
|
272 |
|
273 |
|
274 | return super.pipe(ds_dst);
|
275 | }
|
276 |
|
277 |
|
278 | _read() {
|
279 | this.push(this.datum);
|
280 | this.push(this.null);
|
281 | }
|
282 | }
|
283 |
|
284 | const exit = (s_exit) => {
|
285 | console.error(s_exit);
|
286 | process.exit(1);
|
287 | };
|
288 |
|
289 | const command = s_command => mk_yargs()
|
290 | .strict()
|
291 | .usage(`Usage: $0 ${s_command} [OPTIONS] [--pipe COMMAND]`);
|
292 |
|
293 | const reader = f_reader => (a_args, g_context) => new Promise((fk_resolve) => {
|
294 | let g_argv = command(g_context.command)
|
295 | .boolean('v')
|
296 | .alias('v', 'validate')
|
297 | .describe('v', 'validate all tokens within the RDF document')
|
298 | .string('b')
|
299 | .alias('b', ['base', 'base-uri'])
|
300 | .describe('b', 'set a base URI on the document')
|
301 | .help()
|
302 | .version(false)
|
303 | .parse(a_args);
|
304 |
|
305 | let gc_read = {
|
306 | validate: g_argv.validate || false,
|
307 | };
|
308 |
|
309 | if(g_argv['base-uri']) {
|
310 | gc_read.baseUri = g_argv['base-uri'];
|
311 | }
|
312 |
|
313 | fk_resolve(g_context.inputs.map((ds_input) => {
|
314 | let ds_reader = f_reader({
|
315 | ...gc_read,
|
316 | error(e_read) {
|
317 | g_context.failure(e_read);
|
318 | },
|
319 | });
|
320 |
|
321 | return ds_input.pipe(ds_reader);
|
322 | }));
|
323 | });
|
324 |
|
325 | const writer = f_writer => (a_args, g_context) => {
|
326 | let g_argv = command(g_context.command)
|
327 | .help()
|
328 | .parse(a_args);
|
329 |
|
330 | let gc_write = {};
|
331 |
|
332 | return g_context.inputs.map((ds_input) => {
|
333 | let ds_writer = f_writer({
|
334 | ...gc_write,
|
335 | error(e_write) {
|
336 | g_context.failure(e_write);
|
337 | },
|
338 | });
|
339 | return ds_input.pipe(ds_writer);
|
340 | });
|
341 | };
|
342 |
|
343 |
|
344 | let h_commands = {
|
345 |
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 | 'content.nt.read': reader(graphy.content.nt.read),
|
360 | 'content.nq.read': reader(graphy.content.nq.read),
|
361 | 'content.ttl.read': reader(graphy.content.ttl.read),
|
362 | 'content.trig.read': reader(graphy.content.trig.read),
|
363 |
|
364 | 'content.nt.write': writer(graphy.content.nt.write),
|
365 | 'content.nq.write': writer(graphy.content.nq.write),
|
366 | 'content.ttl.write': writer(graphy.content.ttl.write),
|
367 | 'content.trig.write': writer(graphy.content.trig.write),
|
368 |
|
369 | 'util.dataset.tree': async(a_args, g_context) => {
|
370 | const dataset_tree = graphy.util.dataset.tree;
|
371 |
|
372 | let s_group_multi_input = 'Transform 1 or more inputs to 1 output:';
|
373 | let s_group_dual_input = 'Transform exactly 2 inputs into 1 output:';
|
374 | let s_group_boolean = 'Test exactly 2 inputs to get `true` or `false`:';
|
375 |
|
376 | let h_operations = {
|
377 | z: {
|
378 | type: 'boolean',
|
379 | alias: ['canonicalize'],
|
380 | group: s_group_multi_input,
|
381 | describe: 'canonicalize 1 or more inputs',
|
382 | },
|
383 | u: {
|
384 | type: 'boolean',
|
385 | alias: ['union'],
|
386 | group: s_group_multi_input,
|
387 | describe: 'perform the union of 1 or more inputs',
|
388 | },
|
389 | i: {
|
390 | type: 'boolean',
|
391 | alias: ['intersect', 'intersection'],
|
392 | group: s_group_multi_input,
|
393 | describe: 'perform the intersection of 1 or more inputs',
|
394 | },
|
395 | d: {
|
396 | type: 'boolean',
|
397 | alias: ['diff', 'difference'],
|
398 | group: s_group_dual_input,
|
399 | describe: 'perform a difference between two inputs',
|
400 | },
|
401 | m: {
|
402 | type: 'boolean',
|
403 | alias: ['minus', 'subtract', 'subtraction'],
|
404 | group: s_group_dual_input,
|
405 | describe: 'perform a subtraction by removing input-B from input-A',
|
406 | },
|
407 | c: {
|
408 | type: 'boolean',
|
409 | alias: ['contains'],
|
410 | group: s_group_boolean,
|
411 | describe: 'test if input-A completely contains input-B, i.e., if B is a subset of A',
|
412 | },
|
413 | j: {
|
414 | type: 'boolean',
|
415 | alias: ['disjoint'],
|
416 | group: s_group_boolean,
|
417 | describe: 'test if input-A is disjoint with input-B',
|
418 | },
|
419 | e: {
|
420 | type: 'boolean',
|
421 | alias: ['equals'],
|
422 | group: s_group_boolean,
|
423 | describe: 'test if input-A is exactly equal to input-B (you can test for isomorphism by piping thru --canonicalize first)',
|
424 | },
|
425 | };
|
426 |
|
427 | let a_operation_keys = Object.keys(h_operations);
|
428 | for(let s_operation of a_operation_keys) {
|
429 | h_operations[s_operation].conflicts = a_operation_keys.filter(s => s_operation !== s);
|
430 | }
|
431 |
|
432 | let g_argv = command(g_context.command)
|
433 | .options(h_operations)
|
434 | .help()
|
435 | .version(false)
|
436 | .parse(a_args);
|
437 |
|
438 |
|
439 | let a_inputs = g_context.inputs;
|
440 | let n_inputs = a_inputs.length;
|
441 |
|
442 |
|
443 | if(g_argv.union || g_argv.intersection) {
|
444 | let s_operation = g_argv.union
|
445 | ? 'union'
|
446 | : 'intersection';
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 | let a_trees = a_inputs.map(() => dataset_tree());
|
453 |
|
454 |
|
455 | let k_tree_out = a_trees[0];
|
456 |
|
457 |
|
458 | for(let i_input=0; i_input<n_inputs; i_input++) {
|
459 | let k_tree_b = a_trees[i_input];
|
460 |
|
461 |
|
462 | a_inputs[i_input].pipe(k_tree_b);
|
463 |
|
464 |
|
465 | await k_tree_b.until('finish');
|
466 |
|
467 |
|
468 | if(i_input) {
|
469 |
|
470 | k_tree_out = k_tree_out[s_operation](k_tree_b);
|
471 | }
|
472 | }
|
473 |
|
474 |
|
475 | return [k_tree_out];
|
476 | }
|
477 |
|
478 | else if(g_argv.difference || g_argv.subtraction) {
|
479 | let s_operation = g_argv.difference
|
480 | ? 'difference'
|
481 | : 'minus';
|
482 |
|
483 |
|
484 | if(2 !== n_inputs) {
|
485 | exit(`operation '${s_operation}' expects two inputs but found ${n_inputs}`);
|
486 | }
|
487 |
|
488 |
|
489 | return new Promise((fk_resolve) => {
|
490 | let operate = () => [k_tree_a[s_operation](k_tree_b)];
|
491 |
|
492 |
|
493 | let k_tree_a = dataset_tree();
|
494 | let b_finished_a = false;
|
495 | k_tree_a.on('finish', () => {
|
496 | b_finished_a = true;
|
497 | if(b_finished_b) fk_resolve(operate());
|
498 | });
|
499 |
|
500 |
|
501 | let k_tree_b = dataset_tree();
|
502 | let b_finished_b = false;
|
503 | k_tree_b.on('finish', () => {
|
504 | b_finished_b = true;
|
505 | if(b_finished_a) fk_resolve(operate());
|
506 | });
|
507 |
|
508 |
|
509 | let [ds_input_a, ds_input_b] = a_inputs;
|
510 |
|
511 |
|
512 | ds_input_a.pipe(k_tree_a);
|
513 | ds_input_b.pipe(k_tree_b);
|
514 | });
|
515 | }
|
516 |
|
517 | else if(g_argv.contains || g_argv.disjoint || g_argv.equals) {
|
518 | let s_operation = g_argv.contains
|
519 | ? 'contains'
|
520 | : (g_argv.disjoint
|
521 | ? 'disjoint'
|
522 | : 'equals');
|
523 |
|
524 |
|
525 | if(2 !== n_inputs) {
|
526 | exit(`boolean operation '${s_operation}' expects two inputs but found ${n_inputs}`);
|
527 | }
|
528 |
|
529 |
|
530 | return new Promise((fk_resolve) => {
|
531 | let operate = () => [new answer_source(k_tree_a[s_operation](k_tree_b))];
|
532 |
|
533 |
|
534 | let k_tree_a = dataset_tree();
|
535 | let b_finished_a = false;
|
536 | k_tree_a.on('finish', () => {
|
537 | b_finished_a = true;
|
538 | if(b_finished_b) fk_resolve(operate());
|
539 | });
|
540 |
|
541 |
|
542 | let k_tree_b = dataset_tree();
|
543 | let b_finished_b = false;
|
544 | k_tree_b.on('finish', () => {
|
545 | b_finished_b = true;
|
546 | if(b_finished_a) fk_resolve(operate());
|
547 | });
|
548 |
|
549 |
|
550 | let [ds_input_a, ds_input_b] = a_inputs;
|
551 |
|
552 |
|
553 | ds_input_a.pipe(k_tree_a);
|
554 | ds_input_b.pipe(k_tree_b);
|
555 | });
|
556 | }
|
557 |
|
558 | else {
|
559 | return g_context.inputs.map(ds_input => ds_input.pipe(dataset_tree({
|
560 | canonicalize: g_argv.canonicalize,
|
561 | })));
|
562 | }
|
563 | },
|
564 | };
|
565 |
|
566 | let a_argv = process.argv.slice(2);
|
567 | let n_args = a_argv.length;
|
568 |
|
569 |
|
570 | if(!a_argv.length) {
|
571 | exit('no arguments given');
|
572 | }
|
573 |
|
574 |
|
575 | let a_inputs = [];
|
576 |
|
577 |
|
578 | let a_pipeline = [];
|
579 | {
|
580 | let a_series = [];
|
581 |
|
582 | for(let i_argv=0; i_argv<n_args; i_argv++) {
|
583 | let s_arg = a_argv[i_argv];
|
584 |
|
585 |
|
586 | if(i_argv) {
|
587 |
|
588 | if('--pipe' === s_arg) {
|
589 | a_pipeline.push(a_series);
|
590 | if(i_argv === n_args) {
|
591 | exit(`was expecting pipe destination after --pipe: ${a_argv}`);
|
592 | }
|
593 | a_series = [];
|
594 | continue;
|
595 | }
|
596 |
|
597 | else if('--inputs' === s_arg) {
|
598 |
|
599 | a_inputs.push(...a_argv.slice(i_argv+1).map(p => fs.createReadStream(p)));
|
600 | break;
|
601 | }
|
602 | }
|
603 |
|
604 |
|
605 | if('-h' === s_arg || '--help' === s_arg) {
|
606 |
|
607 | console.log('\n'+gobble(`
|
608 | Usage: graphy COMMAND [--pipe COMMAND]*
|
609 |
|
610 | Commands:
|
611 | content.nt.read Read an N-Triples document
|
612 | content.nt.write Write to N-Triples format
|
613 | content.nq.read Read an N-Quads document
|
614 | content.nq.write Write to N-Quads format
|
615 | content.ttl.read Read a Turtle document
|
616 | content.ttl.write Write to Turtle format
|
617 | content.trig.read Read a TriG document
|
618 | content.trig.write Write to TriG format
|
619 | util.dataset.tree Perform some transformation on a datset
|
620 |
|
621 | Run 'graphy COMMAND --help' for more information on a command.
|
622 | `));
|
623 | process.exit(0);
|
624 | }
|
625 |
|
626 | else if('-v' === s_arg || '--version' === s_arg) {
|
627 |
|
628 | console.log(require(path.join(__dirname, './package.json')).version);
|
629 | process.exit(0);
|
630 | }
|
631 |
|
632 | a_series.push(s_arg);
|
633 | }
|
634 |
|
635 |
|
636 | if(a_series.length) {
|
637 | a_pipeline.push(a_series);
|
638 | }
|
639 | }
|
640 |
|
641 |
|
642 | if(!a_pipeline.length) {
|
643 | exit('no commands given');
|
644 | }
|
645 |
|
646 |
|
647 |
|
648 |
|
649 |
|
650 |
|
651 |
|
652 |
|
653 |
|
654 |
|
655 |
|
656 |
|
657 |
|
658 |
|
659 |
|
660 |
|
661 |
|
662 |
|
663 |
|
664 |
|
665 |
|
666 |
|
667 |
|
668 |
|
669 |
|
670 |
|
671 |
|
672 |
|
673 |
|
674 |
|
675 |
|
676 |
|
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 |
|
686 |
|
687 |
|
688 | (async() => {
|
689 |
|
690 | let f_failure = (e_command) => {
|
691 | exit(e_command.message);
|
692 | };
|
693 |
|
694 |
|
695 | let a_prev = a_inputs.length? a_inputs: [process.stdin];
|
696 |
|
697 |
|
698 | for(let a_series of a_pipeline) {
|
699 |
|
700 | let s_command = a_series[0];
|
701 |
|
702 |
|
703 | if(!(s_command in h_commands)) {
|
704 | exit(`no such command '${s_command}'`);
|
705 | }
|
706 |
|
707 | try {
|
708 |
|
709 | let a_curr = await h_commands[s_command](a_series.slice(1), {
|
710 | command: s_command,
|
711 | inputs: a_prev,
|
712 | failure: f_failure,
|
713 | });
|
714 |
|
715 |
|
716 | a_prev = a_curr;
|
717 | }
|
718 | catch(e_command) {
|
719 | exit(e_command.message);
|
720 | }
|
721 | }
|
722 |
|
723 |
|
724 | if(1 !== a_prev.length) {
|
725 | exit(`expected a single output stream but last command produces ${a_prev.length} streams`);
|
726 | }
|
727 |
|
728 |
|
729 | a_prev[0].pipe(process.stdout);
|
730 | })();
|
731 | }
|