Skip to content

Commit 32f481a

Browse files
committed
Add event handler.
- Current use is for custom handling of warnings. - Replay events when using cached contexts. - Flexible chainable handlers. Can use functions, arrays, object code/function map shortcut, or a combination of these. - Use handler for current context warnings.
1 parent 3cf7dfb commit 32f481a

File tree

5 files changed

+345
-12
lines changed

5 files changed

+345
-12
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
- Support test environment in EARL output.
66
- Support benchmark output in EARL output.
77
- Benchmark comparison tool.
8+
- Event handler option "`handleEvent`" to allow custom handling of warnings and
9+
potentially other events in the future. Handles event replay for cached
10+
contexts.
811

912
### Changed
1013
- Change EARL Assertor to Digital Bazaar, Inc.

lib/context.js

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const {
1919
prependBase
2020
} = require('./url');
2121

22+
const {
23+
handleEvent: _handleEvent
24+
} = require('./events');
25+
2226
const {
2327
asArray: _asArray,
2428
compareShortestLeast: _compareShortestLeast
@@ -61,6 +65,23 @@ api.process = async ({
6165
return activeCtx;
6266
}
6367

68+
// event handler for capturing events to replay when using a cached context
69+
const events = [];
70+
const handleEvent = [
71+
({event, next}) => {
72+
events.push(event);
73+
next();
74+
}
75+
];
76+
// chain to original handler
77+
if(options.handleEvent) {
78+
handleEvent.push(options.handleEvent);
79+
}
80+
// store original options to use when replaying events
81+
const originalOptions = options;
82+
// shallow clone options with custom event handler
83+
options = Object.assign({}, options, {handleEvent});
84+
6485
// resolve contexts
6586
const resolved = await options.contextResolver.resolve({
6687
activeCtx,
@@ -112,7 +133,12 @@ api.process = async ({
112133
// get processed context from cache if available
113134
const processed = resolvedContext.getProcessed(activeCtx);
114135
if(processed) {
115-
rval = activeCtx = processed;
136+
// replay events with original non-capturing options
137+
for(const event of processed.events) {
138+
_handleEvent({event, options: originalOptions});
139+
}
140+
141+
rval = activeCtx = processed.context;
116142
continue;
117143
}
118144

@@ -380,7 +406,10 @@ api.process = async ({
380406
}
381407

382408
// cache processed result
383-
resolvedContext.setProcessed(activeCtx, rval);
409+
resolvedContext.setProcessed(activeCtx, {
410+
context: rval,
411+
events
412+
});
384413
}
385414

386415
return rval;
@@ -445,9 +474,18 @@ api.createTermDefinition = ({
445474
'jsonld.SyntaxError',
446475
{code: 'keyword redefinition', context: localCtx, term});
447476
} else if(term.match(KEYWORD_PATTERN)) {
448-
// FIXME: remove logging and use a handler
449-
console.warn('WARNING: terms beginning with "@" are reserved' +
450-
' for future use and ignored', {term});
477+
_handleEvent({
478+
event: {
479+
code: 'invalid reserved term',
480+
level: 'warning',
481+
message:
482+
'Terms beginning with "@" are reserved for future use and ignored.',
483+
details: {
484+
term
485+
}
486+
},
487+
options
488+
});
451489
return;
452490
} else if(term === '') {
453491
throw new JsonLdError(
@@ -527,10 +565,20 @@ api.createTermDefinition = ({
527565
'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx});
528566
}
529567

530-
if(!api.isKeyword(reverse) && reverse.match(KEYWORD_PATTERN)) {
531-
// FIXME: remove logging and use a handler
532-
console.warn('WARNING: values beginning with "@" are reserved' +
533-
' for future use and ignored', {reverse});
568+
if(reverse.match(KEYWORD_PATTERN)) {
569+
_handleEvent({
570+
event: {
571+
code: 'invalid reserved value',
572+
level: 'warning',
573+
message:
574+
'Values beginning with "@" are reserved for future use and' +
575+
' ignored.',
576+
details: {
577+
reverse
578+
}
579+
},
580+
options
581+
});
534582
if(previousMapping) {
535583
activeCtx.mappings.set(term, previousMapping);
536584
} else {
@@ -564,9 +612,19 @@ api.createTermDefinition = ({
564612
// reserve a null term, which may be protected
565613
mapping['@id'] = null;
566614
} else if(!api.isKeyword(id) && id.match(KEYWORD_PATTERN)) {
567-
// FIXME: remove logging and use a handler
568-
console.warn('WARNING: values beginning with "@" are reserved' +
569-
' for future use and ignored', {id});
615+
_handleEvent({
616+
event: {
617+
code: 'invalid reserved value',
618+
level: 'warning',
619+
message:
620+
'Values beginning with "@" are reserved for future use and' +
621+
' ignored.',
622+
details: {
623+
id
624+
}
625+
},
626+
options
627+
});
570628
if(previousMapping) {
571629
activeCtx.mappings.set(term, previousMapping);
572630
} else {

lib/events.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved.
3+
*/
4+
'use strict';
5+
6+
const JsonLdError = require('./JsonLdError');
7+
8+
const {
9+
isArray: _isArray
10+
} = require('./types');
11+
12+
const {
13+
asArray: _asArray
14+
} = require('./util');
15+
16+
const api = {};
17+
module.exports = api;
18+
19+
/**
20+
* Handle an event.
21+
*
22+
* Top level APIs have a common 'handleEvent' option. This option can be a
23+
* function, array of functions, object mapping event.code to functions (with a
24+
* default to call next()), or any combination of such handlers. Handlers will
25+
* be called with an object with an 'event' entry and a 'next' function. Custom
26+
* handlers should process the event as appropriate. The 'next()' function
27+
* should be called to let the next handler process the event.
28+
*
29+
* The final default handler will use 'console.warn' for events of level
30+
* 'warning'.
31+
*
32+
* @param {object} event - event structure:
33+
* {string} code - event code
34+
* {string} level - severity level, one of: ['warning']
35+
* {string} message - human readable message
36+
* {object} details - event specific details
37+
* @param {object} options - original API options
38+
*/
39+
api.handleEvent = ({
40+
event,
41+
options
42+
}) => {
43+
const handlers = [].concat(
44+
options.handleEvent ? _asArray(options.handleEvent) : [],
45+
_defaultHandler
46+
);
47+
_handle({event, handlers});
48+
};
49+
50+
function _handle({event, handlers}) {
51+
let doNext = true;
52+
for(let i = 0; doNext && i < handlers.length; ++i) {
53+
doNext = false;
54+
const handler = handlers[i];
55+
if(_isArray(handler)) {
56+
doNext = _handle({event, handlers: handler});
57+
} else if(typeof handler === 'function') {
58+
handler({event, next: () => {
59+
doNext = true;
60+
}});
61+
} else if(typeof handler === 'object') {
62+
if(event.code in handler) {
63+
handler[event.code]({event, next: () => {
64+
doNext = true;
65+
}});
66+
} else {
67+
doNext = true;
68+
}
69+
} else {
70+
throw new JsonLdError(
71+
'Invalid event handler.',
72+
'jsonld.InvalidEventHandler',
73+
{event});
74+
}
75+
}
76+
return doNext;
77+
}
78+
79+
function _defaultHandler({event}) {
80+
if(event.level === 'warning') {
81+
console.warn(`WARNING: ${event.message}`, {
82+
code: event.code,
83+
details: event.details
84+
});
85+
return;
86+
}
87+
// fallback to ensure events are handled somehow
88+
throw new JsonLdError(
89+
'No handler for event.',
90+
'jsonld.UnhandledEvent',
91+
{event});
92+
}

lib/jsonld.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE});
119119
* unmappable values (or to throw an error when they are detected);
120120
* if this function returns `undefined` then the default behavior
121121
* will be used.
122+
* [handleEvent] handler for events such as warnings.
122123
* [contextResolver] internal use only.
123124
*
124125
* @return a Promise that resolves to the compacted output.
@@ -257,6 +258,7 @@ jsonld.compact = async function(input, ctx, options) {
257258
* unmappable values (or to throw an error when they are detected);
258259
* if this function returns `undefined` then the default behavior
259260
* will be used.
261+
* [handleEvent] handler for events such as warnings.
260262
* [contextResolver] internal use only.
261263
*
262264
* @return a Promise that resolves to the expanded output.
@@ -354,6 +356,7 @@ jsonld.expand = async function(input, options) {
354356
* [base] the base IRI to use.
355357
* [expandContext] a context to expand with.
356358
* [documentLoader(url, options)] the document loader.
359+
* [handleEvent] handler for events such as warnings.
357360
* [contextResolver] internal use only.
358361
*
359362
* @return a Promise that resolves to the flattened output.
@@ -409,6 +412,7 @@ jsonld.flatten = async function(input, ctx, options) {
409412
* [requireAll] default @requireAll flag (default: true).
410413
* [omitDefault] default @omitDefault flag (default: false).
411414
* [documentLoader(url, options)] the document loader.
415+
* [handleEvent] handler for events such as warnings.
412416
* [contextResolver] internal use only.
413417
*
414418
* @return a Promise that resolves to the framed output.
@@ -507,6 +511,7 @@ jsonld.frame = async function(input, frame, options) {
507511
* [base] the base IRI to use.
508512
* [expandContext] a context to expand with.
509513
* [documentLoader(url, options)] the document loader.
514+
* [handleEvent] handler for events such as warnings.
510515
* [contextResolver] internal use only.
511516
*
512517
* @return a Promise that resolves to the linked output.
@@ -542,6 +547,7 @@ jsonld.link = async function(input, ctx, options) {
542547
* 'application/n-quads' for N-Quads.
543548
* [documentLoader(url, options)] the document loader.
544549
* [useNative] true to use a native canonize algorithm
550+
* [handleEvent] handler for events such as warnings.
545551
* [contextResolver] internal use only.
546552
*
547553
* @return a Promise that resolves to the normalized output.
@@ -596,6 +602,7 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
596602
* (default: false).
597603
* [useNativeTypes] true to convert XSD types into native types
598604
* (boolean, integer, double), false not to (default: false).
605+
* [handleEvent] handler for events such as warnings.
599606
*
600607
* @return a Promise that resolves to the JSON-LD document.
601608
*/
@@ -645,6 +652,7 @@ jsonld.fromRDF = async function(dataset, options) {
645652
* [produceGeneralizedRdf] true to output generalized RDF, false
646653
* to produce only standard RDF (default: false).
647654
* [documentLoader(url, options)] the document loader.
655+
* [handleEvent] handler for events such as warnings.
648656
* [contextResolver] internal use only.
649657
*
650658
* @return a Promise that resolves to the RDF dataset.
@@ -698,6 +706,7 @@ jsonld.toRDF = async function(input, options) {
698706
* [expandContext] a context to expand with.
699707
* [issuer] a jsonld.IdentifierIssuer to use to label blank nodes.
700708
* [documentLoader(url, options)] the document loader.
709+
* [handleEvent] handler for events such as warnings.
701710
* [contextResolver] internal use only.
702711
*
703712
* @return a Promise that resolves to the merged node map.
@@ -737,6 +746,7 @@ jsonld.createNodeMap = async function(input, options) {
737746
* new properties where a node is in the `object` position
738747
* (default: true).
739748
* [documentLoader(url, options)] the document loader.
749+
* [handleEvent] handler for events such as warnings.
740750
* [contextResolver] internal use only.
741751
*
742752
* @return a Promise that resolves to the merged output.
@@ -899,6 +909,7 @@ jsonld.get = async function(url, options) {
899909
* @param localCtx the local context to process.
900910
* @param [options] the options to use:
901911
* [documentLoader(url, options)] the document loader.
912+
* [handleEvent] handler for events such as warnings.
902913
* [contextResolver] internal use only.
903914
*
904915
* @return a Promise that resolves to the new active context.

0 commit comments

Comments
 (0)