From 3cf7dfbe7ce945515bc87064984ce8a0679369ee Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 21:17:44 -0500 Subject: [PATCH 1/5] Remove experimental protectedMode option. --- CHANGELOG.md | 3 +++ lib/context.js | 62 +++++--------------------------------------------- 2 files changed, 9 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c7b4c5..03e29c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ ### Changed - Change EARL Assertor to Digital Bazaar, Inc. +### Removed +- Experimental non-standard `protectedMode` option. + ## 5.2.0 - 2021-04-07 ### Changed diff --git a/lib/context.js b/lib/context.js index 5f0de789..39451262 100644 --- a/lib/context.js +++ b/lib/context.js @@ -98,46 +98,12 @@ api.process = async ({ if(ctx === null) { // We can't nullify if there are protected terms and we're // not allowing overrides (e.g. processing a property term scoped context) - if(!overrideProtected && - Object.keys(activeCtx.protected).length !== 0) { - const protectedMode = (options && options.protectedMode) || 'error'; - if(protectedMode === 'error') { - throw new JsonLdError( - 'Tried to nullify a context with protected terms outside of ' + - 'a term definition.', - 'jsonld.SyntaxError', - {code: 'invalid context nullification'}); - } else if(protectedMode === 'warn') { - // FIXME: remove logging and use a handler - console.warn('WARNING: invalid context nullification'); - - // get processed context from cache if available - const processed = resolvedContext.getProcessed(activeCtx); - if(processed) { - rval = activeCtx = processed; - continue; - } - - const oldActiveCtx = activeCtx; - // copy all protected term definitions to fresh initial context - rval = activeCtx = api.getInitialContext(options).clone(); - for(const [term, _protected] of - Object.entries(oldActiveCtx.protected)) { - if(_protected) { - activeCtx.mappings[term] = - util.clone(oldActiveCtx.mappings[term]); - } - } - activeCtx.protected = util.clone(oldActiveCtx.protected); - - // cache processed result - resolvedContext.setProcessed(oldActiveCtx, rval); - continue; - } + if(!overrideProtected && Object.keys(activeCtx.protected).length !== 0) { throw new JsonLdError( - 'Invalid protectedMode.', + 'Tried to nullify a context with protected terms outside of ' + + 'a term definition.', 'jsonld.SyntaxError', - {code: 'invalid protected mode', context: localCtx, protectedMode}); + {code: 'invalid context nullification'}); } rval = activeCtx = api.getInitialContext(options).clone(); continue; @@ -429,9 +395,6 @@ api.process = async ({ * @param defined a map of defining/defined keys to detect cycles and prevent * double definitions. * @param {Object} [options] - creation options. - * @param {string} [options.protectedMode="error"] - "error" to throw error - * on `@protected` constraint violation, "warn" to allow violations and - * signal a warning. * @param overrideProtected `false` allows protected terms to be modified. */ api.createTermDefinition = ({ @@ -918,23 +881,10 @@ api.createTermDefinition = ({ activeCtx.protected[term] = true; mapping.protected = true; if(!_deepCompare(previousMapping, mapping)) { - const protectedMode = (options && options.protectedMode) || 'error'; - if(protectedMode === 'error') { - throw new JsonLdError( - `Invalid JSON-LD syntax; tried to redefine "${term}" which is a ` + - 'protected term.', - 'jsonld.SyntaxError', - {code: 'protected term redefinition', context: localCtx, term}); - } else if(protectedMode === 'warn') { - // FIXME: remove logging and use a handler - console.warn('WARNING: protected term redefinition', {term}); - return; - } throw new JsonLdError( - 'Invalid protectedMode.', + 'Invalid JSON-LD syntax; tried to redefine a protected term.', 'jsonld.SyntaxError', - {code: 'invalid protected mode', context: localCtx, term, - protectedMode}); + {code: 'protected term redefinition', context: localCtx, term}); } } }; From 32f481aa1b68e16eadb621048fda1cb7a288e480 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 21:25:17 -0500 Subject: [PATCH 2/5] 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. --- CHANGELOG.md | 3 + lib/context.js | 82 ++++++++++++++++++++---- lib/events.js | 92 +++++++++++++++++++++++++++ lib/jsonld.js | 11 ++++ tests/misc.js | 169 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 345 insertions(+), 12 deletions(-) create mode 100644 lib/events.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e29c4e..0c3772f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ - Support test environment in EARL output. - Support benchmark output in EARL output. - Benchmark comparison tool. +- Event handler option "`handleEvent`" to allow custom handling of warnings and + potentially other events in the future. Handles event replay for cached + contexts. ### Changed - Change EARL Assertor to Digital Bazaar, Inc. diff --git a/lib/context.js b/lib/context.js index 39451262..6fb3a6cc 100644 --- a/lib/context.js +++ b/lib/context.js @@ -19,6 +19,10 @@ const { prependBase } = require('./url'); +const { + handleEvent: _handleEvent +} = require('./events'); + const { asArray: _asArray, compareShortestLeast: _compareShortestLeast @@ -61,6 +65,23 @@ api.process = async ({ return activeCtx; } + // event handler for capturing events to replay when using a cached context + const events = []; + const handleEvent = [ + ({event, next}) => { + events.push(event); + next(); + } + ]; + // chain to original handler + if(options.handleEvent) { + handleEvent.push(options.handleEvent); + } + // store original options to use when replaying events + const originalOptions = options; + // shallow clone options with custom event handler + options = Object.assign({}, options, {handleEvent}); + // resolve contexts const resolved = await options.contextResolver.resolve({ activeCtx, @@ -112,7 +133,12 @@ api.process = async ({ // get processed context from cache if available const processed = resolvedContext.getProcessed(activeCtx); if(processed) { - rval = activeCtx = processed; + // replay events with original non-capturing options + for(const event of processed.events) { + _handleEvent({event, options: originalOptions}); + } + + rval = activeCtx = processed.context; continue; } @@ -380,7 +406,10 @@ api.process = async ({ } // cache processed result - resolvedContext.setProcessed(activeCtx, rval); + resolvedContext.setProcessed(activeCtx, { + context: rval, + events + }); } return rval; @@ -445,9 +474,18 @@ api.createTermDefinition = ({ 'jsonld.SyntaxError', {code: 'keyword redefinition', context: localCtx, term}); } else if(term.match(KEYWORD_PATTERN)) { - // FIXME: remove logging and use a handler - console.warn('WARNING: terms beginning with "@" are reserved' + - ' for future use and ignored', {term}); + _handleEvent({ + event: { + code: 'invalid reserved term', + level: 'warning', + message: + 'Terms beginning with "@" are reserved for future use and ignored.', + details: { + term + } + }, + options + }); return; } else if(term === '') { throw new JsonLdError( @@ -527,10 +565,20 @@ api.createTermDefinition = ({ 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } - if(!api.isKeyword(reverse) && reverse.match(KEYWORD_PATTERN)) { - // FIXME: remove logging and use a handler - console.warn('WARNING: values beginning with "@" are reserved' + - ' for future use and ignored', {reverse}); + if(reverse.match(KEYWORD_PATTERN)) { + _handleEvent({ + event: { + code: 'invalid reserved value', + level: 'warning', + message: + 'Values beginning with "@" are reserved for future use and' + + ' ignored.', + details: { + reverse + } + }, + options + }); if(previousMapping) { activeCtx.mappings.set(term, previousMapping); } else { @@ -564,9 +612,19 @@ api.createTermDefinition = ({ // reserve a null term, which may be protected mapping['@id'] = null; } else if(!api.isKeyword(id) && id.match(KEYWORD_PATTERN)) { - // FIXME: remove logging and use a handler - console.warn('WARNING: values beginning with "@" are reserved' + - ' for future use and ignored', {id}); + _handleEvent({ + event: { + code: 'invalid reserved value', + level: 'warning', + message: + 'Values beginning with "@" are reserved for future use and' + + ' ignored.', + details: { + id + } + }, + options + }); if(previousMapping) { activeCtx.mappings.set(term, previousMapping); } else { diff --git a/lib/events.js b/lib/events.js new file mode 100644 index 00000000..77a72833 --- /dev/null +++ b/lib/events.js @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved. + */ +'use strict'; + +const JsonLdError = require('./JsonLdError'); + +const { + isArray: _isArray +} = require('./types'); + +const { + asArray: _asArray +} = require('./util'); + +const api = {}; +module.exports = api; + +/** + * Handle an event. + * + * Top level APIs have a common 'handleEvent' option. This option can be a + * function, array of functions, object mapping event.code to functions (with a + * default to call next()), or any combination of such handlers. Handlers will + * be called with an object with an 'event' entry and a 'next' function. Custom + * handlers should process the event as appropriate. The 'next()' function + * should be called to let the next handler process the event. + * + * The final default handler will use 'console.warn' for events of level + * 'warning'. + * + * @param {object} event - event structure: + * {string} code - event code + * {string} level - severity level, one of: ['warning'] + * {string} message - human readable message + * {object} details - event specific details + * @param {object} options - original API options + */ +api.handleEvent = ({ + event, + options +}) => { + const handlers = [].concat( + options.handleEvent ? _asArray(options.handleEvent) : [], + _defaultHandler + ); + _handle({event, handlers}); +}; + +function _handle({event, handlers}) { + let doNext = true; + for(let i = 0; doNext && i < handlers.length; ++i) { + doNext = false; + const handler = handlers[i]; + if(_isArray(handler)) { + doNext = _handle({event, handlers: handler}); + } else if(typeof handler === 'function') { + handler({event, next: () => { + doNext = true; + }}); + } else if(typeof handler === 'object') { + if(event.code in handler) { + handler[event.code]({event, next: () => { + doNext = true; + }}); + } else { + doNext = true; + } + } else { + throw new JsonLdError( + 'Invalid event handler.', + 'jsonld.InvalidEventHandler', + {event}); + } + } + return doNext; +} + +function _defaultHandler({event}) { + if(event.level === 'warning') { + console.warn(`WARNING: ${event.message}`, { + code: event.code, + details: event.details + }); + return; + } + // fallback to ensure events are handled somehow + throw new JsonLdError( + 'No handler for event.', + 'jsonld.UnhandledEvent', + {event}); +} diff --git a/lib/jsonld.js b/lib/jsonld.js index ffd974cb..8fcead78 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -119,6 +119,7 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the compacted output. @@ -257,6 +258,7 @@ jsonld.compact = async function(input, ctx, options) { * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the expanded output. @@ -354,6 +356,7 @@ jsonld.expand = async function(input, options) { * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the flattened output. @@ -409,6 +412,7 @@ jsonld.flatten = async function(input, ctx, options) { * [requireAll] default @requireAll flag (default: true). * [omitDefault] default @omitDefault flag (default: false). * [documentLoader(url, options)] the document loader. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the framed output. @@ -507,6 +511,7 @@ jsonld.frame = async function(input, frame, options) { * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the linked output. @@ -542,6 +547,7 @@ jsonld.link = async function(input, ctx, options) { * 'application/n-quads' for N-Quads. * [documentLoader(url, options)] the document loader. * [useNative] true to use a native canonize algorithm + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the normalized output. @@ -596,6 +602,7 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { * (default: false). * [useNativeTypes] true to convert XSD types into native types * (boolean, integer, double), false not to (default: false). + * [handleEvent] handler for events such as warnings. * * @return a Promise that resolves to the JSON-LD document. */ @@ -645,6 +652,7 @@ jsonld.fromRDF = async function(dataset, options) { * [produceGeneralizedRdf] true to output generalized RDF, false * to produce only standard RDF (default: false). * [documentLoader(url, options)] the document loader. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the RDF dataset. @@ -698,6 +706,7 @@ jsonld.toRDF = async function(input, options) { * [expandContext] a context to expand with. * [issuer] a jsonld.IdentifierIssuer to use to label blank nodes. * [documentLoader(url, options)] the document loader. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged node map. @@ -737,6 +746,7 @@ jsonld.createNodeMap = async function(input, options) { * new properties where a node is in the `object` position * (default: true). * [documentLoader(url, options)] the document loader. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged output. @@ -899,6 +909,7 @@ jsonld.get = async function(url, options) { * @param localCtx the local context to process. * @param [options] the options to use: * [documentLoader(url, options)] the document loader. + * [handleEvent] handler for events such as warnings. * [contextResolver] internal use only. * * @return a Promise that resolves to the new active context. diff --git a/tests/misc.js b/tests/misc.js index d9dae4fc..723e2d33 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1006,3 +1006,172 @@ describe('expansionMap', () => { }); }); }); + +describe('events', () => { + it('handle warning event with function', async () => { + const d = +{ + "@context": { + "@RESERVED": "ex:test-function-handler" + }, + "@RESERVED": "test" +} +; + const ex = []; + + let handled = false; + const e = await jsonld.expand(d, { + handleEvent: ({event, next}) => { + if(event.code === 'invalid reserved term') { + handled = true; + } else { + next(); + } + } + }); + assert.deepStrictEqual(e, ex); + assert.equal(handled, true); + }); + it('cached context event replay', async () => { + const d = +{ + "@context": { + "@RESERVED": "ex:test" + }, + "@RESERVED": "test" +} +; + const ex = []; + + let handled0 = false; + let handled1 = false; + const e0 = await jsonld.expand(d, { + handleEvent: { + 'invalid reserved term': () => { + handled0 = true; + } + } + }); + // FIXME: ensure cache is being used + const e1 = await jsonld.expand(d, { + handleEvent: { + 'invalid reserved term': () => { + handled1 = true; + } + } + }); + assert.deepStrictEqual(e0, ex); + assert.deepStrictEqual(e1, ex); + assert.equal(handled0, true, 'handled 0'); + assert.equal(handled1, true, 'handled 1'); + }); + it('handle warning event with array of functions', async () => { + const d = +{ + "@context": { + "@RESERVED": "ex:test-function-array-handler" + }, + "@RESERVED": "test" +} +; + const ex = []; + + let ranHandler0 = false; + let ranHandler1 = false; + let handled = false; + const e = await jsonld.expand(d, { + handleEvent: [ + ({next}) => { + ranHandler0 = true; + // skip to next handler + next(); + }, + ({event, next}) => { + ranHandler1 = true; + if(event.code === 'invalid reserved term') { + handled = true; + return; + } + next(); + } + ] + }); + assert.deepStrictEqual(e, ex); + assert.equal(ranHandler0, true, 'ran handler 0'); + assert.equal(ranHandler1, true, 'ran handler 1'); + assert.equal(handled, true, 'handled'); + }); + it('handle warning event with code:function object', async () => { + const d = +{ + "@context": { + "@RESERVED": "ex:test-object-handler" + }, + "@RESERVED": "test" +} +; + const ex = []; + + let handled = false; + const e = await jsonld.expand(d, { + handleEvent: { + 'invalid reserved term': ({event}) => { + assert.equal(event.details.term, '@RESERVED'); + handled = true; + } + } + }); + assert.deepStrictEqual(e, ex); + assert.equal(handled, true, 'handled'); + }); + it('handle warning event with complex handler', async () => { + const d = +{ + "@context": { + "@RESERVED": "ex:test-complex-handler" + }, + "@RESERVED": "test" +} +; + const ex = []; + + let ranHandler0 = false; + let ranHandler1 = false; + let ranHandler2 = false; + let ranHandler3 = false; + let handled = false; + const e = await jsonld.expand(d, { + handleEvent: [ + ({next}) => { + ranHandler0 = true; + next(); + }, + [ + ({next}) => { + ranHandler1 = true; + next(); + }, + { + 'bogus code': () => {} + } + ], + ({next}) => { + ranHandler2 = true; + next(); + }, + { + 'invalid reserved term': () => { + ranHandler3 = true; + handled = true; + } + } + ] + }); + assert.deepStrictEqual(e, ex); + assert.equal(ranHandler0, true, 'ran handler 0'); + assert.equal(ranHandler1, true, 'ran handler 1'); + assert.equal(ranHandler2, true, 'ran handler 2'); + assert.equal(ranHandler3, true, 'ran handler 3'); + assert.equal(handled, true, 'handled'); + }); +}); From e84e6917ca76b94c4294dd2c10ddf8e563ad3995 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 23:18:29 -0500 Subject: [PATCH 3/5] Use events for language value warnings. --- lib/expand.js | 20 ++++++++-- lib/fromRdf.js | 33 ++++++++++++---- lib/jsonld.js | 2 + tests/misc.js | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 11 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index 737def7b..0f64aec3 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -39,6 +39,10 @@ const { validateTypeValue: _validateTypeValue } = require('./util'); +const { + handleEvent: _handleEvent +} = require('./events'); + const api = {}; module.exports = api; const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; @@ -609,9 +613,19 @@ async function _expandObject({ value = _asArray(value).map(v => _isString(v) ? v.toLowerCase() : v); // ensure language tag matches BCP47 - for(const lang of value) { - if(_isString(lang) && !lang.match(REGEX_BCP47)) { - console.warn(`@language must be valid BCP47: ${lang}`); + for(const language of value) { + if(_isString(language) && !language.match(REGEX_BCP47)) { + _handleEvent({ + event: { + code: 'invalid @language value', + level: 'warning', + message: '@language value must be valid BCP47.', + details: { + language + } + }, + options + }); } } diff --git a/lib/fromRdf.js b/lib/fromRdf.js index fb3567c8..509bde0b 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -8,6 +8,10 @@ const graphTypes = require('./graphTypes'); const types = require('./types'); const util = require('./util'); +const { + handleEvent: _handleEvent +} = require('./events'); + // constants const { // RDF, @@ -44,15 +48,16 @@ module.exports = api; */ api.fromRDF = async ( dataset, - { - useRdfType = false, - useNativeTypes = false, - rdfDirection = null - } + options ) => { const defaultGraph = {}; const graphMap = {'@default': defaultGraph}; const referencedOnce = {}; + const { + useRdfType = false, + useNativeTypes = false, + rdfDirection = null + } = options; for(const quad of dataset) { // TODO: change 'name' to 'graph' @@ -87,7 +92,7 @@ api.fromRDF = async ( continue; } - const value = _RDFToObject(o, useNativeTypes, rdfDirection); + const value = _RDFToObject(o, useNativeTypes, rdfDirection, options); util.addValue(node, p, value, {propertyIsArray: true}); // object may be an RDF list/partial list node but we can't know easily @@ -275,10 +280,12 @@ api.fromRDF = async ( * * @param o the RDF triple object to convert. * @param useNativeTypes true to output native types, false not to. + * @param rdfDirection text direction mode [null, i18n-datatype] + * @param options top level API options * * @return the JSON-LD object. */ -function _RDFToObject(o, useNativeTypes, rdfDirection) { +function _RDFToObject(o, useNativeTypes, rdfDirection, options) { // convert NamedNode/BlankNode object to JSON-LD if(o.termType.endsWith('Node')) { return {'@id': o.value}; @@ -334,7 +341,17 @@ function _RDFToObject(o, useNativeTypes, rdfDirection) { if(language.length > 0) { rval['@language'] = language; if(!language.match(REGEX_BCP47)) { - console.warn(`@language must be valid BCP47: ${language}`); + _handleEvent({ + event: { + code: 'invalid @language value', + level: 'warning', + message: '@language value must be valid BCP47.', + details: { + language + } + }, + options + }); } } rval['@direction'] = direction; diff --git a/lib/jsonld.js b/lib/jsonld.js index 8fcead78..e6d504a7 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -602,6 +602,8 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { * (default: false). * [useNativeTypes] true to convert XSD types into native types * (boolean, integer, double), false not to (default: false). + * [rdfDirection] 'i18n-datatype' to support RDF transformation of + * @direction (default: null). * [handleEvent] handler for events such as warnings. * * @return a Promise that resolves to the JSON-LD document. diff --git a/tests/misc.js b/tests/misc.js index 723e2d33..beb9b45d 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1174,4 +1174,109 @@ describe('events', () => { assert.equal(ranHandler3, true, 'ran handler 3'); assert.equal(handled, true, 'handled'); }); + it('handle known warning events', async () => { + const d = +{ + "@context": { + "id-at": {"@id": "@test"}, + "@RESERVED": "ex:test" + }, + "@RESERVED": "test", + "ex:language": { + "@value": "test", + "@language": "!" + } +} +; + const ex = +[ + { + "ex:language": [ + { + "@value": "test", + "@language": "!" + } + ] + } +] +; + + let handledReservedTerm = false; + let handledReservedValue = false; + let handledLanguage = false; + const e = await jsonld.expand(d, { + handleEvent: { + 'invalid reserved term': () => { + handledReservedTerm = true; + }, + 'invalid reserved value': () => { + handledReservedValue = true; + }, + 'invalid @language value': () => { + handledLanguage = true; + } + } + }); + assert.deepStrictEqual(e, ex); + assert.equal(handledReservedTerm, true); + assert.equal(handledReservedValue, true); + assert.equal(handledLanguage, true); + + // dataset with invalid language tag + // Equivalent N-Quads: + // "..."^^ .' + // Using JSON dataset to bypass N-Quads parser checks. + const d2 = +[ + { + "subject": { + "termType": "NamedNode", + "value": "ex:s" + }, + "predicate": { + "termType": "NamedNode", + "value": "ex:p" + }, + "object": { + "termType": "Literal", + "value": "invalid @language value", + "datatype": { + "termType": "NamedNode", + "value": "https://www.w3.org/ns/i18n#!_rtl" + } + }, + "graph": { + "termType": "DefaultGraph", + "value": "" + } + } +] +; + const ex2 = +[ + { + "@id": "ex:s", + "ex:p": [ + { + "@value": "invalid @language value", + "@language": "!", + "@direction": "rtl" + } + ] + } +] +; + + let handledLanguage2 = false; + const e2 = await jsonld.fromRDF(d2, { + rdfDirection: 'i18n-datatype', + handleEvent: { + 'invalid @language value': () => { + handledLanguage2 = true; + } + } + }); + assert.deepStrictEqual(e2, ex2); + assert.equal(handledLanguage2, true); + }); }); From 04788014413a22390adda92c9af1f6ce005f1005 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 24 Feb 2020 13:15:31 -0500 Subject: [PATCH 4/5] Change handleEvent option to eventHandler. --- CHANGELOG.md | 2 +- lib/context.js | 8 ++++---- lib/events.js | 6 +++--- lib/jsonld.js | 22 +++++++++++----------- tests/misc.js | 16 ++++++++-------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3772f2..46068ffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Support test environment in EARL output. - Support benchmark output in EARL output. - Benchmark comparison tool. -- Event handler option "`handleEvent`" to allow custom handling of warnings and +- Event handler option `"eventHandler"` to allow custom handling of warnings and potentially other events in the future. Handles event replay for cached contexts. diff --git a/lib/context.js b/lib/context.js index 6fb3a6cc..3013320d 100644 --- a/lib/context.js +++ b/lib/context.js @@ -67,20 +67,20 @@ api.process = async ({ // event handler for capturing events to replay when using a cached context const events = []; - const handleEvent = [ + const eventHandler = [ ({event, next}) => { events.push(event); next(); } ]; // chain to original handler - if(options.handleEvent) { - handleEvent.push(options.handleEvent); + if(options.eventHandler) { + eventHandler.push(options.eventHandler); } // store original options to use when replaying events const originalOptions = options; // shallow clone options with custom event handler - options = Object.assign({}, options, {handleEvent}); + options = Object.assign({}, options, {eventHandler}); // resolve contexts const resolved = await options.contextResolver.resolve({ diff --git a/lib/events.js b/lib/events.js index 77a72833..0061a9cd 100644 --- a/lib/events.js +++ b/lib/events.js @@ -19,7 +19,7 @@ module.exports = api; /** * Handle an event. * - * Top level APIs have a common 'handleEvent' option. This option can be a + * Top level APIs have a common 'eventHandler' option. This option can be a * function, array of functions, object mapping event.code to functions (with a * default to call next()), or any combination of such handlers. Handlers will * be called with an object with an 'event' entry and a 'next' function. Custom @@ -34,14 +34,14 @@ module.exports = api; * {string} level - severity level, one of: ['warning'] * {string} message - human readable message * {object} details - event specific details - * @param {object} options - original API options + * @param {object} options - processing options */ api.handleEvent = ({ event, options }) => { const handlers = [].concat( - options.handleEvent ? _asArray(options.handleEvent) : [], + options.eventHandler ? _asArray(options.eventHandler) : [], _defaultHandler ); _handle({event, handlers}); diff --git a/lib/jsonld.js b/lib/jsonld.js index e6d504a7..f45fc68c 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -119,7 +119,7 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the compacted output. @@ -258,7 +258,7 @@ jsonld.compact = async function(input, ctx, options) { * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the expanded output. @@ -356,7 +356,7 @@ jsonld.expand = async function(input, options) { * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the flattened output. @@ -412,7 +412,7 @@ jsonld.flatten = async function(input, ctx, options) { * [requireAll] default @requireAll flag (default: true). * [omitDefault] default @omitDefault flag (default: false). * [documentLoader(url, options)] the document loader. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the framed output. @@ -511,7 +511,7 @@ jsonld.frame = async function(input, frame, options) { * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the linked output. @@ -547,7 +547,7 @@ jsonld.link = async function(input, ctx, options) { * 'application/n-quads' for N-Quads. * [documentLoader(url, options)] the document loader. * [useNative] true to use a native canonize algorithm - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the normalized output. @@ -604,7 +604,7 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { * (boolean, integer, double), false not to (default: false). * [rdfDirection] 'i18n-datatype' to support RDF transformation of * @direction (default: null). - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * * @return a Promise that resolves to the JSON-LD document. */ @@ -654,7 +654,7 @@ jsonld.fromRDF = async function(dataset, options) { * [produceGeneralizedRdf] true to output generalized RDF, false * to produce only standard RDF (default: false). * [documentLoader(url, options)] the document loader. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the RDF dataset. @@ -708,7 +708,7 @@ jsonld.toRDF = async function(input, options) { * [expandContext] a context to expand with. * [issuer] a jsonld.IdentifierIssuer to use to label blank nodes. * [documentLoader(url, options)] the document loader. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged node map. @@ -748,7 +748,7 @@ jsonld.createNodeMap = async function(input, options) { * new properties where a node is in the `object` position * (default: true). * [documentLoader(url, options)] the document loader. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged output. @@ -911,7 +911,7 @@ jsonld.get = async function(url, options) { * @param localCtx the local context to process. * @param [options] the options to use: * [documentLoader(url, options)] the document loader. - * [handleEvent] handler for events such as warnings. + * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the new active context. diff --git a/tests/misc.js b/tests/misc.js index beb9b45d..0cb3e36f 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1021,7 +1021,7 @@ describe('events', () => { let handled = false; const e = await jsonld.expand(d, { - handleEvent: ({event, next}) => { + eventHandler: ({event, next}) => { if(event.code === 'invalid reserved term') { handled = true; } else { @@ -1046,7 +1046,7 @@ describe('events', () => { let handled0 = false; let handled1 = false; const e0 = await jsonld.expand(d, { - handleEvent: { + eventHandler: { 'invalid reserved term': () => { handled0 = true; } @@ -1054,7 +1054,7 @@ describe('events', () => { }); // FIXME: ensure cache is being used const e1 = await jsonld.expand(d, { - handleEvent: { + eventHandler: { 'invalid reserved term': () => { handled1 = true; } @@ -1080,7 +1080,7 @@ describe('events', () => { let ranHandler1 = false; let handled = false; const e = await jsonld.expand(d, { - handleEvent: [ + eventHandler: [ ({next}) => { ranHandler0 = true; // skip to next handler @@ -1114,7 +1114,7 @@ describe('events', () => { let handled = false; const e = await jsonld.expand(d, { - handleEvent: { + eventHandler: { 'invalid reserved term': ({event}) => { assert.equal(event.details.term, '@RESERVED'); handled = true; @@ -1141,7 +1141,7 @@ describe('events', () => { let ranHandler3 = false; let handled = false; const e = await jsonld.expand(d, { - handleEvent: [ + eventHandler: [ ({next}) => { ranHandler0 = true; next(); @@ -1205,7 +1205,7 @@ describe('events', () => { let handledReservedValue = false; let handledLanguage = false; const e = await jsonld.expand(d, { - handleEvent: { + eventHandler: { 'invalid reserved term': () => { handledReservedTerm = true; }, @@ -1270,7 +1270,7 @@ describe('events', () => { let handledLanguage2 = false; const e2 = await jsonld.fromRDF(d2, { rdfDirection: 'i18n-datatype', - handleEvent: { + eventHandler: { 'invalid @language value': () => { handledLanguage2 = true; } From 1ebcc1fb25915f189276d91449366501a84a634c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 24 Feb 2020 13:22:17 -0500 Subject: [PATCH 5/5] Use object spread. --- lib/context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/context.js b/lib/context.js index 3013320d..acece63e 100644 --- a/lib/context.js +++ b/lib/context.js @@ -80,7 +80,7 @@ api.process = async ({ // store original options to use when replaying events const originalOptions = options; // shallow clone options with custom event handler - options = Object.assign({}, options, {eventHandler}); + options = {...options, eventHandler}; // resolve contexts const resolved = await options.contextResolver.resolve({