Skip to content

Commit edc5607

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 456b2d6 commit edc5607

File tree

5 files changed

+344
-11
lines changed

5 files changed

+344
-11
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
- Support for expansion and compaction of values container `"@direction"`.
2929
- Support for RDF transformation of `@direction` when `rdfDirection` is
3030
'i18n-datatype'.
31+
- Event handler option "`handleEvent`" to allow custom handling of warnings and
32+
potentially other events in the future. Handles event replay for cached
33+
contexts.
3134

3235
### Removed
3336
- Experimental non-standard `protectedMode` option.

lib/context.js

+69-11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const {
2020
parse: parseUrl
2121
} = require('./url');
2222

23+
const {
24+
handleEvent: _handleEvent
25+
} = require('./events');
26+
2327
const {
2428
asArray: _asArray,
2529
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
context: localCtx,
@@ -111,7 +132,12 @@ api.process = async ({
111132
// get processed context from cache if available
112133
const processed = resolvedContext.getProcessed(activeCtx);
113134
if(processed) {
114-
rval = activeCtx = processed;
135+
// replay events with original non-capturing options
136+
for(const event of processed.events) {
137+
_handleEvent({event, options: originalOptions});
138+
}
139+
140+
rval = activeCtx = processed.context;
115141
continue;
116142
}
117143

@@ -330,7 +356,10 @@ api.process = async ({
330356
}
331357

332358
// cache processed result
333-
resolvedContext.setProcessed(activeCtx, rval);
359+
resolvedContext.setProcessed(activeCtx, {
360+
context: rval,
361+
events
362+
});
334363
}
335364

336365
return rval;
@@ -394,9 +423,18 @@ api.createTermDefinition = ({
394423
'jsonld.SyntaxError',
395424
{code: 'keyword redefinition', context: localCtx, term});
396425
} else if(term.match(KEYWORD_PATTERN)) {
397-
// FIXME: remove logging and use a handler
398-
console.warn('WARNING: terms beginning with "@" are reserved' +
399-
' for future use and ignored', {term});
426+
_handleEvent({
427+
event: {
428+
code: 'invalid reserved term',
429+
level: 'warning',
430+
message:
431+
'Terms beginning with "@" are reserved for future use and ignored.',
432+
details: {
433+
term
434+
}
435+
},
436+
options
437+
});
400438
return;
401439
} else if(term === '') {
402440
throw new JsonLdError(
@@ -488,9 +526,19 @@ api.createTermDefinition = ({
488526
}
489527

490528
if(reverse.match(KEYWORD_PATTERN)) {
491-
// FIXME: remove logging and use a handler
492-
console.warn('WARNING: values beginning with "@" are reserved' +
493-
' for future use and ignored', {reverse});
529+
_handleEvent({
530+
event: {
531+
code: 'invalid reserved value',
532+
level: 'warning',
533+
message:
534+
'Values beginning with "@" are reserved for future use and' +
535+
' ignored.',
536+
details: {
537+
id
538+
}
539+
},
540+
options
541+
});
494542
if(previousMapping) {
495543
activeCtx.mappings.set(term, previousMapping);
496544
} else {
@@ -513,9 +561,19 @@ api.createTermDefinition = ({
513561
// reserve a null term, which may be protected
514562
mapping['@id'] = null;
515563
} else if(!api.isKeyword(id) && id.match(KEYWORD_PATTERN)) {
516-
// FIXME: remove logging and use a handler
517-
console.warn('WARNING: values beginning with "@" are reserved' +
518-
' for future use and ignored', {id});
564+
_handleEvent({
565+
event: {
566+
code: 'invalid reserved value',
567+
level: 'warning',
568+
message:
569+
'Values beginning with "@" are reserved for future use and' +
570+
' ignored.',
571+
details: {
572+
id
573+
}
574+
},
575+
options
576+
});
519577
if(previousMapping) {
520578
activeCtx.mappings.set(term, previousMapping);
521579
} else {

lib/events.js

+92
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

+11
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE});
123123
* unmappable values (or to throw an error when they are detected);
124124
* if this function returns `undefined` then the default behavior
125125
* will be used.
126+
* [handleEvent] handler for events such as warnings.
126127
* [contextResolver] internal use only.
127128
*
128129
* @return a Promise that resolves to the compacted output.
@@ -271,6 +272,7 @@ jsonld.compact = async function(input, ctx, options) {
271272
* unmappable values (or to throw an error when they are detected);
272273
* if this function returns `undefined` then the default behavior
273274
* will be used.
275+
* [handleEvent] handler for events such as warnings.
274276
* [contextResolver] internal use only.
275277
*
276278
* @return a Promise that resolves to the expanded output.
@@ -368,6 +370,7 @@ jsonld.expand = async function(input, options) {
368370
* [base] the base IRI to use.
369371
* [expandContext] a context to expand with.
370372
* [documentLoader(url, options)] the document loader.
373+
* [handleEvent] handler for events such as warnings.
371374
* [contextResolver] internal use only.
372375
*
373376
* @return a Promise that resolves to the flattened output.
@@ -423,6 +426,7 @@ jsonld.flatten = async function(input, ctx, options) {
423426
* [requireAll] default @requireAll flag (default: true).
424427
* [omitDefault] default @omitDefault flag (default: false).
425428
* [documentLoader(url, options)] the document loader.
429+
* [handleEvent] handler for events such as warnings.
426430
* [contextResolver] internal use only.
427431
*
428432
* @return a Promise that resolves to the framed output.
@@ -505,6 +509,7 @@ jsonld.frame = async function(input, frame, options) {
505509
* [base] the base IRI to use.
506510
* [expandContext] a context to expand with.
507511
* [documentLoader(url, options)] the document loader.
512+
* [handleEvent] handler for events such as warnings.
508513
* [contextResolver] internal use only.
509514
*
510515
* @return a Promise that resolves to the linked output.
@@ -540,6 +545,7 @@ jsonld.link = async function(input, ctx, options) {
540545
* 'application/n-quads' for N-Quads.
541546
* [documentLoader(url, options)] the document loader.
542547
* [useNative] true to use a native canonize algorithm
548+
* [handleEvent] handler for events such as warnings.
543549
* [contextResolver] internal use only.
544550
*
545551
* @return a Promise that resolves to the normalized output.
@@ -594,6 +600,7 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
594600
* (default: false).
595601
* [useNativeTypes] true to convert XSD types into native types
596602
* (boolean, integer, double), false not to (default: false).
603+
* [handleEvent] handler for events such as warnings.
597604
*
598605
* @return a Promise that resolves to the JSON-LD document.
599606
*/
@@ -643,6 +650,7 @@ jsonld.fromRDF = async function(dataset, options) {
643650
* [produceGeneralizedRdf] true to output generalized RDF, false
644651
* to produce only standard RDF (default: false).
645652
* [documentLoader(url, options)] the document loader.
653+
* [handleEvent] handler for events such as warnings.
646654
* [contextResolver] internal use only.
647655
*
648656
* @return a Promise that resolves to the RDF dataset.
@@ -696,6 +704,7 @@ jsonld.toRDF = async function(input, options) {
696704
* [expandContext] a context to expand with.
697705
* [issuer] a jsonld.IdentifierIssuer to use to label blank nodes.
698706
* [documentLoader(url, options)] the document loader.
707+
* [handleEvent] handler for events such as warnings.
699708
* [contextResolver] internal use only.
700709
*
701710
* @return a Promise that resolves to the merged node map.
@@ -735,6 +744,7 @@ jsonld.createNodeMap = async function(input, options) {
735744
* new properties where a node is in the `object` position
736745
* (default: true).
737746
* [documentLoader(url, options)] the document loader.
747+
* [handleEvent] handler for events such as warnings.
738748
* [contextResolver] internal use only.
739749
*
740750
* @return a Promise that resolves to the merged output.
@@ -897,6 +907,7 @@ jsonld.get = async function(url, options) {
897907
* @param localCtx the local context to process.
898908
* @param [options] the options to use:
899909
* [documentLoader(url, options)] the document loader.
910+
* [handleEvent] handler for events such as warnings.
900911
* [contextResolver] internal use only.
901912
*
902913
* @return a Promise that resolves to the new active context.

0 commit comments

Comments
 (0)