Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add event handler #366

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@
- Support test environment in EARL output.
- Support benchmark output in EARL output.
- Benchmark comparison tool.
- Event handler option `"eventHandler"` 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.

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

## 5.2.0 - 2021-04-07

### Changed
Expand Down
144 changes: 76 additions & 68 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const {
prependBase
} = require('./url');

const {
handleEvent: _handleEvent
} = require('./events');

const {
asArray: _asArray,
compareShortestLeast: _compareShortestLeast
Expand Down Expand Up @@ -61,6 +65,23 @@ api.process = async ({
return activeCtx;
}

// event handler for capturing events to replay when using a cached context
const events = [];
const eventHandler = [
({event, next}) => {
events.push(event);
next();
}
];
// chain to original handler
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 = {...options, eventHandler};

// resolve contexts
const resolved = await options.contextResolver.resolve({
activeCtx,
Expand Down Expand Up @@ -98,46 +119,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;
Expand All @@ -146,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;
}

Expand Down Expand Up @@ -414,7 +406,10 @@ api.process = async ({
}

// cache processed result
resolvedContext.setProcessed(activeCtx, rval);
resolvedContext.setProcessed(activeCtx, {
context: rval,
events
});
}

return rval;
Expand All @@ -429,9 +424,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 = ({
Expand Down Expand Up @@ -482,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(
Expand Down Expand Up @@ -564,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)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's an example where this would match? Looks like the above checks wouldn't allow {"@reverse": "@foo"} or similar to get this far.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous checks don't specifically look at @reverse, except to verify that it's a string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It checks it's a string, _expandIris it, then _isAbsoluteIri checks that. Seems like a keyword like string wouldn't get beyond those steps. Is there an example where it would?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just created w3c/json-ld-api#389, as the order of checks was inverted.

_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 {
Expand Down Expand Up @@ -601,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 {
Expand Down Expand Up @@ -918,23 +939,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});
}
}
};
Expand Down
92 changes: 92 additions & 0 deletions lib/events.js
Original file line number Diff line number Diff line change
@@ -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 '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
* 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 - processing options
*/
api.handleEvent = ({
event,
options
}) => {
const handlers = [].concat(
options.eventHandler ? _asArray(options.eventHandler) : [],
_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});
}
20 changes: 17 additions & 3 deletions lib/expand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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})*$/;
Expand Down Expand Up @@ -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
});
}
}

Expand Down
Loading