From 0fa6515f976e74e485b536da3aa5d446f4735952 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Mon, 17 May 2021 16:49:40 +1200 Subject: [PATCH 001/181] feat: extend expansion map for detecting relative IRIs for @id and @type terms --- lib/expand.js | 47 ++++++++++-- tests/misc.js | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+), 6 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index 44c5102f..50a455fa 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -505,10 +505,27 @@ async function _expandObject({ } } + const expandedValues = _asArray(value).map(v => + _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v); + + for(const i in expandedValues) { + if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) { + const expansionMapResult = await expansionMap({ + relativeIri: {type: key, value: expandedValues[i]}, + activeCtx, + activeProperty, + options, + insideList + }); + if(expansionMapResult !== undefined) { + expandedValues[i] = expansionMapResult; + } + } + } + _addValue( expandedParent, '@id', - _asArray(value).map(v => - _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v), + expandedValues, {propertyIsArray: options.isFrame}); continue; } @@ -525,12 +542,30 @@ async function _expandObject({ ])); } _validateTypeValue(value, options.isFrame); + + const expandedValues = _asArray(value).map(v => + _isString(v) ? + _expandIri(typeScopedContext, v, + {base: true, vocab: true}, options) : v); + + for(const i in expandedValues) { + if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) { + const expansionMapResult = await expansionMap({ + relativeIri: {type: key, value: expandedValues[i]}, + activeCtx, + activeProperty, + options, + insideList + }); + if(expansionMapResult !== undefined) { + expandedValues[i] = expansionMapResult; + } + } + } + _addValue( expandedParent, '@type', - _asArray(value).map(v => - _isString(v) ? - _expandIri(typeScopedContext, v, - {base: true, vocab: true}, options) : v), + expandedValues, {propertyIsArray: options.isFrame}); continue; } diff --git a/tests/misc.js b/tests/misc.js index 1c3758e0..c7156209 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -478,3 +478,207 @@ describe('literal JSON', () => { }); }); }); + +describe('expansionMap', () => { + it('should be called on un-mapped term', async () => { + const docWithUnMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + definedTerm: "is defined", + testUndefined: "is undefined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.unmappedProperty === 'testUndefined') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on nested un-mapped term', async () => { + const docWithUnMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + definedTerm: { + testUndefined: "is undefined" + } + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.unmappedProperty === 'testUndefined') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for id term', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + '@id': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for id term (nested)', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + '@id': "urn:absoluteIri", + definedTerm: { + '@id': "relativeiri" + } + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for aliased id term', async () => { + const docWithRelativeIriId = { + '@context': { + 'id': '@id', + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for type term', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for \ + type term with multiple relative iri types', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': ["relativeiri", "anotherRelativeiri" ], + definedTerm: "is defined" + }; + + let expansionMapCalledTimes = 0; + const expansionMap = info => { + if(info.relativeIri && + (info.relativeIri.value === 'relativeiri' || + info.relativeIri.value === 'anotherRelativeiri')) { + expansionMapCalledTimes++; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalledTimes, 2); + }); + + it('should be called on relative iri for \ + type term with multiple types', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': ["relativeiri", "definedTerm" ], + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for aliased type term', async () => { + const docWithRelativeIriId = { + '@context': { + 'type': "@type", + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + 'type': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); +}); From ac309feb6d093e01a0471d2a3f559a81322132a1 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Mon, 17 May 2021 16:49:49 +1200 Subject: [PATCH 002/181] fix: linting --- lib/util.js | 2 +- tests/test-common.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/util.js b/lib/util.js index 77da8f61..1458005a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -130,7 +130,7 @@ api.parseLinkHeader = header => { while((match = REGEX_LINK_HEADER_PARAMS.exec(params))) { result[match[1]] = (match[2] === undefined) ? match[3] : match[2]; } - const rel = result['rel'] || ''; + const rel = result.rel || ''; if(Array.isArray(rval[rel])) { rval[rel].push(result); } else if(rval.hasOwnProperty(rel)) { diff --git a/tests/test-common.js b/tests/test-common.js index b9982182..9cc520c7 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -398,7 +398,7 @@ function addManifest(manifest, parent) { */ function addTest(manifest, test, tests) { // expand @id and input base - const test_id = test['@id'] || test['id']; + const test_id = test['@id'] || test.id; //var number = test_id.substr(2); test['@id'] = manifest.baseIri + @@ -958,10 +958,10 @@ function createDocumentLoader(test) { } // If not JSON-LD, alternate may point there - if(linkHeaders['alternate'] && - linkHeaders['alternate'].type == 'application/ld+json' && + if(linkHeaders.alternate && + linkHeaders.alternate.type == 'application/ld+json' && !(contentType || '').match(/^application\/(\w*\+)?json$/)) { - doc.documentUrl = prependBase(url, linkHeaders['alternate'].target); + doc.documentUrl = prependBase(url, linkHeaders.alternate.target); } } } From 7463ebce2d32ce5fa96c0b7ef4f84acc7ab25f89 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Tue, 18 May 2021 11:05:39 +1200 Subject: [PATCH 003/181] fix: call expansion map from _expandIri instead of _expandObject --- lib/context.js | 13 ++++++++++++- lib/expand.js | 3 +++ tests/misc.js | 19 +++++++++---------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/context.js b/lib/context.js index 1a0161b5..0ad282ef 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1053,7 +1053,18 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { return prependBase(prependBase(options.base, activeCtx['@base']), value); } } else if(relativeTo.base) { - return prependBase(options.base, value); + value = prependBase(options.base, value); + } + + if(!_isAbsoluteIri(value) && options.expansionMap) { + // TODO: use `await` to support async + const expandedResult = options.expansionMap({ + relativeIri: value, + activeCtx, + }); + if(expandedResult !== undefined) { + value = expandedResult; + } } return value; diff --git a/lib/expand.js b/lib/expand.js index 50a455fa..a9caa6e9 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -420,6 +420,9 @@ async function _expandObject({ const nests = []; let unexpandedValue; + // Add expansion map to the processing options + options = {...options, expansionMap}; + // Figure out if this is the type for a JSON literal const isJsonType = element[typeKey] && _expandIri(activeCtx, diff --git a/tests/misc.js b/tests/misc.js index c7156209..bc8c7aac 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -534,7 +534,7 @@ describe('expansionMap', () => { let expansionMapCalled = false; const expansionMap = info => { - if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + if(info.relativeIri === 'relativeiri') { expansionMapCalled = true; } }; @@ -557,7 +557,7 @@ describe('expansionMap', () => { let expansionMapCalled = false; const expansionMap = info => { - if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + if(info.relativeIri === 'relativeiri') { expansionMapCalled = true; } }; @@ -579,7 +579,7 @@ describe('expansionMap', () => { let expansionMapCalled = false; const expansionMap = info => { - if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + if(info.relativeIri === 'relativeiri') { expansionMapCalled = true; } }; @@ -601,7 +601,7 @@ describe('expansionMap', () => { let expansionMapCalled = false; const expansionMap = info => { - if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + if(info.relativeIri === 'relativeiri') { expansionMapCalled = true; } }; @@ -624,16 +624,15 @@ describe('expansionMap', () => { let expansionMapCalledTimes = 0; const expansionMap = info => { - if(info.relativeIri && - (info.relativeIri.value === 'relativeiri' || - info.relativeIri.value === 'anotherRelativeiri')) { + if(info.relativeIri === 'relativeiri' || + info.relativeIri === 'anotherRelativeiri') { expansionMapCalledTimes++; } }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalledTimes, 2); + assert.equal(expansionMapCalledTimes, 3); }); it('should be called on relative iri for \ @@ -649,7 +648,7 @@ describe('expansionMap', () => { let expansionMapCalled = false; const expansionMap = info => { - if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + if(info.relativeIri === 'relativeiri') { expansionMapCalled = true; } }; @@ -672,7 +671,7 @@ describe('expansionMap', () => { let expansionMapCalled = false; const expansionMap = info => { - if(info.relativeIri && info.relativeIri.value === 'relativeiri') { + if(info.relativeIri === 'relativeiri') { expansionMapCalled = true; } }; From ba0aca98bed0f05d1d4e4b9d644afec57d6b5bd7 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Tue, 18 May 2021 11:11:25 +1200 Subject: [PATCH 004/181] fix: duplicate expansionMap logic --- lib/expand.js | 46 ++++++---------------------------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index a9caa6e9..6c852294 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -508,27 +508,10 @@ async function _expandObject({ } } - const expandedValues = _asArray(value).map(v => - _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v); - - for(const i in expandedValues) { - if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) { - const expansionMapResult = await expansionMap({ - relativeIri: {type: key, value: expandedValues[i]}, - activeCtx, - activeProperty, - options, - insideList - }); - if(expansionMapResult !== undefined) { - expandedValues[i] = expansionMapResult; - } - } - } - _addValue( expandedParent, '@id', - expandedValues, + _asArray(value).map(v => + _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v), {propertyIsArray: options.isFrame}); continue; } @@ -546,29 +529,12 @@ async function _expandObject({ } _validateTypeValue(value, options.isFrame); - const expandedValues = _asArray(value).map(v => - _isString(v) ? - _expandIri(typeScopedContext, v, - {base: true, vocab: true}, options) : v); - - for(const i in expandedValues) { - if(_isString(expandedValues[i]) && !_isAbsoluteIri(expandedValues[i])) { - const expansionMapResult = await expansionMap({ - relativeIri: {type: key, value: expandedValues[i]}, - activeCtx, - activeProperty, - options, - insideList - }); - if(expansionMapResult !== undefined) { - expandedValues[i] = expansionMapResult; - } - } - } - _addValue( expandedParent, '@type', - expandedValues, + _asArray(value).map(v => + _isString(v) ? + _expandIri(typeScopedContext, v, + {base: true, vocab: true}, options) : v), {propertyIsArray: options.isFrame}); continue; } From a525dd29a62c81cbdd3a6423bdd5d6d4aa459b74 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Tue, 18 May 2021 11:46:58 +1200 Subject: [PATCH 005/181] feat: add support for @base being './' --- lib/context.js | 2 +- lib/expand.js | 1 - tests/misc.js | 20 ++++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/context.js b/lib/context.js index 0ad282ef..d3a613a1 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1050,7 +1050,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { if(relativeTo.base && '@base' in activeCtx) { if(activeCtx['@base']) { // The null case preserves value as potentially relative - return prependBase(prependBase(options.base, activeCtx['@base']), value); + value = prependBase(prependBase(options.base, activeCtx['@base']), value); } } else if(relativeTo.base) { value = prependBase(options.base, value); diff --git a/lib/expand.js b/lib/expand.js index 6c852294..f0236e30 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -528,7 +528,6 @@ async function _expandObject({ ])); } _validateTypeValue(value, options.isFrame); - _addValue( expandedParent, '@type', _asArray(value).map(v => diff --git a/tests/misc.js b/tests/misc.js index bc8c7aac..72509fe7 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -680,4 +680,24 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); + + it("should be called on relative iri when @base value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@base": "./", + }, + '@id': "absoluteiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/absoluteiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); }); From 2f2ea3cde11f132258c172c83fd7ffa8bbe9be9c Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Thu, 20 May 2021 13:52:12 +1200 Subject: [PATCH 006/181] fix: add support for when @vocab results in relative iri --- lib/context.js | 5 +++-- lib/expand.js | 4 ++++ tests/misc.js | 44 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/lib/context.js b/lib/context.js index d3a613a1..60db67ea 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1043,11 +1043,11 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { // prepend vocab if(relativeTo.vocab && '@vocab' in activeCtx) { - return activeCtx['@vocab'] + value; + value = activeCtx['@vocab'] + value; } // prepend base - if(relativeTo.base && '@base' in activeCtx) { + else if(relativeTo.base && '@base' in activeCtx) { if(activeCtx['@base']) { // The null case preserves value as potentially relative value = prependBase(prependBase(options.base, activeCtx['@base']), value); @@ -1061,6 +1061,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { const expandedResult = options.expansionMap({ relativeIri: value, activeCtx, + options }); if(expandedResult !== undefined) { value = expandedResult; diff --git a/lib/expand.js b/lib/expand.js index f0236e30..6201661e 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -75,6 +75,10 @@ api.expand = async ({ typeScopedContext = null, expansionMap = () => undefined }) => { + + // Add expansion map to the processing options + options = { ...options, expansionMap }; + // nothing to expand if(element === null || element === undefined) { return null; diff --git a/tests/misc.js b/tests/misc.js index 72509fe7..73aca8ff 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -686,12 +686,52 @@ describe('expansionMap', () => { '@context': { "@base": "./", }, - '@id': "absoluteiri", + '@id': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when @base value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@base": "./", + }, + '@id': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when @vocab value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@vocab": "./", + }, + '@type': "relativeiri", }; let expansionMapCalled = false; const expansionMap = info => { - if(info.relativeIri === '/absoluteiri') { + if(info.relativeIri === '/relativeiri') { expansionMapCalled = true; } }; From 74f6069bcda83b7e331ebcc7d17a89f87bba8e18 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Fri, 21 May 2021 05:05:47 +1200 Subject: [PATCH 007/181] Update lib/expand.js Co-authored-by: Dave Longley --- lib/expand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/expand.js b/lib/expand.js index 6201661e..518f1428 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -76,7 +76,7 @@ api.expand = async ({ expansionMap = () => undefined }) => { - // Add expansion map to the processing options + // add expansion map to the processing options options = { ...options, expansionMap }; // nothing to expand From 163bb5d249e04e66a4a7be332502f930b43b4832 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Fri, 21 May 2021 05:06:14 +1200 Subject: [PATCH 008/181] Update lib/expand.js Co-authored-by: Dave Longley --- lib/expand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/expand.js b/lib/expand.js index 518f1428..62e41e59 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -424,7 +424,7 @@ async function _expandObject({ const nests = []; let unexpandedValue; - // Add expansion map to the processing options + // add expansion map to the processing options options = {...options, expansionMap}; // Figure out if this is the type for a JSON literal From 3bbe9170db1b2ceaf8630a1794e9ff7ef566f5a7 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Fri, 21 May 2021 05:06:24 +1200 Subject: [PATCH 009/181] Update tests/misc.js Co-authored-by: Dave Longley --- tests/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/misc.js b/tests/misc.js index 73aca8ff..f98f0abd 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -480,7 +480,7 @@ describe('literal JSON', () => { }); describe('expansionMap', () => { - it('should be called on un-mapped term', async () => { + it('should be called on unmapped term', async () => { const docWithUnMappedTerm = { '@context': { 'definedTerm': 'https://example.com#definedTerm' From f0c846749c2508b9f9f652ca523446d8c31c2def Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Fri, 21 May 2021 05:06:32 +1200 Subject: [PATCH 010/181] Update tests/misc.js Co-authored-by: Dave Longley --- tests/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/misc.js b/tests/misc.js index f98f0abd..39b44cfa 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -501,7 +501,7 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it('should be called on nested un-mapped term', async () => { + it('should be called on nested unmapped term', async () => { const docWithUnMappedTerm = { '@context': { 'definedTerm': 'https://example.com#definedTerm' From 16f336705556584062c9a1aae7e236daf4c96719 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Fri, 21 May 2021 05:58:16 +1200 Subject: [PATCH 011/181] fix: style nit --- lib/context.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/context.js b/lib/context.js index 60db67ea..308c4445 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1041,13 +1041,11 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { } } - // prepend vocab if(relativeTo.vocab && '@vocab' in activeCtx) { + // prepend vocab value = activeCtx['@vocab'] + value; - } - - // prepend base - else if(relativeTo.base && '@base' in activeCtx) { + } else if(relativeTo.base && '@base' in activeCtx) { + // prepend base if(activeCtx['@base']) { // The null case preserves value as potentially relative value = prependBase(prependBase(options.base, activeCtx['@base']), value); From e3d8c15bb08c2e7a051444f7e75235059cf2cfae Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Fri, 21 May 2021 10:05:35 +1200 Subject: [PATCH 012/181] feat: add prependIri expansionMap hook and tests --- lib/context.js | 88 ++++++- lib/expand.js | 2 +- tests/misc.js | 609 ++++++++++++++++++++++++++++++++----------------- 3 files changed, 486 insertions(+), 213 deletions(-) diff --git a/lib/context.js b/lib/context.js index 308c4445..f6fdd85e 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1043,18 +1043,96 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { if(relativeTo.vocab && '@vocab' in activeCtx) { // prepend vocab - value = activeCtx['@vocab'] + value; + const prependedResult = activeCtx['@vocab'] + value; + let expansionMapResult = undefined; + if(options && options.expansionMap) { + // if we are about to expand the value by prepending + // @vocab then call the expansion map to inform + // interested callers that this is occurring + + // TODO: use `await` to support async + expansionMapResult = options.expansionMap({ + prependedIri: { + type: '@vocab', + vocab: activeCtx['@vocab'], + value, + result: prependedResult + }, + activeCtx, + options + }); + + } + if(expansionMapResult !== undefined) { + value = expansionMapResult; + } else { + // the null case preserves value as potentially relative + value = prependedResult; + } } else if(relativeTo.base && '@base' in activeCtx) { // prepend base if(activeCtx['@base']) { - // The null case preserves value as potentially relative - value = prependBase(prependBase(options.base, activeCtx['@base']), value); + const prependedResult = prependBase( + prependBase(options.base, activeCtx['@base']), value); + + let expansionMapResult = undefined; + if(options && options.expansionMap) { + // if we are about to expand the value by prepending + // @base then call the expansion map to inform + // interested callers that this is occurring + + // TODO: use `await` to support async + expansionMapResult = options.expansionMap({ + prependedIri: { + type: '@base', + base: activeCtx['@base'], + value, + result: prependedResult + }, + activeCtx, + options + }); + } + if(expansionMapResult !== undefined) { + value = expansionMapResult; + } else { + // the null case preserves value as potentially relative + value = prependedResult; + } } } else if(relativeTo.base) { - value = prependBase(options.base, value); + const prependedResult = prependBase(options.base, value); + let expansionMapResult = undefined; + if(options && options.expansionMap) { + // if we are about to expand the value by prepending + // @base then call the expansion map to inform + // interested callers that this is occurring + + // TODO: use `await` to support async + expansionMapResult = options.expansionMap({ + prependedIri: { + type: '@base', + base: options.base, + value, + result: prependedResult + }, + activeCtx, + options + }); + } + if(expansionMapResult !== undefined) { + value = expansionMapResult; + } else { + value = prependedResult; + } } - if(!_isAbsoluteIri(value) && options.expansionMap) { + if(!_isAbsoluteIri(value) && options && options.expansionMap) { + // if the result of the expansion is not an absolute iri then + // call the expansion map to inform interested callers that + // the resulting value is a relative iri, which can result in + // it being dropped when converting to other RDF representations + // TODO: use `await` to support async const expandedResult = options.expansionMap({ relativeIri: value, diff --git a/lib/expand.js b/lib/expand.js index 62e41e59..0cf50746 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -77,7 +77,7 @@ api.expand = async ({ }) => { // add expansion map to the processing options - options = { ...options, expansionMap }; + options = {...options, expansionMap}; // nothing to expand if(element === null || element === undefined) { diff --git a/tests/misc.js b/tests/misc.js index 39b44cfa..4ac9578e 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -480,264 +480,459 @@ describe('literal JSON', () => { }); describe('expansionMap', () => { - it('should be called on unmapped term', async () => { - const docWithUnMappedTerm = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - definedTerm: "is defined", - testUndefined: "is undefined" - }; + describe('unmappedProperty', () => { + it('should be called on unmapped term', async () => { + const docWithUnMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + definedTerm: "is defined", + testUndefined: "is undefined" + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.unmappedProperty === 'testUndefined') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.unmappedProperty === 'testUndefined') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it('should be called on nested unmapped term', async () => { - const docWithUnMappedTerm = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - definedTerm: { - testUndefined: "is undefined" - } - }; + it('should be called on nested unmapped term', async () => { + const docWithUnMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + definedTerm: { + testUndefined: "is undefined" + } + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.unmappedProperty === 'testUndefined') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.unmappedProperty === 'testUndefined') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.equal(expansionMapCalled, true); + }); }); - it('should be called on relative iri for id term', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - '@id': "relativeiri", - definedTerm: "is defined" - }; + describe('relativeIri', () => { + it('should be called on relative iri for id term', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + '@id': "relativeiri", + definedTerm: "is defined" + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it('should be called on relative iri for id term (nested)', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - '@id': "urn:absoluteIri", - definedTerm: { - '@id': "relativeiri" - } - }; + it('should be called on relative iri for id term (nested)', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + '@id': "urn:absoluteIri", + definedTerm: { + '@id': "relativeiri" + } + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it('should be called on relative iri for aliased id term', async () => { - const docWithRelativeIriId = { - '@context': { - 'id': '@id', - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "relativeiri", - definedTerm: "is defined" - }; + it('should be called on relative iri for aliased id term', async () => { + const docWithRelativeIriId = { + '@context': { + 'id': '@id', + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "relativeiri", + definedTerm: "is defined" + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it('should be called on relative iri for type term', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - '@type': "relativeiri", - definedTerm: "is defined" - }; + it('should be called on relative iri for type term', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': "relativeiri", + definedTerm: "is defined" + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it('should be called on relative iri for \ - type term with multiple relative iri types', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - '@type': ["relativeiri", "anotherRelativeiri" ], - definedTerm: "is defined" - }; + it('should be called on relative iri for \ + type term with multiple relative iri types', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': ["relativeiri", "anotherRelativeiri" ], + definedTerm: "is defined" + }; - let expansionMapCalledTimes = 0; - const expansionMap = info => { - if(info.relativeIri === 'relativeiri' || - info.relativeIri === 'anotherRelativeiri') { - expansionMapCalledTimes++; - } - }; + let expansionMapCalledTimes = 0; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri' || + info.relativeIri === 'anotherRelativeiri') { + expansionMapCalledTimes++; + } + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalledTimes, 3); - }); + assert.equal(expansionMapCalledTimes, 3); + }); - it('should be called on relative iri for \ - type term with multiple types', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - '@type': ["relativeiri", "definedTerm" ], - definedTerm: "is defined" - }; + it('should be called on relative iri for \ + type term with multiple types', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': ["relativeiri", "definedTerm" ], + definedTerm: "is defined" + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it('should be called on relative iri for aliased type term', async () => { - const docWithRelativeIriId = { - '@context': { - 'type': "@type", - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - 'type': "relativeiri", - definedTerm: "is defined" - }; + it('should be called on relative iri for aliased type term', async () => { + const docWithRelativeIriId = { + '@context': { + 'type': "@type", + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + 'type': "relativeiri", + definedTerm: "is defined" + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } - }; + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when \ + @base value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@base": "./", + }, + '@id': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when \ + @base value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@base": "./", + }, + '@id': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when \ + @vocab value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@vocab": "./", + }, + '@type': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); }); - it("should be called on relative iri when @base value is './'", async () => { - const docWithRelativeIriId = { - '@context': { - "@base": "./", - }, - '@id': "relativeiri", - }; + describe('prependedIri', () => { + it("should be called when property is \ + being expanded with `@vocab`", async () => { + const doc = { + '@context': { + "@vocab": "http://example.com/", + }, + 'term': "termValue", + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === '/relativeiri') { + let expansionMapCalled = false; + const expansionMap = info => { + assert.deepStrictEqual(info.prependedIri, { + type: '@vocab', + vocab: 'http://example.com/', + value: 'term', + result: 'http://example.com/term' + }); expansionMapCalled = true; - } - }; + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it("should be called on relative iri when @base value is './'", async () => { - const docWithRelativeIriId = { - '@context': { - "@base": "./", - }, - '@id': "relativeiri", - }; + it("should be called when '@type' is \ + being expanded with `@vocab`", async () => { + const doc = { + '@context': { + "@vocab": "http://example.com/", + }, + '@type': "relativeIri", + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === '/relativeiri') { + let expansionMapCalled = false; + const expansionMap = info => { + assert.deepStrictEqual(info.prependedIri, { + type: '@vocab', + vocab: 'http://example.com/', + value: 'relativeIri', + result: 'http://example.com/relativeIri' + }); expansionMapCalled = true; - } - }; + }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); - }); + assert.equal(expansionMapCalled, true); + }); - it("should be called on relative iri when @vocab value is './'", async () => { - const docWithRelativeIriId = { - '@context': { - "@vocab": "./", - }, - '@type': "relativeiri", - }; + it("should be called when aliased '@type' is \ + being expanded with `@vocab`", async () => { + const doc = { + '@context': { + "@vocab": "http://example.com/", + "type": "@type" + }, + 'type': "relativeIri", + }; - let expansionMapCalled = false; - const expansionMap = info => { - if(info.relativeIri === '/relativeiri') { + let expansionMapCalled = false; + const expansionMap = info => { + assert.deepStrictEqual(info.prependedIri, { + type: '@vocab', + vocab: 'http://example.com/', + value: 'relativeIri', + result: 'http://example.com/relativeIri' + }); expansionMapCalled = true; - } - }; + }; + + await jsonld.expand(doc, {expansionMap}); - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + assert.equal(expansionMapCalled, true); + }); + + it("should be called when '@id' is being \ + expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + }, + '@id': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; - assert.equal(expansionMapCalled, true); + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when aliased '@id' \ + is being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + "id": "@id" + }, + 'id': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when '@type' is \ + being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + }, + '@type': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when aliased '@type' is \ + being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + "type": "@type" + }, + 'type': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); }); }); From e7c6dc093c32964301d4456b9c9057041548e093 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Wed, 26 May 2021 10:37:24 +1200 Subject: [PATCH 013/181] fix: refactor @base expansionLogic --- lib/context.js | 49 +++++++++++++++---------------------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/lib/context.js b/lib/context.js index f6fdd85e..9e559c09 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1069,42 +1069,22 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { // the null case preserves value as potentially relative value = prependedResult; } - } else if(relativeTo.base && '@base' in activeCtx) { + } else if(relativeTo.base) { // prepend base - if(activeCtx['@base']) { - const prependedResult = prependBase( - prependBase(options.base, activeCtx['@base']), value); - - let expansionMapResult = undefined; - if(options && options.expansionMap) { - // if we are about to expand the value by prepending - // @base then call the expansion map to inform - // interested callers that this is occurring - - // TODO: use `await` to support async - expansionMapResult = options.expansionMap({ - prependedIri: { - type: '@base', - base: activeCtx['@base'], - value, - result: prependedResult - }, - activeCtx, - options - }); - } - if(expansionMapResult !== undefined) { - value = expansionMapResult; - } else { - // the null case preserves value as potentially relative - value = prependedResult; + let prependedResult; + let expansionMapResult; + let base; + if('@base' in activeCtx) { + if(activeCtx['@base']) { + base = prependBase(options.base, activeCtx['@base']); + prependedResult = prependBase(base, value); } + } else { + base = options.base; + prependedResult = prependBase(options.base, value); } - } else if(relativeTo.base) { - const prependedResult = prependBase(options.base, value); - let expansionMapResult = undefined; if(options && options.expansionMap) { - // if we are about to expand the value by prepending + // if we are about to expand the value by pre-pending // @base then call the expansion map to inform // interested callers that this is occurring @@ -1112,7 +1092,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { expansionMapResult = options.expansionMap({ prependedIri: { type: '@base', - base: options.base, + base, value, result: prependedResult }, @@ -1122,7 +1102,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { } if(expansionMapResult !== undefined) { value = expansionMapResult; - } else { + } else if(prependedResult !== undefined) { + // the null case preserves value as potentially relative value = prependedResult; } } From 1f51f75a988c60f65e05d4f8105d45ece1cd88a7 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Wed, 9 Jun 2021 12:06:47 +1200 Subject: [PATCH 014/181] fix: add hook for typeExpansion, cover @base null --- lib/context.js | 20 ++++++++++++--- lib/expand.js | 8 +++--- tests/misc.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/lib/context.js b/lib/context.js index 9e559c09..cc2aefa1 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1041,6 +1041,14 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { } } + // A flag that captures whether the iri being expanded is + // the value for an @type + let typeExpansion = false; + + if (options !== undefined && options.typeExpansion !== undefined) { + typeExpansion = options.typeExpansion; + } + if(relativeTo.vocab && '@vocab' in activeCtx) { // prepend vocab const prependedResult = activeCtx['@vocab'] + value; @@ -1056,7 +1064,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { type: '@vocab', vocab: activeCtx['@vocab'], value, - result: prependedResult + result: prependedResult, + typeExpansion, }, activeCtx, options @@ -1078,6 +1087,9 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { if(activeCtx['@base']) { base = prependBase(options.base, activeCtx['@base']); prependedResult = prependBase(base, value); + } else { + base = activeCtx['@base']; + prependedResult = value; } } else { base = options.base; @@ -1094,7 +1106,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { type: '@base', base, value, - result: prependedResult + result: prependedResult, + typeExpansion, }, activeCtx, options @@ -1102,7 +1115,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { } if(expansionMapResult !== undefined) { value = expansionMapResult; - } else if(prependedResult !== undefined) { + } else { // the null case preserves value as potentially relative value = prependedResult; } @@ -1118,6 +1131,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { const expandedResult = options.expansionMap({ relativeIri: value, activeCtx, + typeExpansion, options }); if(expandedResult !== undefined) { diff --git a/lib/expand.js b/lib/expand.js index 0cf50746..5503555b 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -431,7 +431,7 @@ async function _expandObject({ const isJsonType = element[typeKey] && _expandIri(activeCtx, (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), - {vocab: true}, options) === '@json'; + {vocab: true}, { ...options, typeExpansion: true }) === '@json'; for(const key of keys) { let value = element[key]; @@ -527,7 +527,7 @@ async function _expandObject({ value = Object.fromEntries(Object.entries(value).map(([k, v]) => [ _expandIri(typeScopedContext, k, {vocab: true}), _asArray(v).map(vv => - _expandIri(typeScopedContext, vv, {base: true, vocab: true}) + _expandIri(typeScopedContext, vv, {base: true, vocab: true}, { ...options, typeExpansion: true }) ) ])); } @@ -537,7 +537,7 @@ async function _expandObject({ _asArray(value).map(v => _isString(v) ? _expandIri(typeScopedContext, v, - {base: true, vocab: true}, options) : v), + {base: true, vocab: true}, { ...options, typeExpansion: true }) : v), {propertyIsArray: options.isFrame}); continue; } @@ -937,7 +937,7 @@ function _expandValue({activeCtx, activeProperty, value, options}) { if(expandedProperty === '@id') { return _expandIri(activeCtx, value, {base: true}, options); } else if(expandedProperty === '@type') { - return _expandIri(activeCtx, value, {vocab: true, base: true}, options); + return _expandIri(activeCtx, value, {vocab: true, base: true}, { ...options, typeExpansion: true }); } // get type definition from context diff --git a/tests/misc.js b/tests/misc.js index 4ac9578e..aace2768 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -614,6 +614,36 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); + it('should be called on relative iri for type term in scoped context', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedType': { + '@id': 'https://example.com#definedType', + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + + } + } + }, + 'id': "urn:absoluteiri", + '@type': "definedType", + definedTerm: { + '@type': 'relativeiri' + } + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + it('should be called on relative iri for \ type term with multiple relative iri types', async () => { const docWithRelativeIriId = { @@ -638,6 +668,38 @@ describe('expansionMap', () => { assert.equal(expansionMapCalledTimes, 3); }); + it('should be called on relative iri for \ + type term with multiple relative iri types in scoped context', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedType': { + '@id': 'https://example.com#definedType', + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + + } + } + }, + 'id': "urn:absoluteiri", + '@type': "definedType", + definedTerm: { + '@type': ["relativeiri", "anotherRelativeiri" ] + } + }; + + let expansionMapCalledTimes = 0; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri' || + info.relativeIri === 'anotherRelativeiri') { + expansionMapCalledTimes++; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalledTimes, 3); + }); + it('should be called on relative iri for \ type term with multiple types', async () => { const docWithRelativeIriId = { @@ -764,6 +826,7 @@ describe('expansionMap', () => { type: '@vocab', vocab: 'http://example.com/', value: 'term', + typeExpansion: false, result: 'http://example.com/term' }); expansionMapCalled = true; @@ -789,6 +852,7 @@ describe('expansionMap', () => { type: '@vocab', vocab: 'http://example.com/', value: 'relativeIri', + typeExpansion: true, result: 'http://example.com/relativeIri' }); expansionMapCalled = true; @@ -815,6 +879,7 @@ describe('expansionMap', () => { type: '@vocab', vocab: 'http://example.com/', value: 'relativeIri', + typeExpansion: true, result: 'http://example.com/relativeIri' }); expansionMapCalled = true; @@ -841,6 +906,7 @@ describe('expansionMap', () => { type: '@base', base: 'http://example.com/', value: 'relativeIri', + typeExpansion: false, result: 'http://example.com/relativeIri' }); expansionMapCalled = true; @@ -869,6 +935,7 @@ describe('expansionMap', () => { type: '@base', base: 'http://example.com/', value: 'relativeIri', + typeExpansion: false, result: 'http://example.com/relativeIri' }); expansionMapCalled = true; @@ -896,6 +963,7 @@ describe('expansionMap', () => { type: '@base', base: 'http://example.com/', value: 'relativeIri', + typeExpansion: true, result: 'http://example.com/relativeIri' }); expansionMapCalled = true; @@ -924,6 +992,7 @@ describe('expansionMap', () => { type: '@base', base: 'http://example.com/', value: 'relativeIri', + typeExpansion: true, result: 'http://example.com/relativeIri' }); expansionMapCalled = true; From 32226419c3ae2c5ef196e1e28ac9e04dab3e1db7 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Wed, 9 Jun 2021 13:35:16 +1200 Subject: [PATCH 015/181] Update lib/context.js Co-authored-by: Dave Longley --- lib/context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/context.js b/lib/context.js index cc2aefa1..5f0de789 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1045,7 +1045,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { // the value for an @type let typeExpansion = false; - if (options !== undefined && options.typeExpansion !== undefined) { + if(options !== undefined && options.typeExpansion !== undefined) { typeExpansion = options.typeExpansion; } From 20e2286d198ce7d376320306f9df3667f48a4544 Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Wed, 9 Jun 2021 13:38:15 +1200 Subject: [PATCH 016/181] fix: minor linting issues --- lib/expand.js | 11 +++++++---- tests/misc.js | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index 5503555b..737def7b 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -431,7 +431,7 @@ async function _expandObject({ const isJsonType = element[typeKey] && _expandIri(activeCtx, (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), - {vocab: true}, { ...options, typeExpansion: true }) === '@json'; + {vocab: true}, {...options, typeExpansion: true}) === '@json'; for(const key of keys) { let value = element[key]; @@ -527,7 +527,8 @@ async function _expandObject({ value = Object.fromEntries(Object.entries(value).map(([k, v]) => [ _expandIri(typeScopedContext, k, {vocab: true}), _asArray(v).map(vv => - _expandIri(typeScopedContext, vv, {base: true, vocab: true}, { ...options, typeExpansion: true }) + _expandIri(typeScopedContext, vv, {base: true, vocab: true}, + {...options, typeExpansion: true}) ) ])); } @@ -537,7 +538,8 @@ async function _expandObject({ _asArray(value).map(v => _isString(v) ? _expandIri(typeScopedContext, v, - {base: true, vocab: true}, { ...options, typeExpansion: true }) : v), + {base: true, vocab: true}, + {...options, typeExpansion: true}) : v), {propertyIsArray: options.isFrame}); continue; } @@ -937,7 +939,8 @@ function _expandValue({activeCtx, activeProperty, value, options}) { if(expandedProperty === '@id') { return _expandIri(activeCtx, value, {base: true}, options); } else if(expandedProperty === '@type') { - return _expandIri(activeCtx, value, {vocab: true, base: true}, { ...options, typeExpansion: true }); + return _expandIri(activeCtx, value, {vocab: true, base: true}, + {...options, typeExpansion: true}); } // get type definition from context diff --git a/tests/misc.js b/tests/misc.js index aace2768..d9dae4fc 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -614,7 +614,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it('should be called on relative iri for type term in scoped context', async () => { + it('should be called on relative iri for type\ + term in scoped context', async () => { const docWithRelativeIriId = { '@context': { 'definedType': { From 6baad64cdad46086c4f8367a67c73ef403899b33 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 23:25:43 -0400 Subject: [PATCH 017/181] Skip failing compact tests. --- tests/test-common.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test-common.js b/tests/test-common.js index 9cc520c7..114e615e 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -35,6 +35,9 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ + /compact-manifest#t0111$/, + /compact-manifest#t0112$/, + /compact-manifest#t0113$/, // html /html-manifest#tc001$/, /html-manifest#tc002$/, From 5dcc0d2429c6f2484d2b972d7e0a3d43d8a519ee Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 23:27:58 -0400 Subject: [PATCH 018/181] Test on Node.js 16.x. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7f20f42..27cb66e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [12.x, 14.x] + node-version: [12.x, 14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From a6b5e4febd517387d122bb193d04b79c39309880 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 19 Feb 2019 20:30:15 -0500 Subject: [PATCH 019/181] Support test loader url rewrite map option. - Experimental option used for benchmarking. --- tests/test-common.js | 97 ++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/tests/test-common.js b/tests/test-common.js index 114e615e..2c040cc7 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -570,42 +570,21 @@ function addTest(manifest, test, tests) { throw Error('Unknown test type: ' + test.type); } + let benchResults = null; if(options.benchmark) { - // pre-load params to avoid doc loader and parser timing - const benchParams = testInfo.params.map(param => param(test, { - load: true - })); - const benchValues = await Promise.all(benchParams); - - await new Promise((resolve, reject) => { - const suite = new benchmark.Suite(); - suite.add({ - name: test.name, - defer: true, - fn: deferred => { - jsonld[fn].apply(null, benchValues).then(() => { - deferred.resolve(); - }); - } - }); - suite - .on('start', e => { - self.timeout((e.target.maxTime + 2) * 1000); - }) - .on('cycle', e => { - console.log(String(e.target)); - }) - .on('error', err => { - reject(new Error(err)); - }) - .on('complete', () => { - resolve(); - }) - .run({async: true}); + benchResults = await runBenchmark({ + test, + fn, + params: testInfo.params.map(param => param(test, { + // pre-load params to avoid doc loader and parser timing + load: true + })), + mochaTest: self }); } if(options.earl.report) { + // TODO: add benchmark info options.earl.report.addAssertion(test, true); } } catch(err) { @@ -625,6 +604,38 @@ function addTest(manifest, test, tests) { } } +async function runBenchmark({test, fn, params, mochaTest}) { + const values = await Promise.all(params); + + return new Promise((resolve, reject) => { + const suite = new benchmark.Suite(); + suite.add({ + name: test.name, + defer: true, + fn: deferred => { + jsonld[fn].apply(null, values).then(() => { + deferred.resolve(); + }); + } + }); + suite + .on('start', e => { + // set timeout to a bit more than max benchmark time + mochaTest.timeout((e.target.maxTime + 2) * 1000); + }) + .on('cycle', e => { + console.log(String(e.target)); + }) + .on('error', err => { + reject(new Error(err)); + }) + .on('complete', e => { + resolve(); + }) + .run({async: true}); + }); +} + function getJsonLdTestType(test) { const types = Object.keys(TEST_TYPES); for(let i = 0; i < types.length; ++i) { @@ -892,6 +903,26 @@ function basename(filename) { return filename.substr(idx + 1); } +// check test.option.loader.rewrite map for url, +// if no test rewrite, check manifest, +// else no rewrite +function rewrite(test, url) { + if(test.option && + test.option.loader && + test.option.loader.rewrite && + url in test.option.loader.rewrite) { + return test.option.loader.rewrite[url]; + } + const manifest = test.manifest; + if(manifest.option && + manifest.option.loader && + manifest.option.loader.rewrite && + url in manifest.option.loader.rewrite) { + return manifest.option.loader.rewrite[url]; + } + return url; +} + /** * Creates a test remote document loader. * @@ -906,6 +937,7 @@ function createDocumentLoader(test) { 'https://w3c.github.io/json-ld-api/tests', 'https://w3c.github.io/json-ld-framing/tests' ]; + const localLoader = function(url) { // always load remote-doc tests remotely in node // NOTE: disabled due to github pages issues. @@ -913,6 +945,9 @@ function createDocumentLoader(test) { // return jsonld.documentLoader(url); //} + // handle loader rewrite options for test or manifest + url = rewrite(test, url); + // FIXME: this check only works for main test suite and will not work if: // - running other tests and main test suite not installed // - use other absolute URIs but want to load local files From 9d5b66c7b88b985b54730f1e923b90e8cc53926a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 7 Sep 2019 20:10:53 -0400 Subject: [PATCH 020/181] Output benchmark Hz in EARL report. - Output Hz in EARL report. - Add extra env data in EARL fields. --- tests/earl-report.js | 21 ++++++++++++--------- tests/test-common.js | 21 +++++++++++++++------ tests/test-karma.js | 3 +++ tests/test.js | 3 +++ 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/tests/earl-report.js b/tests/earl-report.js index 6fee3570..3a2feec9 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -11,6 +11,7 @@ * * @param options {Object} reporter options * id: {String} report id + * env: {String} environment description */ function EarlReport(options) { let today = new Date(); @@ -22,6 +23,7 @@ function EarlReport(options) { this.now = new Date(); this.now.setMilliseconds(0); this.id = options.id; + this.env = options.env; /* eslint-disable quote-props */ this._report = { '@context': { @@ -30,6 +32,7 @@ function EarlReport(options) { 'dc': 'http://purl.org/dc/terms/', 'earl': 'http://www.w3.org/ns/earl#', 'xsd': 'http://www.w3.org/2001/XMLSchema#', + 'jsonld': 'http://www.w3.org/ns/json-ld#', 'doap:homepage': {'@type': '@id'}, 'doap:license': {'@type': '@id'}, 'dc:creator': {'@type': '@id'}, @@ -73,17 +76,16 @@ function EarlReport(options) { 'subjectOf': [] }; /* eslint-enable quote-props */ - const version = require('../package.json').version; - // FIXME: Improve "Node.js" vs "browser" id reporting - // this._report['@id'] += '#' + this.id; - // this._report['doap:name'] += ' ' + this.id; - // this._report['dc:title'] += ' ' + this.id; - this._report['doap:release']['doap:name'] = - this._report['doap:name'] + ' ' + version; - this._report['doap:release']['doap:revision'] = version; + this._report['@id'] += + '#' + this.id; + this._report['doap:name'] += + ' ' + this.id + (this.env ? ' ' + this.env : ''); + this._report['dc:title'] += + ' ' + this.id + (this.env ? ' ' + this.env : ''); } -EarlReport.prototype.addAssertion = function(test, pass) { +EarlReport.prototype.addAssertion = function(test, pass, options) { + options = options || {}; this._report.subjectOf.push({ '@type': 'earl:Assertion', 'earl:assertedBy': this._report['doap:developer']['@id'], @@ -93,6 +95,7 @@ EarlReport.prototype.addAssertion = function(test, pass) { '@type': 'earl:TestResult', 'dc:date': this.now.toISOString(), 'earl:outcome': pass ? 'earl:passed' : 'earl:failed' + ...options.extra } }); return this; diff --git a/tests/test-common.js b/tests/test-common.js index 2c040cc7..ca885549 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -264,7 +264,10 @@ const SKIP_TESTS = []; // create earl report if(options.earl && options.earl.filename) { - options.earl.report = new EarlReport({id: options.earl.id}); + options.earl.report = new EarlReport({ + id: options.earl.id, + env: options.earl.env + }); } return new Promise(resolve => { @@ -570,9 +573,9 @@ function addTest(manifest, test, tests) { throw Error('Unknown test type: ' + test.type); } - let benchResults = null; + let benchResult = null if(options.benchmark) { - benchResults = await runBenchmark({ + const result = await runBenchmark({ test, fn, params: testInfo.params.map(param => param(test, { @@ -581,11 +584,17 @@ function addTest(manifest, test, tests) { })), mochaTest: self }); + benchResult = { + 'jsonld:benchmarkHz': result.target.hz + }; } if(options.earl.report) { - // TODO: add benchmark info - options.earl.report.addAssertion(test, true); + options.earl.report.addAssertion(test, true, { + extra: { + ...benchResult + } + }); } } catch(err) { if(options.bailOnError) { @@ -630,7 +639,7 @@ async function runBenchmark({test, fn, params, mochaTest}) { reject(new Error(err)); }) .on('complete', e => { - resolve(); + resolve(e); }) .run({async: true}); }); diff --git a/tests/test-karma.js b/tests/test-karma.js index 77313832..9e4f5853 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -7,6 +7,8 @@ * JSONLD_TESTS="r1 r2 ..." * Output an EARL report: * EARL=filename + * Output EARL environment description (any appropraite string): + * EARL_ENV='CPU=Intel-i7-4790K@4.00GHz,Node.js=v10.16.3,jsonld.js=v1.7.0' * Bail with tests fail: * BAIL=true * Verbose skip reasons: @@ -95,6 +97,7 @@ const options = { }, earl: { id: 'browser', + env: process.env.EARL_ENV, filename: process.env.EARL }, verboseSkip: process.env.VERBOSE_SKIP === 'true', diff --git a/tests/test.js b/tests/test.js index b7680b09..5167b817 100644 --- a/tests/test.js +++ b/tests/test.js @@ -7,6 +7,8 @@ * JSONLD_TESTS="r1 r2 ..." * Output an EARL report: * EARL=filename + * Output EARL environment description (any appropraite string): + * EARL_ENV='CPU=Intel-i7-4790K@4.00GHz,Node.js=v10.16.3,jsonld.js=v1.7.0' * Bail with tests fail: * BAIL=true * Verbose skip reasons: @@ -102,6 +104,7 @@ const options = { exit: code => process.exit(code), earl: { id: 'Node.js', + env: process.env.EARL_ENV, filename: process.env.EARL }, verboseSkip: process.env.VERBOSE_SKIP === 'true', From e99d1184ae77b9caa5fbdbc3804eb73b9557bd5e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 7 Sep 2019 20:29:13 -0400 Subject: [PATCH 021/181] Add more benchmark details. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index e1e849ff..02be20c0 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,13 @@ Use a command line with a test suite and a benchmark flag: JSONLD_TESTS=/tmp/benchmark-manifest.jsonld JSONLD_BENCHMARK=1 npm test +EARL reports with benchmark data can be generated with an optional environment +tag: + + JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld EARL_ENV='CPU=Intel-i7-4790K@4.00GHz,Node.js=v10.16.3,jsonld.js=v1.7.0' npm test + +These reports can be compared at the [JSON-LD Benchmarks][] site. + [Digital Bazaar]: https://digitalbazaar.com/ [JSON-LD 1.0 API]: http://www.w3.org/TR/2014/REC-json-ld-api-20140116/ @@ -477,6 +484,7 @@ Use a command line with a test suite and a benchmark flag: [JSON-LD WG Framing latest]: https://w3c.github.io/json-ld-framing/ [JSON-LD WG latest]: https://w3c.github.io/json-ld-syntax/ +[JSON-LD Benchmarks]: https://json-ld.org/benchmarks/ [JSON-LD Processor Conformance]: https://w3c.github.io/json-ld-api/reports [JSON-LD WG]: https://www.w3.org/2018/json-ld-wg/ [JSON-LD]: https://json-ld.org/ From e97c937d909e6e98f1c57676edcfc977c550b36f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 7 Sep 2019 20:58:14 -0400 Subject: [PATCH 022/181] Add EARL_ENV env var. --- karma.conf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/karma.conf.js b/karma.conf.js index 46135406..2a133dd8 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -67,6 +67,7 @@ module.exports = function(config) { new webpack.DefinePlugin({ 'process.env.BAIL': JSON.stringify(process.env.BAIL), 'process.env.EARL': JSON.stringify(process.env.EARL), + 'process.env.EARL_ENV': JSON.stringify(process.env.EARL_ENV), 'process.env.JSONLD_BENCHMARK': JSON.stringify(process.env.JSONLD_BENCHMARK), 'process.env.JSONLD_TESTS': JSON.stringify(process.env.JSONLD_TESTS), From f4853b3d8533c0e9bcf1e6c1cfbe83ee31525ab0 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 7 Sep 2019 20:58:50 -0400 Subject: [PATCH 023/181] Add local benchmark base. --- tests/test-common.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test-common.js b/tests/test-common.js index ca885549..cbfd7db0 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -943,6 +943,7 @@ function createDocumentLoader(test) { const localBases = [ 'http://json-ld.org/test-suite', 'https://json-ld.org/test-suite', + 'https://json-ld.org/benchmarks', 'https://w3c.github.io/json-ld-api/tests', 'https://w3c.github.io/json-ld-framing/tests' ]; From 42f965d90cf6ffc555d839e612e4e0ecb12903cb Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 10 Sep 2019 13:48:00 -0400 Subject: [PATCH 024/181] Support benchmarks in Karma tests. --- CHANGELOG.md | 2 ++ karma.conf.js | 4 ++++ tests/test-common.js | 6 +++--- tests/test-karma.js | 16 ++++++++++++---- tests/test.js | 10 ++++++---- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d037c30..b60a5b22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # jsonld ChangeLog +- Support benchmarks in Karma tests. + ## 5.2.0 - 2021-04-07 ### Changed diff --git a/karma.conf.js b/karma.conf.js index 2a133dd8..4a865cbd 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -120,6 +120,10 @@ module.exports = function(config) { } } } + ], + noParse: [ + // avoid munging internal benchmark script magic + /benchmark/ ] }, node: { diff --git a/tests/test-common.js b/tests/test-common.js index cbfd7db0..5ba8e8e0 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -3,7 +3,6 @@ */ /* eslint-disable indent */ const EarlReport = require('./earl-report'); -const benchmark = require('benchmark'); const join = require('join-path-js'); const rdfCanonize = require('rdf-canonize'); const {prependBase} = require('../lib/url'); @@ -13,6 +12,7 @@ module.exports = function(options) { 'use strict'; const assert = options.assert; +const benchmark = options.benchmark; const jsonld = options.jsonld; const manifest = options.manifest || { @@ -573,8 +573,8 @@ function addTest(manifest, test, tests) { throw Error('Unknown test type: ' + test.type); } - let benchResult = null - if(options.benchmark) { + let benchResult = null; + if(options.benchmarkOptions) { const result = await runBenchmark({ test, fn, diff --git a/tests/test-karma.js b/tests/test-karma.js index 9e4f5853..4304cd41 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -35,6 +35,13 @@ const server = require('karma-server-side'); const webidl = require('./test-webidl'); const join = require('join-path-js'); +// special benchmark setup +const _ = require('lodash'); +const _process = require('process'); +const benchmark = require('benchmark'); +const Benchmark = benchmark.runInContext({_, _process}); +window.Benchmark = Benchmark; + const entries = []; if(process.env.JSONLD_TESTS) { @@ -75,13 +82,13 @@ if(process.env.JSONLD_TESTS) { entries.push(webidl); } -let benchmark = null; +let benchmarkOptions = null; if(process.env.JSONLD_BENCHMARK) { - benchmark = {}; + benchmarkOptions = {}; if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { const kv = pair.split('='); - benchmark[kv[0]] = kv[1]; + benchmarkOptions[kv[0]] = kv[1]; }); } } @@ -89,6 +96,7 @@ if(process.env.JSONLD_BENCHMARK) { const options = { nodejs: false, assert, + benchmark, jsonld, /* eslint-disable-next-line no-unused-vars */ exit: code => { @@ -103,7 +111,7 @@ const options = { verboseSkip: process.env.VERBOSE_SKIP === 'true', bailOnError: process.env.BAIL === 'true', entries, - benchmark, + benchmarkOptions, readFile: filename => { return server.run(filename, function(filename) { const fs = serverRequire('fs-extra'); diff --git a/tests/test.js b/tests/test.js index 5167b817..78a91d5d 100644 --- a/tests/test.js +++ b/tests/test.js @@ -25,6 +25,7 @@ * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. */ const assert = require('chai').assert; +const benchmark = require('benchmark'); const common = require('./test-common'); const fs = require('fs-extra'); const jsonld = require('..'); @@ -84,13 +85,13 @@ if(process.env.JSONLD_TESTS) { entries.push(path.resolve(_top, 'tests/node-document-loader-tests.js')); } -let benchmark = null; +let benchmarkOptions = null; if(process.env.JSONLD_BENCHMARK) { - benchmark = {}; + benchmarkOptions = {}; if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { const kv = pair.split('='); - benchmark[kv[0]] = kv[1]; + benchmarkOptions[kv[0]] = kv[1]; }); } } @@ -100,6 +101,7 @@ const options = { path }, assert, + benchmark, jsonld, exit: code => process.exit(code), earl: { @@ -110,7 +112,7 @@ const options = { verboseSkip: process.env.VERBOSE_SKIP === 'true', bailOnError: process.env.BAIL === 'true', entries, - benchmark, + benchmarkOptions, readFile: filename => { return fs.readFile(filename, 'utf8'); }, From 4b5b3190fd6a98267eea91de286b0118e298c0b0 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 17:52:41 -0400 Subject: [PATCH 025/181] Change EARL Assertor to Digital Bazaar, Inc. --- CHANGELOG.md | 4 ++++ tests/earl-report.js | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b60a5b22..a1770bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ # jsonld ChangeLog +### Added - Support benchmarks in Karma tests. +### Changed +- Change EARL Assertor to Digital Bazaar, Inc. + ## 5.2.0 - 2021-04-07 ### Changed diff --git a/tests/earl-report.js b/tests/earl-report.js index 3a2feec9..95c75e95 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -3,7 +3,7 @@ * * @author Dave Longley * - * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. */ /** @@ -58,15 +58,15 @@ function EarlReport(options) { 'https://github.com/digitalbazaar/jsonld.js/blob/master/LICENSE', 'doap:description': 'A JSON-LD processor for JavaScript', 'doap:programming-language': 'JavaScript', - 'dc:creator': 'https://github.com/dlongley', + 'dc:creator': 'https://github.com/digitalbazaar', 'doap:developer': { - '@id': 'https://github.com/dlongley', + '@id': 'https://github.com/digitalbazaar', '@type': [ - 'foaf:Person', + 'foaf:Organization', 'earl:Assertor' ], - 'foaf:name': 'Dave Longley', - 'foaf:homepage': 'https://github.com/dlongley' + 'foaf:name': 'Digital Bazaar, Inc.', + 'foaf:homepage': 'https://github.com/digitalbazaar' }, 'doap:release': { 'doap:name': '', From c65f5e8f62174051e6657202f4f94d4e8aa5a513 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 17:52:52 -0400 Subject: [PATCH 026/181] Fix typo. --- tests/earl-report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/earl-report.js b/tests/earl-report.js index 95c75e95..0f387693 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -94,7 +94,7 @@ EarlReport.prototype.addAssertion = function(test, pass, options) { 'earl:result': { '@type': 'earl:TestResult', 'dc:date': this.now.toISOString(), - 'earl:outcome': pass ? 'earl:passed' : 'earl:failed' + 'earl:outcome': pass ? 'earl:passed' : 'earl:failed', ...options.extra } }); From 110523dc0b9c9652062d18d44766aa7e18343ba4 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 22:31:58 -0400 Subject: [PATCH 027/181] Use digitalbazaar.com homepage. --- tests/earl-report.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/earl-report.js b/tests/earl-report.js index 0f387693..8376e5ba 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -58,15 +58,15 @@ function EarlReport(options) { 'https://github.com/digitalbazaar/jsonld.js/blob/master/LICENSE', 'doap:description': 'A JSON-LD processor for JavaScript', 'doap:programming-language': 'JavaScript', - 'dc:creator': 'https://github.com/digitalbazaar', + 'dc:creator': 'https://digitalbazaar.com/', 'doap:developer': { - '@id': 'https://github.com/digitalbazaar', + '@id': 'https://digitalbazaar.com/', '@type': [ 'foaf:Organization', 'earl:Assertor' ], 'foaf:name': 'Digital Bazaar, Inc.', - 'foaf:homepage': 'https://github.com/digitalbazaar' + 'foaf:homepage': 'https://digitalbazaar.com/' }, 'doap:release': { 'doap:name': '', From d4d9ba36f4b0b3b52c98927f066897b72ea83990 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 23:14:17 -0400 Subject: [PATCH 028/181] Benchmarking updates. - Support test environment in EARL output. - Support benchmark output in EARL output. - Add extra test files to karma setup. --- CHANGELOG.md | 2 + README.md | 9 ++-- karma.conf.js | 21 +++++++-- tests/earl-report.js | 108 ++++++++++++++++++++++++++++++++++++++----- tests/test-common.js | 17 ++++--- tests/test-karma.js | 97 +++++++++++++++++++++++++++++++++----- tests/test.js | 96 +++++++++++++++++++++++++++++++++----- 7 files changed, 303 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1770bfd..2558daac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added - Support benchmarks in Karma tests. +- Support test environment in EARL output. +- Support benchmark output in EARL output. ### Changed - Change EARL Assertor to Digital Bazaar, Inc. diff --git a/README.md b/README.md index 02be20c0..c51c0131 100644 --- a/README.md +++ b/README.md @@ -456,11 +456,14 @@ Use a command line with a test suite and a benchmark flag: JSONLD_TESTS=/tmp/benchmark-manifest.jsonld JSONLD_BENCHMARK=1 npm test EARL reports with benchmark data can be generated with an optional environment -tag: +details: - JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld EARL_ENV='CPU=Intel-i7-4790K@4.00GHz,Node.js=v10.16.3,jsonld.js=v1.7.0' npm test + JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test -These reports can be compared at the [JSON-LD Benchmarks][] site. +See `tests/test.js` for more `TEST_ENV` control and options. + +These reports can be compared with the `tests/benchmark-compare` tool and at +the [JSON-LD Benchmarks][] site. [Digital Bazaar]: https://digitalbazaar.com/ diff --git a/karma.conf.js b/karma.conf.js index 4a865cbd..bd8d5c4f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -13,6 +13,7 @@ * * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. */ +const os = require('os'); const webpack = require('webpack'); module.exports = function(config) { @@ -67,12 +68,19 @@ module.exports = function(config) { new webpack.DefinePlugin({ 'process.env.BAIL': JSON.stringify(process.env.BAIL), 'process.env.EARL': JSON.stringify(process.env.EARL), - 'process.env.EARL_ENV': JSON.stringify(process.env.EARL_ENV), + 'process.env.TEST_ENV': JSON.stringify(process.env.TEST_ENV), 'process.env.JSONLD_BENCHMARK': JSON.stringify(process.env.JSONLD_BENCHMARK), 'process.env.JSONLD_TESTS': JSON.stringify(process.env.JSONLD_TESTS), 'process.env.TEST_ROOT_DIR': JSON.stringify(__dirname), - 'process.env.VERBOSE_SKIP': JSON.stringify(process.env.VERBOSE_SKIP) + 'process.env.VERBOSE_SKIP': JSON.stringify(process.env.VERBOSE_SKIP), + // for 'auto' test env + 'process.env._TEST_ENV_ARCH': JSON.stringify(process.arch), + 'process.env._TEST_ENV_CPU': JSON.stringify(os.cpus()[0].model), + 'process.env._TEST_ENV_CPU_COUNT': JSON.stringify(os.cpus().length), + 'process.env._TEST_ENV_PLATFORM': JSON.stringify(process.platform), + 'process.env._TEST_VERSION': + JSON.stringify(require('./package.json').version) }) ], module: { @@ -142,10 +150,17 @@ module.exports = function(config) { 'envify', { BAIL: process.env.BAIL, EARL: process.env.EARL, + TEST_ENV: process.env.TEST_ENV, JSONLD_BENCHMARK: process.env.JSONLD_BENCHMARK, JSONLD_TESTS: process.env.JSONLD_TESTS, TEST_ROOT_DIR: __dirname, - VERBOSE_SKIP: process.env.VERBOSE_SKIP + VERBOSE_SKIP: process.env.VERBOSE_SKIP, + // for 'auto' test env + _TEST_ENV_ARCH: process.arch, + _TEST_ENV_CPU: os.cpus()[0].model, + _TEST_ENV_CPU_COUNT: os.cpus().length, + _TEST_ENV_PLATFORM: process.platform, + _TEST_VERSION: require('./package.json').version } ] ], diff --git a/tests/earl-report.js b/tests/earl-report.js index 8376e5ba..0edd1aff 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -11,7 +11,7 @@ * * @param options {Object} reporter options * id: {String} report id - * env: {String} environment description + * env: {Object} environment description */ function EarlReport(options) { let today = new Date(); @@ -24,6 +24,8 @@ function EarlReport(options) { this.now.setMilliseconds(0); this.id = options.id; this.env = options.env; + // test environment + this._environment = null; /* eslint-disable quote-props */ this._report = { '@context': { @@ -69,24 +71,20 @@ function EarlReport(options) { 'foaf:homepage': 'https://digitalbazaar.com/' }, 'doap:release': { - 'doap:name': '', 'doap:revision': '', 'doap:created': today }, 'subjectOf': [] }; /* eslint-enable quote-props */ - this._report['@id'] += - '#' + this.id; - this._report['doap:name'] += - ' ' + this.id + (this.env ? ' ' + this.env : ''); - this._report['dc:title'] += - ' ' + this.id + (this.env ? ' ' + this.env : ''); + if(this.env && this.env.version) { + this._report['doap:release']['doap:name'] = this.env.version; + } } EarlReport.prototype.addAssertion = function(test, pass, options) { options = options || {}; - this._report.subjectOf.push({ + const assertion = { '@type': 'earl:Assertion', 'earl:assertedBy': this._report['doap:developer']['@id'], 'earl:mode': 'earl:automatic', @@ -94,10 +92,19 @@ EarlReport.prototype.addAssertion = function(test, pass, options) { 'earl:result': { '@type': 'earl:TestResult', 'dc:date': this.now.toISOString(), - 'earl:outcome': pass ? 'earl:passed' : 'earl:failed', - ...options.extra + 'earl:outcome': pass ? 'earl:passed' : 'earl:failed' } - }); + }; + if(options.benchmarkResult) { + const result = { + ...options.benchmarkResult + }; + if(this._environment) { + result['jldb:environment'] = this._environment['@id']; + } + assertion['jldb:result'] = result; + } + this._report.subjectOf.push(assertion); return this; }; @@ -109,4 +116,81 @@ EarlReport.prototype.reportJson = function() { return JSON.stringify(this._report, null, 2); }; +/* eslint-disable quote-props */ +const _benchmarkContext = { + 'jldb': 'http://json-ld.org/benchmarks/vocab#', + 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + + // environment description + 'jldb:Environment': {'@type': '@id'}, + + // per environment + // architecture type + // ex: x86 + 'jldb:arch': {'@type': 'xsd:string'}, + // cpu model description (may show multiple cpus) + // ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' + 'jldb:cpu': {'@type': 'xsd:string'}, + // count of cpus, may not be uniform, just informative + 'jldb:cpuCount': {'@type': 'xsd:integer'}, + // platform name + // ex: linux + 'jldb:platform': {'@type': 'xsd:string'}, + // runtime name + // ex: Node.js, Chromium, Ruby + 'jldb:runtime': {'@type': 'xsd:string'}, + // runtime version + // ex: v14.19.0 + 'jldb:runtimeVersion': {'@type': 'xsd:string'}, + // arbitrary comment + 'jldb:comment': 'rdfs:comment', + + // benchmark result + 'jldb:BenchmarkResult': {'@type': '@id'}, + + // use in earl:Assertion, type jldb:BenchmarkResult + 'jldb:result': {'@type': '@id'}, + + // per BenchmarkResult + 'jldb:environment': {'@type': '@id'}, + 'jldb:hz': {'@type': 'xsd:float'}, + 'jldb:rme': {'@type': 'xsd:float'} +}; +/* eslint-enable quote-props */ + +// setup @context and environment to handle benchmark data +EarlReport.prototype.setupForBenchmarks = function(options) { + // add context if needed + if(!Array.isArray(this._report['@context'])) { + this._report['@context'] = [this._report['@context']]; + } + if(!this._report['@context'].some(c => c === _benchmarkContext)) { + this._report['@context'].push(_benchmarkContext); + } + if(options.testEnv) { + // add report environment + const fields = [ + ['arch', 'jldb:arch'], + ['cpu', 'jldb:cpu'], + ['cpuCount', 'jldb:cpuCount'], + ['platform', 'jldb:platform'], + ['runtime', 'jldb:runtime'], + ['runtimeVersion', 'jldb:runtimeVersion'], + ['comment', 'jldb:comment'] + ]; + const _env = { + '@id': '_:environment:0' + }; + for(const [field, property] of fields) { + if(options.testEnv[field]) { + _env[property] = options.testEnv[field]; + } + } + this._environment = _env; + this._report['@included'] = this._report['@included'] || []; + this._report['@included'].push(_env); + } +}; + module.exports = EarlReport; diff --git a/tests/test-common.js b/tests/test-common.js index 5ba8e8e0..46a56e45 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -266,8 +266,11 @@ const SKIP_TESTS = []; if(options.earl && options.earl.filename) { options.earl.report = new EarlReport({ id: options.earl.id, - env: options.earl.env + env: options.testEnv }); + if(options.benchmarkOptions) { + options.earl.report.setupForBenchmarks({testEnv: options.testEnv}); + } } return new Promise(resolve => { @@ -573,7 +576,7 @@ function addTest(manifest, test, tests) { throw Error('Unknown test type: ' + test.type); } - let benchResult = null; + let benchmarkResult = null; if(options.benchmarkOptions) { const result = await runBenchmark({ test, @@ -584,16 +587,16 @@ function addTest(manifest, test, tests) { })), mochaTest: self }); - benchResult = { - 'jsonld:benchmarkHz': result.target.hz + benchmarkResult = { + '@type': 'jldb:BenchmarkResult', + 'jldb:hz': result.target.hz, + 'jldb:rme': result.target.stats.rme }; } if(options.earl.report) { options.earl.report.addAssertion(test, true, { - extra: { - ...benchResult - } + benchmarkResult }); } } catch(err) { diff --git a/tests/test-karma.js b/tests/test-karma.js index 4304cd41..8039fcbe 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -7,8 +7,25 @@ * JSONLD_TESTS="r1 r2 ..." * Output an EARL report: * EARL=filename - * Output EARL environment description (any appropraite string): - * EARL_ENV='CPU=Intel-i7-4790K@4.00GHz,Node.js=v10.16.3,jsonld.js=v1.7.0' + * Test environment details for EARL report: + * This is useful for benchmark comparison. + * By default no details are added for privacy reasons. + * Automatic details can be added for all fields with '1', 'true', or 'auto': + * TEST_ENV=1 + * To include only certain fields, set them, or use 'auto': + * TEST_ENV=cpu='Intel i7-4790K @ 4.00GHz',runtime='Node.js',... + * TEST_ENV=cpu=auto # only cpu + * TEST_ENV=cpu,runtime # only cpu and runtime + * TEST_ENV=auto,comment='special test' # all auto with override + * Available fields: + * - arch - ex: 'x64' + * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' + * - cpuCount - ex: 8 + * - platform - ex: 'linux' + * - runtime - ex: 'Node.js' + * - runtimeVersion - ex: 'v14.19.0' + * - comment: any text + * - version: jsonld.js version * Bail with tests fail: * BAIL=true * Verbose skip reasons: @@ -22,7 +39,7 @@ * @author Dave Longley * @author David I. Lehn * - * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. */ /* global serverRequire */ // FIXME: hack to ensure delay is set first @@ -76,20 +93,79 @@ if(process.env.JSONLD_TESTS) { entries.push(join(_top, '../normalization/tests')); // other tests + entries.push(join(_top, 'tests/misc.js')); + entries.push(join(_top, 'tests/graph-container.js')); entries.push(join(_top, 'tests/new-embed-api')); // WebIDL tests entries.push(webidl); } +// test environment +let testEnv = null; +if(process.env.TEST_ENV) { + let _test_env = process.env.TEST_ENV; + if(!(['0', 'false'].includes(_test_env))) { + testEnv = {}; + if(['1', 'true', 'auto'].includes(_test_env)) { + _test_env = 'auto'; + } + _test_env.split(',').forEach(pair => { + if(pair === 'auto') { + testEnv.arch = 'auto'; + testEnv.cpu = 'auto'; + testEnv.cpuCount = 'auto'; + testEnv.platform = 'auto'; + testEnv.runtime = 'auto'; + testEnv.runtimeVersion = 'auto'; + testEnv.comment = 'auto'; + testEnv.version = 'auto'; + } else { + const kv = pair.split('='); + if(kv.length === 1) { + testEnv[kv[0]] = 'auto'; + } else { + testEnv[kv[0]] = kv.slice(1).join('='); + } + } + }); + if(testEnv.arch === 'auto') { + testEnv.arch = process.env._TEST_ENV_ARCH; + } + if(testEnv.cpu === 'auto') { + testEnv.cpu = process.env._TEST_ENV_CPU; + } + if(testEnv.cpuCount === 'auto') { + testEnv.cpuCount = process.env._TEST_ENV_CPU_COUNT; + } + if(testEnv.platform === 'auto') { + testEnv.platform = process.env._TEST_ENV_PLATFORM; + } + if(testEnv.runtime === 'auto') { + testEnv.runtime = 'browser'; + } + if(testEnv.runtimeVersion === 'auto') { + testEnv.runtimeVersion = '(unknown)'; + } + if(testEnv.comment === 'auto') { + testEnv.comment = ''; + } + if(testEnv.version === 'auto') { + testEnv.version = require('../package.json').version; + } + } +} + let benchmarkOptions = null; if(process.env.JSONLD_BENCHMARK) { - benchmarkOptions = {}; - if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { - process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { - const kv = pair.split('='); - benchmarkOptions[kv[0]] = kv[1]; - }); + if(!(['0', 'false'].includes(process.env.JSONLD_BENCHMARK))) { + benchmarkOptions = {}; + if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { + process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { + const kv = pair.split('='); + benchmarkOptions[kv[0]] = kv[1]; + }); + } } } @@ -104,13 +180,12 @@ const options = { throw new Error('exit not implemented'); }, earl: { - id: 'browser', - env: process.env.EARL_ENV, filename: process.env.EARL }, verboseSkip: process.env.VERBOSE_SKIP === 'true', bailOnError: process.env.BAIL === 'true', entries, + testEnv, benchmarkOptions, readFile: filename => { return server.run(filename, function(filename) { diff --git a/tests/test.js b/tests/test.js index 78a91d5d..e0a552b0 100644 --- a/tests/test.js +++ b/tests/test.js @@ -7,8 +7,25 @@ * JSONLD_TESTS="r1 r2 ..." * Output an EARL report: * EARL=filename - * Output EARL environment description (any appropraite string): - * EARL_ENV='CPU=Intel-i7-4790K@4.00GHz,Node.js=v10.16.3,jsonld.js=v1.7.0' + * Test environment details for EARL report: + * This is useful for benchmark comparison. + * By default no details are added for privacy reasons. + * Automatic details can be added for all fields with '1', 'true', or 'auto': + * TEST_ENV=1 + * To include only certain fields, set them, or use 'auto': + * TEST_ENV=cpu='Intel i7-4790K @ 4.00GHz',runtime='Node.js',... + * TEST_ENV=cpu=auto # only cpu + * TEST_ENV=cpu,runtime # only cpu and runtime + * TEST_ENV=auto,comment='special test' # all auto with override + * Available fields: + * - arch - ex: 'x64' + * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' + * - cpuCount - ex: 8 + * - platform - ex: 'linux' + * - runtime - ex: 'Node.js' + * - runtimeVersion - ex: 'v14.19.0' + * - comment: any text + * - version: jsonld.js version * Bail with tests fail: * BAIL=true * Verbose skip reasons: @@ -22,13 +39,14 @@ * @author Dave Longley * @author David I. Lehn * - * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. */ const assert = require('chai').assert; const benchmark = require('benchmark'); const common = require('./test-common'); const fs = require('fs-extra'); const jsonld = require('..'); +const os = require('os'); const path = require('path'); const entries = []; @@ -85,14 +103,71 @@ if(process.env.JSONLD_TESTS) { entries.push(path.resolve(_top, 'tests/node-document-loader-tests.js')); } +// test environment +let testEnv = null; +if(process.env.TEST_ENV) { + let _test_env = process.env.TEST_ENV; + if(!(['0', 'false'].includes(_test_env))) { + testEnv = {}; + if(['1', 'true', 'auto'].includes(_test_env)) { + _test_env = 'auto'; + } + _test_env.split(',').forEach(pair => { + if(pair === 'auto') { + testEnv.arch = 'auto'; + testEnv.cpu = 'auto'; + testEnv.cpuCount = 'auto'; + testEnv.platform = 'auto'; + testEnv.runtime = 'auto'; + testEnv.runtimeVersion = 'auto'; + testEnv.comment = 'auto'; + testEnv.version = 'auto'; + } else { + const kv = pair.split('='); + if(kv.length === 1) { + testEnv[kv[0]] = 'auto'; + } else { + testEnv[kv[0]] = kv.slice(1).join('='); + } + } + }); + if(testEnv.arch === 'auto') { + testEnv.arch = process.arch; + } + if(testEnv.cpu === 'auto') { + testEnv.cpu = os.cpus()[0].model; + } + if(testEnv.cpuCount === 'auto') { + testEnv.cpuCount = os.cpus().length; + } + if(testEnv.platform === 'auto') { + testEnv.platform = process.platform; + } + if(testEnv.runtime === 'auto') { + testEnv.runtime = 'Node.js'; + } + if(testEnv.runtimeVersion === 'auto') { + testEnv.runtimeVersion = process.version; + } + if(testEnv.comment === 'auto') { + testEnv.comment = ''; + } + if(testEnv.version === 'auto') { + testEnv.version = require('../package.json').version; + } + } +} + let benchmarkOptions = null; if(process.env.JSONLD_BENCHMARK) { - benchmarkOptions = {}; - if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { - process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { - const kv = pair.split('='); - benchmarkOptions[kv[0]] = kv[1]; - }); + if(!(['0', 'false'].includes(process.env.JSONLD_BENCHMARK))) { + benchmarkOptions = {}; + if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { + process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { + const kv = pair.split('='); + benchmarkOptions[kv[0]] = kv[1]; + }); + } } } @@ -105,13 +180,12 @@ const options = { jsonld, exit: code => process.exit(code), earl: { - id: 'Node.js', - env: process.env.EARL_ENV, filename: process.env.EARL }, verboseSkip: process.env.VERBOSE_SKIP === 'true', bailOnError: process.env.BAIL === 'true', entries, + testEnv, benchmarkOptions, readFile: filename => { return fs.readFile(filename, 'utf8'); From 02c83e38274848838c075f0cbc83a6ad7ff4ed04 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 23:15:44 -0400 Subject: [PATCH 029/181] Output EARL when tests use "only". --- tests/test-common.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test-common.js b/tests/test-common.js index 46a56e45..8d86fdbe 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -286,11 +286,13 @@ const _tests = []; return addManifest(manifest, _tests) .then(() => { - _testsToMocha(_tests); - }).then(() => { + return _testsToMocha(_tests); + }).then(result => { if(options.earl.report) { describe('Writing EARL report to: ' + options.earl.filename, function() { - it('should print the earl report', function() { + // print out EARL even if .only was used + const _it = result.hadOnly ? it.only : it; + _it('should print the earl report', function() { return options.writeFile( options.earl.filename, options.earl.report.reportJson()); }); @@ -300,6 +302,7 @@ return addManifest(manifest, _tests) // build mocha tests from local test structure function _testsToMocha(tests) { + let hadOnly = false; tests.forEach(suite => { if(suite.skip) { describe.skip(suite.title); @@ -308,17 +311,22 @@ function _testsToMocha(tests) { describe(suite.title, () => { suite.tests.forEach(test => { if(test.only) { + hadOnly = true; it.only(test.title, test.f); return; } it(test.title, test.f); }); - _testsToMocha(suite.suites); + const {hadOnly: _hadOnly} = _testsToMocha(suite.suites); + hadOnly = hadOnly || _hadOnly; }); suite.imports.forEach(f => { options.import(f); }); }); + return { + hadOnly + }; } }); From 52bc462c291ba2edbb8bd48b2c4cee75605bb006 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 22 Mar 2022 00:28:58 -0400 Subject: [PATCH 030/181] Change karma benchmark process call. - Using 'process' is difficult due to how webpack and browserify handle it. - This doesn't seem to be needed now for at least basic functionality. --- tests/test-karma.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test-karma.js b/tests/test-karma.js index 8039fcbe..b9f71c8f 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -54,9 +54,10 @@ const join = require('join-path-js'); // special benchmark setup const _ = require('lodash'); -const _process = require('process'); +//const _process = require('process'); const benchmark = require('benchmark'); -const Benchmark = benchmark.runInContext({_, _process}); +//const Benchmark = benchmark.runInContext({_, _process}); +const Benchmark = benchmark.runInContext({_}); window.Benchmark = Benchmark; const entries = []; From 6bc633f7be47bda97bc80358d14d23da19e5c5ec Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 22 Mar 2022 01:03:56 -0400 Subject: [PATCH 031/181] Fix typo. --- tests/earl-report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/earl-report.js b/tests/earl-report.js index 0edd1aff..a5413cd1 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -78,7 +78,7 @@ function EarlReport(options) { }; /* eslint-enable quote-props */ if(this.env && this.env.version) { - this._report['doap:release']['doap:name'] = this.env.version; + this._report['doap:release']['doap:revision'] = this.env.version; } } From f8b9aa64f6fc6e78e761ffc3aa75990ad175f3bc Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 23 Mar 2022 17:02:17 -0400 Subject: [PATCH 032/181] Add benchmark env 'label' support. --- tests/earl-report.js | 4 ++++ tests/test.js | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/tests/earl-report.js b/tests/earl-report.js index a5413cd1..e5545acd 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -126,6 +126,9 @@ const _benchmarkContext = { 'jldb:Environment': {'@type': '@id'}, // per environment + // label + // ex: 'Setup 1' (for reports) + 'jldb:label': {'@type': 'xsd:string'}, // architecture type // ex: x86 'jldb:arch': {'@type': 'xsd:string'}, @@ -171,6 +174,7 @@ EarlReport.prototype.setupForBenchmarks = function(options) { if(options.testEnv) { // add report environment const fields = [ + ['label', 'jldb:label'], ['arch', 'jldb:arch'], ['cpu', 'jldb:cpu'], ['cpuCount', 'jldb:cpuCount'], diff --git a/tests/test.js b/tests/test.js index e0a552b0..d5f482f5 100644 --- a/tests/test.js +++ b/tests/test.js @@ -18,6 +18,7 @@ * TEST_ENV=cpu,runtime # only cpu and runtime * TEST_ENV=auto,comment='special test' # all auto with override * Available fields: + * - label - ex: 'Setup 1' (short label for reports) * - arch - ex: 'x64' * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' * - cpuCount - ex: 8 @@ -114,6 +115,7 @@ if(process.env.TEST_ENV) { } _test_env.split(',').forEach(pair => { if(pair === 'auto') { + testEnv.name = 'auto'; testEnv.arch = 'auto'; testEnv.cpu = 'auto'; testEnv.cpuCount = 'auto'; @@ -131,6 +133,9 @@ if(process.env.TEST_ENV) { } } }); + if(testEnv.label === 'auto') { + testEnv.label = ''; + } if(testEnv.arch === 'auto') { testEnv.arch = process.arch; } From b7bc6d67e7e9afc5eaf9774716acc19240de778c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 23 Mar 2022 20:45:14 -0400 Subject: [PATCH 033/181] Add benchmark comparison tool. - Currently supports markdown table output. - Can output environment. - Can output ops/s or ops/s with relative differences. --- CHANGELOG.md | 1 + README.md | 4 +- benchmarks/compare/.eslintrc.js | 9 ++ benchmarks/compare/compare.js | 160 ++++++++++++++++++++++++++++++++ benchmarks/compare/package.json | 51 ++++++++++ 5 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 benchmarks/compare/.eslintrc.js create mode 100755 benchmarks/compare/compare.js create mode 100644 benchmarks/compare/package.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2558daac..66c7b4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Support benchmarks in Karma tests. - Support test environment in EARL output. - Support benchmark output in EARL output. +- Benchmark comparison tool. ### Changed - Change EARL Assertor to Digital Bazaar, Inc. diff --git a/README.md b/README.md index c51c0131..ca71d8e0 100644 --- a/README.md +++ b/README.md @@ -462,8 +462,8 @@ details: See `tests/test.js` for more `TEST_ENV` control and options. -These reports can be compared with the `tests/benchmark-compare` tool and at -the [JSON-LD Benchmarks][] site. +These reports can be compared with the `benchmarks/compare/` tool and at the +[JSON-LD Benchmarks][] site. [Digital Bazaar]: https://digitalbazaar.com/ diff --git a/benchmarks/compare/.eslintrc.js b/benchmarks/compare/.eslintrc.js new file mode 100644 index 00000000..afb2fa21 --- /dev/null +++ b/benchmarks/compare/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { + commonjs: true, + node: true, + es2020: true + }, + extends: 'eslint-config-digitalbazaar', + root: true +}; diff --git a/benchmarks/compare/compare.js b/benchmarks/compare/compare.js new file mode 100755 index 00000000..084da43a --- /dev/null +++ b/benchmarks/compare/compare.js @@ -0,0 +1,160 @@ +#!/usr/bin/env node + +import yargs from 'yargs'; +import {hideBin} from 'yargs/helpers'; +import {promises as fs} from 'node:fs'; +import {table} from 'table'; +import {markdownTable} from 'markdown-table'; +import commonPathPrefix from 'common-path-prefix'; + +yargs(hideBin(process.argv)) + .alias('h', 'help') + .option('verbose', { + alias: 'v', + type: 'count', + description: 'Run with verbose logging' + }) + .option('relative', { + alias: 'r', + type: 'boolean', + default: false, + description: 'Show % relative difference' + }) + .option('format', { + alias: 'f', + choices: ['markdown'], + default: 'markdown', + description: 'Output format' + }) + .option('env', { + alias: 'e', + choices: ['none', 'all', 'combined'], + default: 'none', + description: 'Output environment format' + }) + .command( + '$0 ', + 'compare JSON-LD benchmark files', () => {}, + async (argv) => { + return compare(argv); + }) + .parse(); + +async function compare({ + env, + file, + format, + relative, + verbose +}) { + const contents = await Promise.all(file.map(async f => ({ + fn: f, + content: await fs.readFile(f, 'utf8') + }))); + const results = contents + .map(c => ({ + fn: c.fn, + content: JSON.parse(c.content), + // map of test id => assertion + testMap: new Map() + })) + .map(c => ({ + ...c, + // FIXME process properly + env: c.content['@included'][0], + label: c.content['@included'][0]['jldb:label'] + })); + //console.log(JSON.stringify(results, null, 2)); + // order of tests found in each result set + // TODO: provider interleaved mode for new results in + const seen = new Set(); + const ordered = []; + results.forEach(r => { + r.content.subjectOf.forEach(a => { + //console.log(a); + const t = a['earl:test']; + if(!seen.has(t)) { + ordered.push(t); + } + seen.add(t); + r.testMap.set(t, a); + }); + }); + //console.log(ordered); + const tprefixlen = commonPathPrefix(ordered).length; + function hz(a) { + return a['jldb:result']['jldb:hz']; + } + function rfmt(base, a) { + return relative ? (100*(a-base)/base) : a; + } + const compared = ordered.map(t => [ + t.slice(tprefixlen), + hz(results[0].testMap.get(t)).toFixed(2), + ...results.slice(1) + .map(r => rfmt( + hz(results[0].testMap.get(t)), + hz(r.testMap.get(t)))) + .map(d => relative ? d.toFixed(2) + '%' : d.toFixed(2)) + ]); + //console.log(compared); + //console.log(results); + const fnprefixlen = commonPathPrefix(file).length; + console.log('## Comparison'); + console.log(markdownTable([ + [ + 'Test', + ...results.map(r => r.label || r.fn.slice(fnprefixlen)) + ], + ...compared + ], { + align: [ + 'l', + ...results.map(r => 'r') + ] + })); + console.log(); + if(relative) { + console.log('> base ops/s and relative difference (higher is better)'); + } else { + console.log('> ops/s (higher is better)'); + } + + const envProps = [ + ['Label', 'jldb:label'], + ['Arch', 'jldb:arch'], + ['CPU', 'jldb:cpu'], + ['CPUs', 'jldb:cpuCount'], + ['Platform', 'jldb:platform'], + ['Runtime', 'jldb:runtime'], + ['Runtime Version', 'jldb:runtimeVersion'], + ['Comment', 'jldb:comment'] + ]; + + if(env === 'all') { + console.log(); + console.log('## Environment'); + console.log(markdownTable([ + envProps.map(p => p[0]), + ...results.map(r => envProps.map(p => r.env[p[1]] || '')) + ])); + } + + if(env === 'combined') { + console.log(); + console.log('## Environment'); + function envline(key, prop) { + const values = new Set(results + .map(r => r.env[prop]) + .filter(v => v !== undefined) + ); + return [key, values.size ? [...values].join(', ') : []]; + } + console.log(markdownTable([ + ['Key', 'Values'], + ...envProps + .map(p => envline(p[0], p[1])) + .filter(p => p[1].length) + ])); + } +} diff --git a/benchmarks/compare/package.json b/benchmarks/compare/package.json new file mode 100644 index 00000000..4aca2f07 --- /dev/null +++ b/benchmarks/compare/package.json @@ -0,0 +1,51 @@ +{ + "name": "jsonld-benchmarks-compare", + "version": "0.0.1-0", + "private": true, + "description": "A JSON-LD benchmark comparison tool.", + "homepage": "https://github.com/digitalbazaar/jsonld.js", + "author": { + "name": "Digital Bazaar, Inc.", + "email": "support@digitalbazaar.com", + "url": "https://digitalbazaar.com/" + }, + "contributors": [ + "Dave Longley ", + "David I. Lehn " + ], + "repository": { + "type": "git", + "url": "https://github.com/digitalbazaar/jsonld.js" + }, + "bugs": { + "url": "https://github.com/digitalbazaar/jsonld.js/issues", + "email": "support@digitalbazaar.com" + }, + "license": "BSD-3-Clause", + "type": "module", + "main": "compare.js", + "dependencies": { + "common-path-prefix": "^3.0.0", + "markdown-table": "^3.0.2", + "yargs": "^17.4.0" + }, + "devDependencies": { + "eslint": "^8.11.0", + "eslint-config-digitalbazaar": "^2.8.0" + }, + "engines": { + "node": ">=12" + }, + "keywords": [ + "JSON", + "JSON-LD", + "Linked Data", + "RDF", + "Semantic Web", + "jsonld", + "benchmark" + ], + "scripts": { + "lint": "eslint *.js" + } +} From 72f1a2181dfa18df8492961885ab718097730cbe Mon Sep 17 00:00:00 2001 From: Tobias Looker Date: Mon, 17 May 2021 16:49:49 +1200 Subject: [PATCH 034/181] fix: linting --- lib/util.js | 2 +- tests/test-common.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/util.js b/lib/util.js index 77da8f61..1458005a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -130,7 +130,7 @@ api.parseLinkHeader = header => { while((match = REGEX_LINK_HEADER_PARAMS.exec(params))) { result[match[1]] = (match[2] === undefined) ? match[3] : match[2]; } - const rel = result['rel'] || ''; + const rel = result.rel || ''; if(Array.isArray(rval[rel])) { rval[rel].push(result); } else if(rval.hasOwnProperty(rel)) { diff --git a/tests/test-common.js b/tests/test-common.js index b9982182..9cc520c7 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -398,7 +398,7 @@ function addManifest(manifest, parent) { */ function addTest(manifest, test, tests) { // expand @id and input base - const test_id = test['@id'] || test['id']; + const test_id = test['@id'] || test.id; //var number = test_id.substr(2); test['@id'] = manifest.baseIri + @@ -958,10 +958,10 @@ function createDocumentLoader(test) { } // If not JSON-LD, alternate may point there - if(linkHeaders['alternate'] && - linkHeaders['alternate'].type == 'application/ld+json' && + if(linkHeaders.alternate && + linkHeaders.alternate.type == 'application/ld+json' && !(contentType || '').match(/^application\/(\w*\+)?json$/)) { - doc.documentUrl = prependBase(url, linkHeaders['alternate'].target); + doc.documentUrl = prependBase(url, linkHeaders.alternate.target); } } } From 6de93df0416940137e04c000720e723b7b88a96e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 23:25:43 -0400 Subject: [PATCH 035/181] Skip failing compact tests. --- tests/test-common.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test-common.js b/tests/test-common.js index 9cc520c7..114e615e 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -35,6 +35,9 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ + /compact-manifest#t0111$/, + /compact-manifest#t0112$/, + /compact-manifest#t0113$/, // html /html-manifest#tc001$/, /html-manifest#tc002$/, From ed0ff48f7cc446f4672c85040cd0e90b024d9de8 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 21 Mar 2022 23:27:58 -0400 Subject: [PATCH 036/181] Test on Node.js 16.x. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7f20f42..27cb66e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [12.x, 14.x] + node-version: [12.x, 14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From d37c360b682bcdbf251602eb243b6cd88223086c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 3 May 2022 01:42:13 -0400 Subject: [PATCH 037/181] Update to `@digitalbazar/http-client@3`. - Pulls in newer `ky` and `ky-universal` that should address security alerts and provide other improvements. - Newer `ky` will throw errors on redirects even when in `manual` redirect mode. Now using `throwHttpErrors` option to turn off errors. - One of the updated dependencies can cause tests to rewrite redirect `Location` URLs to be relative references. The global `URL` interface is now used to rebuild a full URL for further redirect processing. - Newer `node-forge` will output a one-time warning if code even accesses `response.data`. `@digitalbazar/http-client` will forcefully set `data` *if* it detects a JSON content type. Calling code can't know if that happened, so currently needs to redo content detection to know if JSON `data` can be accessed. This is an issue for sites where JSON-LD was requested but the response is non-JSON with a `Link` header pointing to the JSON-LD. --- CHANGELOG.md | 8 ++++++++ lib/documentLoaders/node.js | 19 ++++++++++++++++--- package.json | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d037c30..fb3f23ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # jsonld ChangeLog +## 5.3.0 - 2022-xx-xx + +### Changed +- Update to `@digitalbazaar/http-client@3`: + - Pulls in newer `ky` and `ky-universal` that should address security alerts + and provide other improvements. + - Use global `URL` interface to handle relative redirects. + ## 5.2.0 - 2021-04-07 ### Changed diff --git a/lib/documentLoaders/node.js b/lib/documentLoaders/node.js index affd62a9..ac21034d 100644 --- a/lib/documentLoaders/node.js +++ b/lib/documentLoaders/node.js @@ -143,7 +143,9 @@ module.exports = ({ }); } redirects.push(url); - return loadDocument(location, redirects); + // location can be relative, turn into full url + const nextUrl = new URL(location, url).href; + return loadDocument(nextUrl, redirects); } // cache for each redirected URL @@ -163,7 +165,12 @@ module.exports = ({ async function _fetch({url, headers, strictSSL, httpAgent, httpsAgent}) { try { - const options = {headers, redirect: 'manual'}; + const options = { + headers, + redirect: 'manual', + // ky specific to avoid redirects throwing + throwHttpErrors: false + }; const isHttps = url.startsWith('https:'); if(isHttps) { options.agent = @@ -174,7 +181,13 @@ async function _fetch({url, headers, strictSSL, httpAgent, httpsAgent}) { } } const res = await httpClient.get(url, options); - return {res, body: res.data}; + // @digitalbazaar/http-client may use node-fetch, which can output + // a warning if response.data is accessed and no json was parsed. + // Used here is the same type detection logic so the data field is + // accessed only if the client likely tried to parse JSON. + const contentType = res.headers.get('content-type'); + const hasJson = contentType && contentType.includes('json'); + return {res, body: hasJson ? res.data : null}; } catch(e) { // HTTP errors have a response in them // ky considers redirects HTTP errors diff --git a/package.json b/package.json index e7baf6c4..a0593ab4 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "lib/**/*.js" ], "dependencies": { - "@digitalbazaar/http-client": "^1.1.0", + "@digitalbazaar/http-client": "^3.0.1", "canonicalize": "^1.0.1", "lru-cache": "^6.0.0", "rdf-canonize": "^3.0.0" From 3f66c02650303e508a2e0eff5dac597736f0c019 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 3 May 2022 18:10:32 -0400 Subject: [PATCH 038/181] Lint all files. - eslint file pattern was not catching subdirs properly. - Ignore webidl js files. - Fix lint issues. --- .eslintrc.js | 8 +++++++- lib/documentLoaders/node.js | 2 +- lib/documentLoaders/xhr.js | 2 +- package.json | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d85a8e5d..b70fd6f7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,5 +6,11 @@ module.exports = { es2020: true }, extends: 'eslint-config-digitalbazaar', - root: true + root: true, + ignorePatterns: [ + 'dist/', + 'tests/webidl/WebIDLParser.js', + 'tests/webidl/idlharness.js', + 'tests/webidl/testharness.js' + ] }; diff --git a/lib/documentLoaders/node.js b/lib/documentLoaders/node.js index ac21034d..568c67b4 100644 --- a/lib/documentLoaders/node.js +++ b/lib/documentLoaders/node.js @@ -110,7 +110,7 @@ module.exports = ({ } // "alternate" link header is a redirect - alternate = linkHeaders['alternate']; + alternate = linkHeaders.alternate; if(alternate && alternate.type == 'application/ld+json' && !(contentType || '') diff --git a/lib/documentLoaders/xhr.js b/lib/documentLoaders/xhr.js index 48849c71..80688817 100644 --- a/lib/documentLoaders/xhr.js +++ b/lib/documentLoaders/xhr.js @@ -90,7 +90,7 @@ module.exports = ({ } // "alternate" link header is a redirect - alternate = linkHeaders['alternate']; + alternate = linkHeaders.alternate; if(alternate && alternate.type == 'application/ld+json' && !(contentType || '').match(/^application\/(\w*\+)?json$/)) { diff --git a/package.json b/package.json index a0593ab4..85bce8b9 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text-summary npm test", "coverage-ci": "cross-env NODE_ENV=test nyc --reporter=lcovonly npm run test", "coverage-report": "nyc report", - "lint": "eslint *.js lib/**.js tests/**.js" + "lint": "eslint ." }, "nyc": { "exclude": [ From 4c1afa0c8d6aed56033424c76a9dd018f5d44e09 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 3 May 2022 18:17:39 -0400 Subject: [PATCH 039/181] Update dependencies and Node.js requirement. - **BREAKING**: Remove Node.js 12 testing and support due to `ky-universal` use of top-level `await`. The core code should still otherwise function with older Node.js releases in this release but a custom document loader may be needed. - Update to `@digitalbazaar/http-client@3.1.0`. - Remove code to deal with accessing `response.data`. --- .github/workflows/main.yml | 2 +- CHANGELOG.md | 9 ++++++++- lib/documentLoaders/node.js | 8 +------- package.json | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 27cb66e5..feca597a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [14.x, 16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3f23ae..63cb93c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,15 @@ # jsonld ChangeLog -## 5.3.0 - 2022-xx-xx +## 6.0.0 - 2022-xx-xx ### Changed +- **BREAKING**: Drop testing and support for Node.js 12.x. The majority of the + code will still run on Node.js 12.x. However, the + `@digitalbazaar/http-client@3` update uses a newer `ky-universal` which uses + a top-level `await` that is unsupported in older Node.js versions. That + causes the included `node` `documentLoader` to not function and tests to + fail. If you wish to still use earlier Node.js versions, you may still be + able to do so with your own custom `documentLoader`. - Update to `@digitalbazaar/http-client@3`: - Pulls in newer `ky` and `ky-universal` that should address security alerts and provide other improvements. diff --git a/lib/documentLoaders/node.js b/lib/documentLoaders/node.js index 568c67b4..9de3b7ba 100644 --- a/lib/documentLoaders/node.js +++ b/lib/documentLoaders/node.js @@ -181,13 +181,7 @@ async function _fetch({url, headers, strictSSL, httpAgent, httpsAgent}) { } } const res = await httpClient.get(url, options); - // @digitalbazaar/http-client may use node-fetch, which can output - // a warning if response.data is accessed and no json was parsed. - // Used here is the same type detection logic so the data field is - // accessed only if the client likely tried to parse JSON. - const contentType = res.headers.get('content-type'); - const hasJson = contentType && contentType.includes('json'); - return {res, body: hasJson ? res.data : null}; + return {res, body: res.data}; } catch(e) { // HTTP errors have a response in them // ky considers redirects HTTP errors diff --git a/package.json b/package.json index 85bce8b9..9b4102d2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "lib/**/*.js" ], "dependencies": { - "@digitalbazaar/http-client": "^3.0.1", + "@digitalbazaar/http-client": "^3.1.0", "canonicalize": "^1.0.1", "lru-cache": "^6.0.0", "rdf-canonize": "^3.0.0" @@ -78,7 +78,7 @@ "webpack-merge": "^5.7.3" }, "engines": { - "node": ">=12" + "node": ">=14" }, "keywords": [ "JSON", From e6baf7e6ed349430b3300deb3f8d99143adea49d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 3 Jun 2022 19:41:33 -0400 Subject: [PATCH 040/181] Update to `@digitalbazaar/http-client@3.2.0`. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b4102d2..7d5ce024 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "lib/**/*.js" ], "dependencies": { - "@digitalbazaar/http-client": "^3.1.0", + "@digitalbazaar/http-client": "^3.2.0", "canonicalize": "^1.0.1", "lru-cache": "^6.0.0", "rdf-canonize": "^3.0.0" From b79fc111ef15e13d8022aa9c1673383a629f6b59 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 3 Jun 2022 19:47:02 -0400 Subject: [PATCH 041/181] Update Node.js test versions. - Test on 16.x by default. - Test on 18.x. --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index feca597a..5a10be90 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x, 16.x] + node-version: [14.x, 16.x, 18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -26,7 +26,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x] + node-version: [16.x] bundler: [webpack, browserify] steps: - uses: actions/checkout@v2 @@ -47,7 +47,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -63,7 +63,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From 6d203965318adc206563e57b22620a30cbeba170 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 3 Jun 2022 21:07:59 -0400 Subject: [PATCH 042/181] Change `prepublish` to `prepack`, add build test. - Changed to avoid build step for development installs. - No `dist/` files will be generated by default for development installs. Run `npm run build` if needed. - CI build test added to check builds work. --- .github/workflows/main.yml | 21 ++++++++++++++++++--- CHANGELOG.md | 7 +++++++ package.json | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a10be90..e533b87c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,6 +3,21 @@ name: Node.js CI on: [push] jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + node-version: [16.x] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - name: Run eslint + run: npm run lint test-node: runs-on: ubuntu-latest timeout-minutes: 10 @@ -42,7 +57,7 @@ jobs: run: npm run test-karma env: BUNDLER: ${{ matrix.bundler }} - lint: + build: runs-on: ubuntu-latest timeout-minutes: 10 strategy: @@ -55,8 +70,8 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm install - - name: Run eslint - run: npm run lint + - name: Run build + run: npm run build coverage: needs: [test-node, test-karma] runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 63cb93c1..41a5a4da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ causes the included `node` `documentLoader` to not function and tests to fail. If you wish to still use earlier Node.js versions, you may still be able to do so with your own custom `documentLoader`. +- **BREAKING**: `npm` `prepublish` script changed to `prepack`. The `dist/` + contents will not be generated by default for development installs. Run `npm + run build` if needed. This was done to avoid extra work only needed for + packing and publication, and for temporary `webpack` version issues. A new CI + `build` test has been added to check builds pass. The `prepack` script could + be `prepare` instead if use cases exist where that is needed. File an issue + if this is a concern. - Update to `@digitalbazaar/http-client@3`: - Pulls in newer `ky` and `ky-universal` that should address security alerts and provide other improvements. diff --git a/package.json b/package.json index 7d5ce024..45aeffd2 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "jsonld" ], "scripts": { - "prepublish": "npm run build", + "prepack": "npm run build", "build": "npm run build-webpack", "build-webpack": "webpack", "fetch-test-suites": "npm run fetch-json-ld-wg-test-suite && npm run fetch-json-ld-org-test-suite && npm run fetch-normalization-test-suite", From 9f61795eb05fcd1dd8e3d6befcc89ae8fff28c34 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 3 Jun 2022 21:57:01 -0400 Subject: [PATCH 043/181] Fix immutable response header bug. Node 18 responses have an immutable location header. Refactor logic to avoid issue. --- lib/documentLoaders/node.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/documentLoaders/node.js b/lib/documentLoaders/node.js index 9de3b7ba..61ad6b30 100644 --- a/lib/documentLoaders/node.js +++ b/lib/documentLoaders/node.js @@ -80,6 +80,7 @@ module.exports = ({ url, headers, strictSSL, httpAgent, httpsAgent }); doc = {contextUrl: null, documentUrl: url, document: body || null}; + // handle error const statusText = http.STATUS_CODES[res.status]; if(res.status >= 400) { @@ -92,7 +93,9 @@ module.exports = ({ }); } const link = res.headers.get('link'); + let location = res.headers.get('location'); const contentType = res.headers.get('content-type'); + // handle Link Header if(link && contentType !== 'application/ld+json') { // only 1 related link header permitted @@ -115,10 +118,10 @@ module.exports = ({ alternate.type == 'application/ld+json' && !(contentType || '') .match(/^application\/(\w*\+)?json$/)) { - res.headers.set('location', prependBase(url, alternate.target)); + location = prependBase(url, alternate.target); } } - const location = res.headers.get('location'); + // handle redirect if((alternate || res.status >= 300 && res.status < 400) && location) { From 51a626065d6edd856f02753f8c5dbb77b6d7c06c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 6 Jun 2022 18:50:54 -0400 Subject: [PATCH 044/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a5a4da..2651830f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 6.0.0 - 2022-xx-xx +## 6.0.0 - 2022-06-06 ### Changed - **BREAKING**: Drop testing and support for Node.js 12.x. The majority of the From d5615507af1f4b66d2f8643835ba3905d7998d6b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 6 Jun 2022 18:50:56 -0400 Subject: [PATCH 045/181] Release 6.0.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45aeffd2..5e04bdf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "5.2.1-0", + "version": "6.0.0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 345bc00c43be488bafc49953c84d3bf022612e7e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 6 Jun 2022 18:52:05 -0400 Subject: [PATCH 046/181] Start 6.0.1-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e04bdf0..090eec02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "6.0.0", + "version": "6.0.1-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 8b865ba89f4bf4403e091321e6773d2472cd62c8 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 6 Jun 2022 19:28:09 -0400 Subject: [PATCH 047/181] Update eslint dependencies, fix issues. - Update to eslint@8. - Update to eslint-config-digitalbazaar@3. - Fix lint issues. --- CHANGELOG.md | 1 + benchmarks/compare/.eslintrc.cjs | 10 ++++++++++ benchmarks/compare/.eslintrc.js | 9 --------- benchmarks/compare/compare.js | 11 +++++------ benchmarks/compare/package.json | 9 +++++---- package.json | 4 ++-- 6 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 benchmarks/compare/.eslintrc.cjs delete mode 100644 benchmarks/compare/.eslintrc.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d345b849..1cf4494b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed - Change EARL Assertor to Digital Bazaar, Inc. +- Update eslint dependencies. ## 6.0.0 - 2022-06-06 diff --git a/benchmarks/compare/.eslintrc.cjs b/benchmarks/compare/.eslintrc.cjs new file mode 100644 index 00000000..cb32e23a --- /dev/null +++ b/benchmarks/compare/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + root: true, + env: { + node: true + }, + extends: [ + 'digitalbazaar', + 'digitalbazaar/module' + ] +}; diff --git a/benchmarks/compare/.eslintrc.js b/benchmarks/compare/.eslintrc.js deleted file mode 100644 index afb2fa21..00000000 --- a/benchmarks/compare/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - env: { - commonjs: true, - node: true, - es2020: true - }, - extends: 'eslint-config-digitalbazaar', - root: true -}; diff --git a/benchmarks/compare/compare.js b/benchmarks/compare/compare.js index 084da43a..12803c9b 100755 --- a/benchmarks/compare/compare.js +++ b/benchmarks/compare/compare.js @@ -3,7 +3,6 @@ import yargs from 'yargs'; import {hideBin} from 'yargs/helpers'; import {promises as fs} from 'node:fs'; -import {table} from 'table'; import {markdownTable} from 'markdown-table'; import commonPathPrefix from 'common-path-prefix'; @@ -35,7 +34,7 @@ yargs(hideBin(process.argv)) .command( '$0 ', 'compare JSON-LD benchmark files', () => {}, - async (argv) => { + async argv => { return compare(argv); }) .parse(); @@ -43,9 +42,9 @@ yargs(hideBin(process.argv)) async function compare({ env, file, - format, + //format, relative, - verbose + //verbose }) { const contents = await Promise.all(file.map(async f => ({ fn: f, @@ -86,7 +85,7 @@ async function compare({ return a['jldb:result']['jldb:hz']; } function rfmt(base, a) { - return relative ? (100*(a-base)/base) : a; + return relative ? (100 * (a - base) / base) : a; } const compared = ordered.map(t => [ t.slice(tprefixlen), @@ -110,7 +109,7 @@ async function compare({ ], { align: [ 'l', - ...results.map(r => 'r') + ...results.map(() => 'r') ] })); console.log(); diff --git a/benchmarks/compare/package.json b/benchmarks/compare/package.json index 4aca2f07..81b5eb37 100644 --- a/benchmarks/compare/package.json +++ b/benchmarks/compare/package.json @@ -26,12 +26,13 @@ "main": "compare.js", "dependencies": { "common-path-prefix": "^3.0.0", + "eslint-plugin-unicorn": "^42.0.0", "markdown-table": "^3.0.2", - "yargs": "^17.4.0" + "yargs": "^17.5.1" }, "devDependencies": { - "eslint": "^8.11.0", - "eslint-config-digitalbazaar": "^2.8.0" + "eslint": "^8.17.0", + "eslint-config-digitalbazaar": "^3.0.0" }, "engines": { "node": ">=12" @@ -46,6 +47,6 @@ "benchmark" ], "scripts": { - "lint": "eslint *.js" + "lint": "eslint ." } } diff --git a/package.json b/package.json index 090eec02..ba950d6e 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,8 @@ "cors": "^2.7.1", "cross-env": "^7.0.3", "envify": "^4.1.0", - "eslint": "^7.23.0", - "eslint-config-digitalbazaar": "^2.6.1", + "eslint": "^8.17.0", + "eslint-config-digitalbazaar": "^3.0.0", "esmify": "^2.1.1", "express": "^4.16.4", "fs-extra": "^9.1.0", From bbcf4dcf8ed12e2c5a98d38088d11a9694c4aa76 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 6 Jun 2022 19:35:33 -0400 Subject: [PATCH 048/181] Update linting. - Remove compare tool local eslint config. - Remove compare tool lint support. Depend on top-level linting. --- .eslintrc.js | 6 ++++-- benchmarks/compare/.eslintrc.cjs | 10 ---------- benchmarks/compare/package.json | 12 ++---------- 3 files changed, 6 insertions(+), 22 deletions(-) delete mode 100644 benchmarks/compare/.eslintrc.cjs diff --git a/.eslintrc.js b/.eslintrc.js index b70fd6f7..eb45c536 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,12 +1,14 @@ module.exports = { + root: true, env: { browser: true, commonjs: true, node: true, es2020: true }, - extends: 'eslint-config-digitalbazaar', - root: true, + extends: [ + 'digitalbazaar' + ], ignorePatterns: [ 'dist/', 'tests/webidl/WebIDLParser.js', diff --git a/benchmarks/compare/.eslintrc.cjs b/benchmarks/compare/.eslintrc.cjs deleted file mode 100644 index cb32e23a..00000000 --- a/benchmarks/compare/.eslintrc.cjs +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - root: true, - env: { - node: true - }, - extends: [ - 'digitalbazaar', - 'digitalbazaar/module' - ] -}; diff --git a/benchmarks/compare/package.json b/benchmarks/compare/package.json index 81b5eb37..141699a3 100644 --- a/benchmarks/compare/package.json +++ b/benchmarks/compare/package.json @@ -26,16 +26,11 @@ "main": "compare.js", "dependencies": { "common-path-prefix": "^3.0.0", - "eslint-plugin-unicorn": "^42.0.0", "markdown-table": "^3.0.2", "yargs": "^17.5.1" }, - "devDependencies": { - "eslint": "^8.17.0", - "eslint-config-digitalbazaar": "^3.0.0" - }, "engines": { - "node": ">=12" + "node": ">=14" }, "keywords": [ "JSON", @@ -45,8 +40,5 @@ "Semantic Web", "jsonld", "benchmark" - ], - "scripts": { - "lint": "eslint ." - } + ] } From b52be256785b9dd286350f61f4e3f62932edf260 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 15 Dec 2021 00:54:06 -0500 Subject: [PATCH 049/181] Fix test names. - Remove spaces in names by using string concatination. --- tests/misc.js | 59 ++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index d9dae4fc..3d003860 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -479,7 +479,7 @@ describe('literal JSON', () => { }); }); -describe('expansionMap', () => { +describe.only('expansionMap', () => { describe('unmappedProperty', () => { it('should be called on unmapped term', async () => { const docWithUnMappedTerm = { @@ -614,8 +614,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it('should be called on relative iri for type\ - term in scoped context', async () => { + it('should be called on relative iri for type ' + + 'term in scoped context', async () => { const docWithRelativeIriId = { '@context': { 'definedType': { @@ -645,8 +645,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it('should be called on relative iri for \ - type term with multiple relative iri types', async () => { + it('should be called on relative iri for ' + + 'type term with multiple relative iri types', async () => { const docWithRelativeIriId = { '@context': { 'definedTerm': 'https://example.com#definedTerm' @@ -669,8 +669,9 @@ describe('expansionMap', () => { assert.equal(expansionMapCalledTimes, 3); }); - it('should be called on relative iri for \ - type term with multiple relative iri types in scoped context', async () => { + it('should be called on relative iri for ' + + 'type term with multiple relative iri types in scoped context' + + '', async () => { const docWithRelativeIriId = { '@context': { 'definedType': { @@ -701,8 +702,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalledTimes, 3); }); - it('should be called on relative iri for \ - type term with multiple types', async () => { + it('should be called on relative iri for ' + + 'type term with multiple types', async () => { const docWithRelativeIriId = { '@context': { 'definedTerm': 'https://example.com#definedTerm' @@ -747,8 +748,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called on relative iri when \ - @base value is './'", async () => { + it("should be called on relative iri when " + + "@base value is './'", async () => { const docWithRelativeIriId = { '@context': { "@base": "./", @@ -768,8 +769,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called on relative iri when \ - @base value is './'", async () => { + it("should be called on relative iri when " + + "@base value is './'", async () => { const docWithRelativeIriId = { '@context': { "@base": "./", @@ -789,8 +790,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called on relative iri when \ - @vocab value is './'", async () => { + it("should be called on relative iri when " + + "@vocab value is './'", async () => { const docWithRelativeIriId = { '@context': { "@vocab": "./", @@ -812,8 +813,8 @@ describe('expansionMap', () => { }); describe('prependedIri', () => { - it("should be called when property is \ - being expanded with `@vocab`", async () => { + it("should be called when property is " + + "being expanded with `@vocab`", async () => { const doc = { '@context': { "@vocab": "http://example.com/", @@ -838,8 +839,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called when '@type' is \ - being expanded with `@vocab`", async () => { + it("should be called when '@type' is " + + "being expanded with `@vocab`", async () => { const doc = { '@context': { "@vocab": "http://example.com/", @@ -864,8 +865,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called when aliased '@type' is \ - being expanded with `@vocab`", async () => { + it("should be called when aliased '@type' is " + + "being expanded with `@vocab`", async () => { const doc = { '@context': { "@vocab": "http://example.com/", @@ -891,8 +892,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called when '@id' is being \ - expanded with `@base`", async () => { + it("should be called when '@id' is being " + + "expanded with `@base`", async () => { const doc = { '@context': { "@base": "http://example.com/", @@ -919,8 +920,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called when aliased '@id' \ - is being expanded with `@base`", async () => { + it("should be called when aliased '@id' " + + "is being expanded with `@base`", async () => { const doc = { '@context': { "@base": "http://example.com/", @@ -948,8 +949,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called when '@type' is \ - being expanded with `@base`", async () => { + it("should be called when '@type' is " + + "being expanded with `@base`", async () => { const doc = { '@context': { "@base": "http://example.com/", @@ -976,8 +977,8 @@ describe('expansionMap', () => { assert.equal(expansionMapCalled, true); }); - it("should be called when aliased '@type' is \ - being expanded with `@base`", async () => { + it("should be called when aliased '@type' is " + + "being expanded with `@base`", async () => { const doc = { '@context': { "@base": "http://example.com/", From 04d0bec177b32002763218a45abbad8af62166af Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 15 Dec 2021 12:36:15 -0500 Subject: [PATCH 050/181] Update expansionMap tests. - Count and check all calls of various types. --- tests/misc.js | 393 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 300 insertions(+), 93 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index 3d003860..22a1daa0 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -480,6 +480,48 @@ describe('literal JSON', () => { }); describe.only('expansionMap', () => { + // track all the counts + // use simple count object (don't use tricky test keys!) + function addCounts(counts, info) { + // overall call count + counts.expansionMap = counts.expansionMap || 0; + counts.expansionMap++; + + if(info.unmappedProperty) { + const c = counts.unmappedProperty = counts.unmappedProperty || {}; + const k = info.unmappedProperty; + c[k] = c[k] || 0; + c[k]++; + } + + if(info.unmappedValue) { + const c = counts.unmappedValue = counts.unmappedValue || {}; + const v = info.unmappedValue; + let k; + if(Object.keys(v).length === 1 && '@id' in v) { + k = v['@id']; + } else { + k = '__unknown__'; + } + c[k] = c[k] || 0; + c[k]++; + } + + if(info.relativeIri) { + const c = counts.relativeIri = counts.relativeIri || {}; + const k = info.relativeIri; + c[k] = c[k] || 0; + c[k]++; + } + + if(info.prependedIri) { + const c = counts.prependedIri = counts.prependedIri || {}; + const k = info.prependedIri.value; + c[k] = c[k] || 0; + c[k]++; + } + } + describe('unmappedProperty', () => { it('should be called on unmapped term', async () => { const docWithUnMappedTerm = { @@ -490,16 +532,22 @@ describe.only('expansionMap', () => { testUndefined: "is undefined" }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.unmappedProperty === 'testUndefined') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithUnMappedTerm, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 3, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + } + }); }); it('should be called on nested unmapped term', async () => { @@ -512,16 +560,22 @@ describe.only('expansionMap', () => { } }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.unmappedProperty === 'testUndefined') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithUnMappedTerm, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 3, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + } + }); }); }); @@ -535,16 +589,22 @@ describe.only('expansionMap', () => { definedTerm: "is defined" }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } + }); }); it('should be called on relative iri for id term (nested)', async () => { @@ -558,16 +618,22 @@ describe.only('expansionMap', () => { } }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } + }); }); it('should be called on relative iri for aliased id term', async () => { @@ -580,16 +646,22 @@ describe.only('expansionMap', () => { definedTerm: "is defined" }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } + }); }); it('should be called on relative iri for type term', async () => { @@ -602,16 +674,26 @@ describe.only('expansionMap', () => { definedTerm: "is defined" }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } + }); }); it('should be called on relative iri for type ' + @@ -633,16 +715,26 @@ describe.only('expansionMap', () => { } }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } + }); }); it('should be called on relative iri for ' + @@ -656,17 +748,28 @@ describe.only('expansionMap', () => { definedTerm: "is defined" }; - let expansionMapCalledTimes = 0; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri' || - info.relativeIri === 'anotherRelativeiri') { - expansionMapCalledTimes++; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalledTimes, 3); + assert.deepStrictEqual(counts, { + expansionMap: 8, + prependedIri: { + anotherRelativeiri: 1, + relativeiri: 1 + }, + relativeIri: { + anotherRelativeiri: 1, + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } + }); }); it('should be called on relative iri for ' + @@ -689,17 +792,28 @@ describe.only('expansionMap', () => { } }; - let expansionMapCalledTimes = 0; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri' || - info.relativeIri === 'anotherRelativeiri') { - expansionMapCalledTimes++; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalledTimes, 3); + assert.deepStrictEqual(counts, { + expansionMap: 8, + prependedIri: { + anotherRelativeiri: 1, + relativeiri: 1 + }, + relativeIri: { + anotherRelativeiri: 1, + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } + }); }); it('should be called on relative iri for ' + @@ -713,16 +827,26 @@ describe.only('expansionMap', () => { definedTerm: "is defined" }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } + }); }); it('should be called on relative iri for aliased type term', async () => { @@ -736,16 +860,26 @@ describe.only('expansionMap', () => { definedTerm: "is defined" }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === 'relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } + }); }); it("should be called on relative iri when " + @@ -757,16 +891,25 @@ describe.only('expansionMap', () => { '@id': "relativeiri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === '/relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 3, + prependedIri: { + 'relativeiri': 1 + }, + relativeIri: { + '/relativeiri': 1 + }, + unmappedValue: { + '/relativeiri': 1 + } + }); }); it("should be called on relative iri when " + @@ -778,16 +921,25 @@ describe.only('expansionMap', () => { '@id': "relativeiri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === '/relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 3, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + '/relativeiri': 1 + }, + unmappedValue: { + '/relativeiri': 1 + } + }); }); it("should be called on relative iri when " + @@ -799,16 +951,24 @@ describe.only('expansionMap', () => { '@type': "relativeiri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { - if(info.relativeIri === '/relativeiri') { - expansionMapCalled = true; - } + addCounts(counts, info); }; await jsonld.expand(docWithRelativeIriId, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 6, + prependedIri: { + './': 1, + relativeiri: 2 + }, + relativeIri: { + '/': 1, + '/relativeiri': 2 + } + }); }); }); @@ -822,8 +982,9 @@ describe.only('expansionMap', () => { 'term': "termValue", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { + addCounts(counts, info); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -831,12 +992,16 @@ describe.only('expansionMap', () => { typeExpansion: false, result: 'http://example.com/term' }); - expansionMapCalled = true; }; await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 4, + prependedIri: { + term: 4 + } + }); }); it("should be called when '@type' is " + @@ -848,8 +1013,9 @@ describe.only('expansionMap', () => { '@type': "relativeIri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { + addCounts(counts, info); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -857,12 +1023,16 @@ describe.only('expansionMap', () => { typeExpansion: true, result: 'http://example.com/relativeIri' }); - expansionMapCalled = true; }; await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 2 + } + }); }); it("should be called when aliased '@type' is " + @@ -875,8 +1045,9 @@ describe.only('expansionMap', () => { 'type': "relativeIri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { + addCounts(counts, info); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -884,12 +1055,16 @@ describe.only('expansionMap', () => { typeExpansion: true, result: 'http://example.com/relativeIri' }); - expansionMapCalled = true; }; await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 2 + } + }); }); it("should be called when '@id' is being " + @@ -901,8 +1076,9 @@ describe.only('expansionMap', () => { '@id': "relativeIri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { + addCounts(counts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -911,13 +1087,20 @@ describe.only('expansionMap', () => { typeExpansion: false, result: 'http://example.com/relativeIri' }); - expansionMapCalled = true; } }; await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + unmappedValue: { + 'http://example.com/relativeIri': 1 + } + }); }); it("should be called when aliased '@id' " + @@ -930,8 +1113,9 @@ describe.only('expansionMap', () => { 'id': "relativeIri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { + addCounts(counts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -940,13 +1124,20 @@ describe.only('expansionMap', () => { typeExpansion: false, result: 'http://example.com/relativeIri' }); - expansionMapCalled = true; } }; await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + unmappedValue: { + 'http://example.com/relativeIri': 1 + } + }); }); it("should be called when '@type' is " + @@ -958,8 +1149,9 @@ describe.only('expansionMap', () => { '@type': "relativeIri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { + addCounts(counts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -968,13 +1160,20 @@ describe.only('expansionMap', () => { typeExpansion: true, result: 'http://example.com/relativeIri' }); - expansionMapCalled = true; } }; await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + relativeIri: { + relativeIri: 1 + } + }); }); it("should be called when aliased '@type' is " + @@ -987,8 +1186,9 @@ describe.only('expansionMap', () => { 'type': "relativeIri", }; - let expansionMapCalled = false; + const counts = {}; const expansionMap = info => { + addCounts(counts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -997,13 +1197,20 @@ describe.only('expansionMap', () => { typeExpansion: true, result: 'http://example.com/relativeIri' }); - expansionMapCalled = true; } }; await jsonld.expand(doc, {expansionMap}); - assert.equal(expansionMapCalled, true); + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + relativeIri: { + relativeIri: 1 + } + }); }); }); }); From 794fb8df1799ff18d60e24225b98274aece7cd3f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 00:06:09 -0400 Subject: [PATCH 051/181] Start more expansionMap tests. --- tests/misc.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/misc.js b/tests/misc.js index 22a1daa0..cfa65dbd 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -523,6 +523,56 @@ describe.only('expansionMap', () => { } describe('unmappedProperty', () => { + // FIXME move to value section + it.skip('should have zero counts with empty input', async () => { + const docWithNoContent = {}; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + }; + + await jsonld.expand(docWithNoContent, {expansionMap}); + + assert.deepStrictEqual(counts, {}); + }); + + // FIXME move to value section + it.skip('should have zero counts with no terms', async () => { + const docWithNoTerms = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + } + }; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + }; + + await jsonld.expand(docWithNoTerms, {expansionMap}); + + assert.deepStrictEqual(counts, {}); + }); + + it.skip('should have zero counts with mapped term', async () => { + const docWithMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + definedTerm: "is defined" + }; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + }; + + await jsonld.expand(docWithMappedTerm, {expansionMap}); + + assert.deepStrictEqual(counts, {}); + }); + it('should be called on unmapped term', async () => { const docWithUnMappedTerm = { '@context': { From 9f053d30b9ffc9b0ea69b8b4104cebef0d6d69ee Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 21:17:44 -0500 Subject: [PATCH 052/181] 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 1cf4494b..534191d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ and provide other improvements. - Use global `URL` interface to handle relative redirects. +### 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 bf234046882dbf62c28175d6743bfcc275719972 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 21:25:17 -0500 Subject: [PATCH 053/181] 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 534191d3..4d73912d 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 cfa65dbd..f420f1c5 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1264,3 +1264,172 @@ describe.only('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 d1dbda155191dc1e42e70c0f48665f89b2cf864b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 23:18:29 -0500 Subject: [PATCH 054/181] 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 f420f1c5..5327bdc3 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1432,4 +1432,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 d4a2620760281b1d9103244d84c721102892972f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 24 Feb 2020 13:15:31 -0500 Subject: [PATCH 055/181] 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 4d73912d..374ef062 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 5327bdc3..9df12041 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1279,7 +1279,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 { @@ -1304,7 +1304,7 @@ describe('events', () => { let handled0 = false; let handled1 = false; const e0 = await jsonld.expand(d, { - handleEvent: { + eventHandler: { 'invalid reserved term': () => { handled0 = true; } @@ -1312,7 +1312,7 @@ describe('events', () => { }); // FIXME: ensure cache is being used const e1 = await jsonld.expand(d, { - handleEvent: { + eventHandler: { 'invalid reserved term': () => { handled1 = true; } @@ -1338,7 +1338,7 @@ describe('events', () => { let ranHandler1 = false; let handled = false; const e = await jsonld.expand(d, { - handleEvent: [ + eventHandler: [ ({next}) => { ranHandler0 = true; // skip to next handler @@ -1372,7 +1372,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; @@ -1399,7 +1399,7 @@ describe('events', () => { let ranHandler3 = false; let handled = false; const e = await jsonld.expand(d, { - handleEvent: [ + eventHandler: [ ({next}) => { ranHandler0 = true; next(); @@ -1463,7 +1463,7 @@ describe('events', () => { let handledReservedValue = false; let handledLanguage = false; const e = await jsonld.expand(d, { - handleEvent: { + eventHandler: { 'invalid reserved term': () => { handledReservedTerm = true; }, @@ -1528,7 +1528,7 @@ describe('events', () => { let handledLanguage2 = false; const e2 = await jsonld.fromRDF(d2, { rdfDirection: 'i18n-datatype', - handleEvent: { + eventHandler: { 'invalid @language value': () => { handledLanguage2 = true; } From b0b0b179730b813551860f35a62a41e93949035a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 24 Feb 2020 13:22:17 -0500 Subject: [PATCH 056/181] 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({ From 56d818f79340bbb4c478ee3b6926b5e2e769b0aa Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 01:01:21 -0400 Subject: [PATCH 057/181] Temporary restrict tests. --- tests/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/misc.js b/tests/misc.js index 9df12041..b752b247 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1265,7 +1265,7 @@ describe.only('expansionMap', () => { }); }); -describe('events', () => { +describe.only('events', () => { it('handle warning event with function', async () => { const d = { From a430611a3d66656d6fede4bf9d3a1eaa528a3ae7 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 01:01:57 -0400 Subject: [PATCH 058/181] Test events before expansionMap. --- tests/misc.js | 760 +++++++++++++++++++++++++------------------------- 1 file changed, 380 insertions(+), 380 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index b752b247..eaad37a4 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -479,6 +479,280 @@ describe('literal JSON', () => { }); }); +describe.only('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, { + eventHandler: ({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, { + eventHandler: { + 'invalid reserved term': () => { + handled0 = true; + } + } + }); + // FIXME: ensure cache is being used + const e1 = await jsonld.expand(d, { + eventHandler: { + '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, { + eventHandler: [ + ({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, { + eventHandler: { + '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, { + eventHandler: [ + ({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'); + }); + 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, { + eventHandler: { + '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', + eventHandler: { + 'invalid @language value': () => { + handledLanguage2 = true; + } + } + }); + assert.deepStrictEqual(e2, ex2); + assert.equal(handledLanguage2, true); + }); +}); + describe.only('expansionMap', () => { // track all the counts // use simple count object (don't use tricky test keys!) @@ -1140,401 +1414,127 @@ describe.only('expansionMap', () => { } }; - await jsonld.expand(doc, {expansionMap}); - - assert.deepStrictEqual(counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - unmappedValue: { - 'http://example.com/relativeIri': 1 - } - }); - }); - - it("should be called when aliased '@id' " + - "is being expanded with `@base`", async () => { - const doc = { - '@context': { - "@base": "http://example.com/", - "id": "@id" - }, - 'id': "relativeIri", - }; - - const counts = {}; - const expansionMap = info => { - addCounts(counts, info); - if(info.prependedIri) { - assert.deepStrictEqual(info.prependedIri, { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: false, - result: 'http://example.com/relativeIri' - }); - } - }; - - await jsonld.expand(doc, {expansionMap}); - - assert.deepStrictEqual(counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - unmappedValue: { - 'http://example.com/relativeIri': 1 - } - }); - }); - - it("should be called when '@type' is " + - "being expanded with `@base`", async () => { - const doc = { - '@context': { - "@base": "http://example.com/", - }, - '@type': "relativeIri", - }; - - const counts = {}; - const expansionMap = info => { - addCounts(counts, info); - if(info.prependedIri) { - assert.deepStrictEqual(info.prependedIri, { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - }); - } - }; - - await jsonld.expand(doc, {expansionMap}); - - assert.deepStrictEqual(counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - relativeIri: { - relativeIri: 1 - } - }); - }); - - it("should be called when aliased '@type' is " + - "being expanded with `@base`", async () => { - const doc = { - '@context': { - "@base": "http://example.com/", - "type": "@type" - }, - 'type': "relativeIri", - }; - - const counts = {}; - const expansionMap = info => { - addCounts(counts, info); - if(info.prependedIri) { - assert.deepStrictEqual(info.prependedIri, { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - }); - } - }; - - await jsonld.expand(doc, {expansionMap}); - - assert.deepStrictEqual(counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - relativeIri: { - relativeIri: 1 - } - }); - }); - }); -}); - -describe.only('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, { - eventHandler: ({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, { - eventHandler: { - 'invalid reserved term': () => { - handled0 = true; - } - } - }); - // FIXME: ensure cache is being used - const e1 = await jsonld.expand(d, { - eventHandler: { - '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, { - eventHandler: [ - ({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, { - eventHandler: { - 'invalid reserved term': ({event}) => { - assert.equal(event.details.term, '@RESERVED'); - handled = true; + await jsonld.expand(doc, {expansionMap}); + + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + unmappedValue: { + 'http://example.com/relativeIri': 1 } - } + }); }); - 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, { - eventHandler: [ - ({next}) => { - ranHandler0 = true; - next(); + it("should be called when aliased '@id' " + + "is being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + "id": "@id" }, - [ - ({next}) => { - ranHandler1 = true; - next(); - }, - { - 'bogus code': () => {} - } - ], - ({next}) => { - ranHandler2 = true; - next(); + 'id': "relativeIri", + }; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: false, + result: 'http://example.com/relativeIri' + }); + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 }, - { - 'invalid reserved term': () => { - ranHandler3 = true; - handled = true; - } + unmappedValue: { + 'http://example.com/relativeIri': 1 } - ] + }); }); - 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'); - }); - 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, { - eventHandler: { - 'invalid reserved term': () => { - handledReservedTerm = true; + it("should be called when '@type' is " + + "being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", }, - 'invalid reserved value': () => { - handledReservedValue = true; + '@type': "relativeIri", + }; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + }); + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 }, - 'invalid @language value': () => { - handledLanguage = true; + relativeIri: { + relativeIri: 1 } - } + }); }); - 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" - } - ] - } -] -; + it("should be called when aliased '@type' is " + + "being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + "type": "@type" + }, + 'type': "relativeIri", + }; - let handledLanguage2 = false; - const e2 = await jsonld.fromRDF(d2, { - rdfDirection: 'i18n-datatype', - eventHandler: { - 'invalid @language value': () => { - handledLanguage2 = true; + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + }); } - } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.deepStrictEqual(counts, { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + relativeIri: { + relativeIri: 1 + } + }); }); - assert.deepStrictEqual(e2, ex2); - assert.equal(handledLanguage2, true); }); }); From 9d168bc2760e7c3a847a9ddb8275b9a5ad3c6fab Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 02:22:59 -0400 Subject: [PATCH 059/181] Improve event and expansionMap tests. - Track and check event counts. - Add more expansionMap tests. --- tests/misc.js | 338 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 270 insertions(+), 68 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index eaad37a4..f202a3a0 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -479,7 +479,23 @@ describe('literal JSON', () => { }); }); +// track all the event counts +// use simple count object (don't use tricky test keys!) +function addEventCounts(counts, event) { + // overall call counts + counts.events = counts.events || 0; + counts.codes = counts.codes || {}; + + counts.codes[event.code] = counts.codes[event.code] || 0; + + counts.events++; + counts.codes[event.code]++; +} + describe.only('events', () => { + // FIXME add default handler tests + // FIXME add object '*' handler tests + it('handle warning event with function', async () => { const d = { @@ -491,19 +507,22 @@ describe.only('events', () => { ; const ex = []; - let handled = false; + const counts = {}; const e = await jsonld.expand(d, { - eventHandler: ({event, next}) => { - if(event.code === 'invalid reserved term') { - handled = true; - } else { - next(); - } + eventHandler: ({event}) => { + addEventCounts(counts, event); } }); assert.deepStrictEqual(e, ex); - assert.equal(handled, true); + assert.deepStrictEqual(counts, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }); }); + it('cached context event replay', async () => { const d = { @@ -515,28 +534,39 @@ describe.only('events', () => { ; const ex = []; - let handled0 = false; - let handled1 = false; + const counts0 = {}; + const counts1 = {}; const e0 = await jsonld.expand(d, { eventHandler: { - 'invalid reserved term': () => { - handled0 = true; + 'invalid reserved term': ({event}) => { + addEventCounts(counts0, event); } } }); // FIXME: ensure cache is being used const e1 = await jsonld.expand(d, { eventHandler: { - 'invalid reserved term': () => { - handled1 = true; + 'invalid reserved term': ({event}) => { + addEventCounts(counts1, event); } } }); assert.deepStrictEqual(e0, ex); assert.deepStrictEqual(e1, ex); - assert.equal(handled0, true, 'handled 0'); - assert.equal(handled1, true, 'handled 1'); + assert.deepStrictEqual(counts0, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts 0'); + assert.deepStrictEqual(counts1, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts 1'); }); + it('handle warning event with array of functions', async () => { const d = { @@ -548,31 +578,89 @@ describe.only('events', () => { ; const ex = []; - let ranHandler0 = false; - let ranHandler1 = false; - let handled = false; + const handlerCounts0 = {}; + const handlerCounts1 = {}; + const handledCounts = {}; const e = await jsonld.expand(d, { eventHandler: [ - ({next}) => { - ranHandler0 = true; + ({event, next}) => { + addEventCounts(handlerCounts0, event); // skip to next handler next(); }, - ({event, next}) => { - ranHandler1 = true; + ({event}) => { + addEventCounts(handlerCounts1, event); if(event.code === 'invalid reserved term') { - handled = true; + addEventCounts(handledCounts, event); 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'); + assert.deepStrictEqual(handlerCounts0, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 0'); + assert.deepStrictEqual(handlerCounts1, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 1'); + assert.deepStrictEqual(handledCounts, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts handled'); }); + + it('handle warning event early with array of functions', async () => { + const d = +{ + "@context": { + "@RESERVED": "ex:test-function-array-handler" + }, + "@RESERVED": "test" +} +; + const ex = []; + + const handlerCounts0 = {}; + const handlerCounts1 = {}; + const handledCounts = {}; + const e = await jsonld.expand(d, { + eventHandler: [ + ({event}) => { + addEventCounts(handlerCounts0, event); + // don't skip to next handler + }, + ({event}) => { + addEventCounts(handlerCounts1, event); + if(event.code === 'invalid reserved term') { + addEventCounts(handledCounts, event); + return; + } + } + ] + }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(handlerCounts0, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 0'); + assert.deepStrictEqual(handlerCounts1, {}, 'counts handler 1'); + assert.deepStrictEqual(handledCounts, {}, 'counts handled'); + }); + it('handle warning event with code:function object', async () => { const d = { @@ -584,18 +672,24 @@ describe.only('events', () => { ; const ex = []; - let handled = false; + const counts = {}; const e = await jsonld.expand(d, { eventHandler: { 'invalid reserved term': ({event}) => { - assert.equal(event.details.term, '@RESERVED'); - handled = true; + addEventCounts(counts, event); + assert.strictEqual(event.details.term, '@RESERVED'); } } }); assert.deepStrictEqual(e, ex); - assert.equal(handled, true, 'handled'); + assert.deepStrictEqual(counts, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts'); }); + it('handle warning event with complex handler', async () => { const d = { @@ -607,45 +701,66 @@ describe.only('events', () => { ; const ex = []; - let ranHandler0 = false; - let ranHandler1 = false; - let ranHandler2 = false; - let ranHandler3 = false; - let handled = false; + const handlerCounts0 = {}; + const handlerCounts1 = {}; + const handlerCounts2 = {}; + const handlerCounts3 = {}; const e = await jsonld.expand(d, { eventHandler: [ - ({next}) => { - ranHandler0 = true; + ({event, next}) => { + addEventCounts(handlerCounts0, event); next(); }, [ - ({next}) => { - ranHandler1 = true; + ({event, next}) => { + addEventCounts(handlerCounts1, event); next(); }, { 'bogus code': () => {} } ], - ({next}) => { - ranHandler2 = true; + ({event, next}) => { + addEventCounts(handlerCounts2, event); next(); }, { - 'invalid reserved term': () => { - ranHandler3 = true; - handled = true; + 'invalid reserved term': ({event}) => { + addEventCounts(handlerCounts3, event); } } ] }); 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'); + assert.deepStrictEqual(handlerCounts0, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 0'); + assert.deepStrictEqual(handlerCounts1, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 1'); + assert.deepStrictEqual(handlerCounts2, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 2'); + assert.deepStrictEqual(handlerCounts3, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts handler 3'); }); + it('handle known warning events', async () => { const d = { @@ -673,26 +788,41 @@ describe.only('events', () => { ] ; - let handledReservedTerm = false; - let handledReservedValue = false; - let handledLanguage = false; + const handledReservedTermCounts = {}; + const handledReservedValueCounts = {}; + const handledLanguageCounts = {}; const e = await jsonld.expand(d, { eventHandler: { - 'invalid reserved term': () => { - handledReservedTerm = true; + 'invalid reserved term': ({event}) => { + addEventCounts(handledReservedTermCounts, event); }, - 'invalid reserved value': () => { - handledReservedValue = true; + 'invalid reserved value': ({event}) => { + addEventCounts(handledReservedValueCounts, event); }, - 'invalid @language value': () => { - handledLanguage = true; + 'invalid @language value': ({event}) => { + addEventCounts(handledLanguageCounts, event); } } }); assert.deepStrictEqual(e, ex); - assert.equal(handledReservedTerm, true); - assert.equal(handledReservedValue, true); - assert.equal(handledLanguage, true); + assert.deepStrictEqual(handledReservedTermCounts, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'handled reserved term counts'); + assert.deepStrictEqual(handledReservedValueCounts, { + codes: { + 'invalid reserved value': 1 + }, + events: 1 + }, 'handled reserved value counts'); + assert.deepStrictEqual(handledLanguageCounts, { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, 'handled language counts'); // dataset with invalid language tag // Equivalent N-Quads: @@ -739,17 +869,22 @@ describe.only('events', () => { ] ; - let handledLanguage2 = false; + const handledLanguageCounts2 = {}; const e2 = await jsonld.fromRDF(d2, { rdfDirection: 'i18n-datatype', eventHandler: { - 'invalid @language value': () => { - handledLanguage2 = true; + 'invalid @language value': ({event}) => { + addEventCounts(handledLanguageCounts2, event); } } }); assert.deepStrictEqual(e2, ex2); - assert.equal(handledLanguage2, true); + assert.deepStrictEqual(handledLanguageCounts2, { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, 'handled language counts'); }); }); @@ -829,6 +964,21 @@ describe.only('expansionMap', () => { assert.deepStrictEqual(counts, {}); }); + it.skip('should have zero counts with absolute term', async () => { + const docWithMappedTerm = { + 'urn:definedTerm': "is defined" + }; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + }; + + await jsonld.expand(docWithMappedTerm, {expansionMap}); + + assert.deepStrictEqual(counts, {}); + }); + it.skip('should have zero counts with mapped term', async () => { const docWithMappedTerm = { '@context': { @@ -847,7 +997,59 @@ describe.only('expansionMap', () => { assert.deepStrictEqual(counts, {}); }); - it('should be called on unmapped term', async () => { + it.skip('should be called on unmapped term with no context', async () => { + const docWithUnMappedTerm = { + testUndefined: "is undefined" + }; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + }; + + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + + assert.deepStrictEqual(counts, { + expansionMap: 3, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + } + }); + }); + + it('should be called on unmapped term with context [1]', async () => { + const docWithUnMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + testUndefined: "is undefined" + }; + + const counts = {}; + const expansionMap = info => { + addCounts(counts, info); + }; + + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + + assert.deepStrictEqual(counts, { + expansionMap: 4, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + }, + unmappedValue: { + '__unknown__': 1 + } + }); + }); + + it('should be called on unmapped term with context [2]', async () => { const docWithUnMappedTerm = { '@context': { 'definedTerm': 'https://example.com#definedTerm' From c96bd594d4f62decd9c25c758938579452b3a2e1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 02:49:47 -0400 Subject: [PATCH 060/181] Update style. --- tests/misc.js | 490 +++++++++++++++++++++++++++----------------------- 1 file changed, 269 insertions(+), 221 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index f202a3a0..22766c03 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -948,11 +948,13 @@ describe.only('expansionMap', () => { // FIXME move to value section it.skip('should have zero counts with no terms', async () => { - const docWithNoTerms = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - } - }; + const docWithNoTerms = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + } +} +; const counts = {}; const expansionMap = info => { @@ -965,9 +967,11 @@ describe.only('expansionMap', () => { }); it.skip('should have zero counts with absolute term', async () => { - const docWithMappedTerm = { - 'urn:definedTerm': "is defined" - }; + const docWithMappedTerm = +{ + "urn:definedTerm": "is defined" +} +; const counts = {}; const expansionMap = info => { @@ -980,12 +984,14 @@ describe.only('expansionMap', () => { }); it.skip('should have zero counts with mapped term', async () => { - const docWithMappedTerm = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - definedTerm: "is defined" - }; + const docWithMappedTerm = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "definedTerm": "is defined" +} +; const counts = {}; const expansionMap = info => { @@ -998,9 +1004,10 @@ describe.only('expansionMap', () => { }); it.skip('should be called on unmapped term with no context', async () => { - const docWithUnMappedTerm = { - testUndefined: "is undefined" - }; + const docWithUnMappedTerm = +{ + "testUndefined": 'is undefined' +}; const counts = {}; const expansionMap = info => { @@ -1021,12 +1028,14 @@ describe.only('expansionMap', () => { }); it('should be called on unmapped term with context [1]', async () => { - const docWithUnMappedTerm = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - testUndefined: "is undefined" - }; + const docWithUnMappedTerm = +{ + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + "testUndefined": 'is undefined' +} +; const counts = {}; const expansionMap = info => { @@ -1050,13 +1059,15 @@ describe.only('expansionMap', () => { }); it('should be called on unmapped term with context [2]', async () => { - const docWithUnMappedTerm = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - definedTerm: "is defined", - testUndefined: "is undefined" - }; + const docWithUnMappedTerm = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "definedTerm": "is defined", + "testUndefined": "is undefined" +} +; const counts = {}; const expansionMap = info => { @@ -1077,14 +1088,16 @@ describe.only('expansionMap', () => { }); it('should be called on nested unmapped term', async () => { - const docWithUnMappedTerm = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - definedTerm: { - testUndefined: "is undefined" - } - }; + const docWithUnMappedTerm = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "definedTerm": { + "testUndefined": "is undefined" + } +} +; const counts = {}; const expansionMap = info => { @@ -1106,14 +1119,16 @@ describe.only('expansionMap', () => { }); describe('relativeIri', () => { - it('should be called on relative iri for id term', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - '@id': "relativeiri", - definedTerm: "is defined" - }; + it('should be called on relative IRI for id term', async () => { + const docWithRelativeIriId = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "@id": "relativeiri", + "definedTerm": "is defined" +} +; const counts = {}; const expansionMap = info => { @@ -1133,16 +1148,18 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for id term (nested)', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - '@id': "urn:absoluteIri", - definedTerm: { - '@id': "relativeiri" - } - }; + it('should be called on relative IRI for id term (nested)', async () => { + const docWithRelativeIriId = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "@id": "urn:absoluteIri", + "definedTerm": { + "@id": "relativeiri" + } +} +; const counts = {}; const expansionMap = info => { @@ -1162,15 +1179,17 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for aliased id term', async () => { - const docWithRelativeIriId = { - '@context': { - 'id': '@id', - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "relativeiri", - definedTerm: "is defined" - }; + it('should be called on relative IRI for aliased id term', async () => { + const docWithRelativeIriId = +{ + "@context": { + "id": "@id", + "definedTerm": "https://example.com#definedTerm" + }, + "id": "relativeiri", + "definedTerm": "is defined" +} +; const counts = {}; const expansionMap = info => { @@ -1190,15 +1209,17 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for type term', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - '@type': "relativeiri", - definedTerm: "is defined" - }; + it('should be called on relative IRI for type term', async () => { + const docWithRelativeIriId = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "id": "urn:absoluteiri", + "@type": "relativeiri", + "definedTerm": "is defined" +} +; const counts = {}; const expansionMap = info => { @@ -1222,24 +1243,26 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for type ' + + it('should be called on relative IRI for type ' + 'term in scoped context', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedType': { - '@id': 'https://example.com#definedType', - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - - } - } - }, - 'id': "urn:absoluteiri", - '@type': "definedType", - definedTerm: { - '@type': 'relativeiri' - } - }; + const docWithRelativeIriId = +{ + "@context": { + "definedType": { + "@id": "https://example.com#definedType", + "@context": { + "definedTerm": "https://example.com#definedTerm" + + } + } + }, + "id": "urn:absoluteiri", + "@type": "definedType", + "definedTerm": { + "@type": "relativeiri" + } +} +; const counts = {}; const expansionMap = info => { @@ -1263,16 +1286,18 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for ' + - 'type term with multiple relative iri types', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - '@type': ["relativeiri", "anotherRelativeiri" ], - definedTerm: "is defined" - }; + it('should be called on relative IRI for ' + + 'type term with multiple relative IRI types', async () => { + const docWithRelativeIriId = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "id": "urn:absoluteiri", + "@type": ["relativeiri", "anotherRelativeiri"], + "definedTerm": "is defined" +} +; const counts = {}; const expansionMap = info => { @@ -1298,25 +1323,26 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for ' + - 'type term with multiple relative iri types in scoped context' + + it('should be called on relative IRI for ' + + 'type term with multiple relative IRI types in scoped context' + '', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedType': { - '@id': 'https://example.com#definedType', - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - - } - } - }, - 'id': "urn:absoluteiri", - '@type': "definedType", - definedTerm: { - '@type': ["relativeiri", "anotherRelativeiri" ] - } - }; + const docWithRelativeIriId = +{ + "@context": { + "definedType": { + "@id": "https://example.com#definedType", + "@context": { + "definedTerm": "https://example.com#definedTerm" + } + } + }, + "id": "urn:absoluteiri", + "@type": "definedType", + "definedTerm": { + "@type": ["relativeiri", "anotherRelativeiri" ] + } +} +; const counts = {}; const expansionMap = info => { @@ -1342,16 +1368,18 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for ' + + it('should be called on relative IRI for ' + 'type term with multiple types', async () => { - const docWithRelativeIriId = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - '@type': ["relativeiri", "definedTerm" ], - definedTerm: "is defined" - }; + const docWithRelativeIriId = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + }, + "id": "urn:absoluteiri", + "@type": ["relativeiri", "definedTerm"], + "definedTerm": "is defined" +} +; const counts = {}; const expansionMap = info => { @@ -1375,16 +1403,17 @@ describe.only('expansionMap', () => { }); }); - it('should be called on relative iri for aliased type term', async () => { - const docWithRelativeIriId = { - '@context': { - 'type': "@type", - 'definedTerm': 'https://example.com#definedTerm' - }, - 'id': "urn:absoluteiri", - 'type': "relativeiri", - definedTerm: "is defined" - }; + it('should be called on relative IRI for aliased type term', async () => { + const docWithRelativeIriId = +{ + "@context": { + "type": "@type", + "definedTerm": "https://example.com#definedTerm" + }, + "id": "urn:absoluteiri", + "type": "relativeiri", + "definedTerm": "is defined" +}; const counts = {}; const expansionMap = info => { @@ -1408,14 +1437,16 @@ describe.only('expansionMap', () => { }); }); - it("should be called on relative iri when " + - "@base value is './'", async () => { - const docWithRelativeIriId = { - '@context': { - "@base": "./", - }, - '@id': "relativeiri", - }; + it('should be called on relative IRI when ' + + '@base value is `./`', async () => { + const docWithRelativeIriId = +{ + "@context": { + "@base": "./" + }, + "@id": "relativeiri" +} +; const counts = {}; const expansionMap = info => { @@ -1438,14 +1469,16 @@ describe.only('expansionMap', () => { }); }); - it("should be called on relative iri when " + - "@base value is './'", async () => { - const docWithRelativeIriId = { - '@context': { - "@base": "./", - }, - '@id': "relativeiri", - }; + it('should be called on relative IRI when ' + + '@base value is `./`', async () => { + const docWithRelativeIriId = +{ + "@context": { + "@base": "./" + }, + "@id": "relativeiri" +} +; const counts = {}; const expansionMap = info => { @@ -1468,14 +1501,16 @@ describe.only('expansionMap', () => { }); }); - it("should be called on relative iri when " + - "@vocab value is './'", async () => { - const docWithRelativeIriId = { - '@context': { - "@vocab": "./", - }, - '@type': "relativeiri", - }; + it('should be called on relative IRI when ' + + '`@vocab` value is `./`', async () => { + const docWithRelativeIriId = +{ + "@context": { + "@vocab": "./" + }, + "@type": "relativeiri" +} +; const counts = {}; const expansionMap = info => { @@ -1499,14 +1534,15 @@ describe.only('expansionMap', () => { }); describe('prependedIri', () => { - it("should be called when property is " + - "being expanded with `@vocab`", async () => { - const doc = { - '@context': { - "@vocab": "http://example.com/", - }, - 'term': "termValue", - }; + it('should be called when property is ' + + 'being expanded with `@vocab`', async () => { + const doc = +{ + "@context": { + "@vocab": "http://example.com/" + }, + "term": "termValue" +}; const counts = {}; const expansionMap = info => { @@ -1530,14 +1566,16 @@ describe.only('expansionMap', () => { }); }); - it("should be called when '@type' is " + - "being expanded with `@vocab`", async () => { - const doc = { - '@context': { - "@vocab": "http://example.com/", - }, - '@type': "relativeIri", - }; + it('should be called when `@type` is ' + + 'being expanded with `@vocab`', async () => { + const doc = +{ + "@context": { + "@vocab": "http://example.com/" + }, + "@type": "relativeIri" +} +; const counts = {}; const expansionMap = info => { @@ -1561,15 +1599,17 @@ describe.only('expansionMap', () => { }); }); - it("should be called when aliased '@type' is " + - "being expanded with `@vocab`", async () => { - const doc = { - '@context': { - "@vocab": "http://example.com/", - "type": "@type" - }, - 'type': "relativeIri", - }; + it('should be called when aliased `@type` is ' + + 'being expanded with `@vocab`', async () => { + const doc = +{ + "@context": { + "@vocab": "http://example.com/", + "type": "@type" + }, + "type": "relativeIri" +} +; const counts = {}; const expansionMap = info => { @@ -1593,14 +1633,16 @@ describe.only('expansionMap', () => { }); }); - it("should be called when '@id' is being " + - "expanded with `@base`", async () => { - const doc = { - '@context': { - "@base": "http://example.com/", - }, - '@id': "relativeIri", - }; + it('should be called when `@id` is being ' + + 'expanded with `@base`', async () => { + const doc = +{ + "@context": { + "@base": "http://example.com/" + }, + "@id": "relativeIri" +} +; const counts = {}; const expansionMap = info => { @@ -1629,15 +1671,17 @@ describe.only('expansionMap', () => { }); }); - it("should be called when aliased '@id' " + - "is being expanded with `@base`", async () => { - const doc = { - '@context': { - "@base": "http://example.com/", - "id": "@id" - }, - 'id': "relativeIri", - }; + it('should be called when aliased `@id` ' + + 'is being expanded with `@base`', async () => { + const doc = +{ + "@context": { + "@base": "http://example.com/", + "id": "@id" + }, + "id": "relativeIri" +} +; const counts = {}; const expansionMap = info => { @@ -1666,14 +1710,16 @@ describe.only('expansionMap', () => { }); }); - it("should be called when '@type' is " + - "being expanded with `@base`", async () => { - const doc = { - '@context': { - "@base": "http://example.com/", - }, - '@type': "relativeIri", - }; + it('should be called when `@type` is ' + + 'being expanded with `@base`', async () => { + const doc = +{ + "@context": { + "@base": "http://example.com/" + }, + "@type": "relativeIri" +} +; const counts = {}; const expansionMap = info => { @@ -1702,15 +1748,17 @@ describe.only('expansionMap', () => { }); }); - it("should be called when aliased '@type' is " + - "being expanded with `@base`", async () => { - const doc = { - '@context': { - "@base": "http://example.com/", - "type": "@type" - }, - 'type': "relativeIri", - }; + it('should be called when aliased `@type` is ' + + 'being expanded with `@base`', async () => { + const doc = +{ + "@context": { + "@base": "http://example.com/", + "type": "@type" + }, + "type": "relativeIri" +} +; const counts = {}; const expansionMap = info => { From d3899b6468f24ff0c01f597db706c23f3b829ce4 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 21:02:28 -0400 Subject: [PATCH 061/181] Add expansion warnings events. - Add 'relateive IRI after expansion' warning event. - Add 'invalid property expansion' warning event. - Update tests to check call counts. --- lib/context.js | 13 +- lib/expand.js | 12 + tests/misc.js | 616 +++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 515 insertions(+), 126 deletions(-) diff --git a/lib/context.js b/lib/context.js index acece63e..38b35b64 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1078,7 +1078,6 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { activeCtx, options }); - } if(expansionMapResult !== undefined) { value = expansionMapResult; @@ -1144,6 +1143,18 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { }); if(expandedResult !== undefined) { value = expandedResult; + } else { + _handleEvent({ + event: { + code: 'relative IRI after expansion', + level: 'warning', + message: 'Expansion resulted in a relative IRI.', + details: { + value + } + }, + options + }); } } diff --git a/lib/expand.js b/lib/expand.js index 0f64aec3..1be64721 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -464,6 +464,18 @@ async function _expandObject({ expandedParent }); if(expandedProperty === undefined) { + _handleEvent({ + event: { + code: 'invalid property expansion', + level: 'warning', + message: 'Invalid expansion for property.', + details: { + // FIXME: include expandedProperty before mapping + property: key + } + }, + options + }); continue; } } diff --git a/tests/misc.js b/tests/misc.js index 22766c03..61ce5445 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -931,23 +931,33 @@ describe.only('expansionMap', () => { } } - describe('unmappedProperty', () => { + describe('unmappedValue', () => { // FIXME move to value section - it.skip('should have zero counts with empty input', async () => { + it('should have zero counts with empty input', async () => { const docWithNoContent = {}; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithNoContent, {expansionMap}); + await jsonld.expand(docWithNoContent, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, {}); + assert.deepStrictEqual(mapCounts, { + expansionMap: 1, + unmappedValue: { + '__unknown__': 1 + } + }); + console.error('FIXME'); + assert.deepStrictEqual(eventCounts, {}); }); - // FIXME move to value section - it.skip('should have zero counts with no terms', async () => { + it('should have zero counts with no terms', async () => { const docWithNoTerms = { "@context": { @@ -956,34 +966,52 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithNoTerms, {expansionMap}); + await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, {}); + assert.deepStrictEqual(mapCounts, { + expansionMap: 1, + unmappedValue: { + '__unknown__': 1 + } + }); + console.error('FIXME'); + assert.deepStrictEqual(eventCounts, {}); }); + }); - it.skip('should have zero counts with absolute term', async () => { + describe('unmappedProperty', () => { + it('should have zero counts with absolute term', async () => { const docWithMappedTerm = { "urn:definedTerm": "is defined" } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithMappedTerm, {expansionMap}); + await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, {}); + assert.deepStrictEqual(mapCounts, {}); + assert.deepStrictEqual(eventCounts, {}); }); - it.skip('should have zero counts with mapped term', async () => { + it('should have zero counts with mapped term', async () => { const docWithMappedTerm = { "@context": { @@ -993,58 +1021,81 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithMappedTerm, {expansionMap}); + await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, {}); + assert.deepStrictEqual(mapCounts, {}); + assert.deepStrictEqual(eventCounts, {}); }); - it.skip('should be called on unmapped term with no context', async () => { + it('should be called on unmapped term with no context', async () => { const docWithUnMappedTerm = { - "testUndefined": 'is undefined' + "testUndefined": "is undefined" }; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { - expansionMap: 3, + assert.deepStrictEqual(mapCounts, { + expansionMap: 4, relativeIri: { testUndefined: 2 }, unmappedProperty: { testUndefined: 1 + }, + unmappedValue: { + '__unknown__': 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 + }); }); it('should be called on unmapped term with context [1]', async () => { const docWithUnMappedTerm = { - '@context': { - 'definedTerm': 'https://example.com#definedTerm' + "@context": { + "definedTerm": "https://example.com#definedTerm" }, - "testUndefined": 'is undefined' + "testUndefined": "is undefined" } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 4, relativeIri: { testUndefined: 2 @@ -1056,6 +1107,13 @@ describe.only('expansionMap', () => { '__unknown__': 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 + }); }); it('should be called on unmapped term with context [2]', async () => { @@ -1069,14 +1127,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 3, relativeIri: { testUndefined: 2 @@ -1085,6 +1147,13 @@ describe.only('expansionMap', () => { testUndefined: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 + }); }); it('should be called on nested unmapped term', async () => { @@ -1099,14 +1168,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 3, relativeIri: { testUndefined: 2 @@ -1115,11 +1188,92 @@ describe.only('expansionMap', () => { testUndefined: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 + }); }); }); describe('relativeIri', () => { - it('should be called on relative IRI for id term', async () => { + it('should be called on relative IRI for id term [1]', async () => { + const docWithRelativeIriId = +{ + "@id": "relativeiri" +} +; + + const mapCounts = {}; + const expansionMap = info => { + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); + + assert.deepStrictEqual(mapCounts, { + expansionMap: 3, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + }, + unmappedValue: { + relativeiri: 1 + } + }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 + }); + }); + + it('should be called on relative IRI for id term [2]', async () => { + const docWithRelativeIriId = +{ + "@id": "relativeiri", + "urn:test": "value" +} +; + + const mapCounts = {}; + const expansionMap = info => { + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); + + assert.deepStrictEqual(mapCounts, { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } + }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 + }); + }); + + it('should be called on relative IRI for id term [3]', async () => { const docWithRelativeIriId = { "@context": { @@ -1130,14 +1284,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeiri: 1 @@ -1146,6 +1304,12 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 + }); }); it('should be called on relative IRI for id term (nested)', async () => { @@ -1161,14 +1325,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeiri: 1 @@ -1177,6 +1345,12 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 + }); }); it('should be called on relative IRI for aliased id term', async () => { @@ -1191,14 +1365,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeiri: 1 @@ -1207,6 +1385,12 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 + }); }); it('should be called on relative IRI for type term', async () => { @@ -1221,14 +1405,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1241,6 +1429,13 @@ describe.only('expansionMap', () => { id: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 + }); }); it('should be called on relative IRI for type ' + @@ -1264,14 +1459,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1284,6 +1483,13 @@ describe.only('expansionMap', () => { id: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 + }); }); it('should be called on relative IRI for ' + @@ -1299,14 +1505,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 8, prependedIri: { anotherRelativeiri: 1, @@ -1321,6 +1531,13 @@ describe.only('expansionMap', () => { id: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 5 + }, + events: 6 + }); }); it('should be called on relative IRI for ' + @@ -1344,14 +1561,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 8, prependedIri: { anotherRelativeiri: 1, @@ -1366,6 +1587,13 @@ describe.only('expansionMap', () => { id: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 5 + }, + events: 6 + }); }); it('should be called on relative IRI for ' + @@ -1381,14 +1609,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1401,6 +1633,13 @@ describe.only('expansionMap', () => { id: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 + }); }); it('should be called on relative IRI for aliased type term', async () => { @@ -1415,14 +1654,18 @@ describe.only('expansionMap', () => { "definedTerm": "is defined" }; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1435,38 +1678,55 @@ describe.only('expansionMap', () => { id: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 + }); }); it('should be called on relative IRI when ' + - '@base value is `./`', async () => { + '@base value is `null`', async () => { const docWithRelativeIriId = { "@context": { - "@base": "./" + "@base": null }, "@id": "relativeiri" } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 3, prependedIri: { 'relativeiri': 1 }, relativeIri: { - '/relativeiri': 1 + 'relativeiri': 1 }, unmappedValue: { - '/relativeiri': 1 + 'relativeiri': 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 + }); }); it('should be called on relative IRI when ' + @@ -1480,14 +1740,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 3, prependedIri: { relativeiri: 1 @@ -1499,6 +1763,51 @@ describe.only('expansionMap', () => { '/relativeiri': 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 + }); + }); + + it('should be called on relative IRI when ' + + '`@vocab` value is `null`', async () => { + const docWithRelativeIriId = +{ + "@context": { + "@vocab": null + }, + "@type": "relativeiri" +} +; + + const mapCounts = {}; + const expansionMap = info => { + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); + + assert.deepStrictEqual(mapCounts, { + expansionMap: 3, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + 'relativeiri': 2 + } + }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 2 + }, + events: 2 + }); }); it('should be called on relative IRI when ' + @@ -1512,14 +1821,18 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); + }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); }; - await jsonld.expand(docWithRelativeIriId, {expansionMap}); + await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 6, prependedIri: { './': 1, @@ -1530,6 +1843,12 @@ describe.only('expansionMap', () => { '/relativeiri': 2 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 3 + }, + events: 3 + }); }); }); @@ -1544,9 +1863,9 @@ describe.only('expansionMap', () => { "term": "termValue" }; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -1555,15 +1874,20 @@ describe.only('expansionMap', () => { result: 'http://example.com/term' }); }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; - await jsonld.expand(doc, {expansionMap}); + await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 4, prependedIri: { term: 4 } }); + assert.deepStrictEqual(eventCounts, {}); }); it('should be called when `@type` is ' + @@ -1577,9 +1901,9 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -1588,15 +1912,20 @@ describe.only('expansionMap', () => { result: 'http://example.com/relativeIri' }); }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; - await jsonld.expand(doc, {expansionMap}); + await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeIri: 2 } }); + assert.deepStrictEqual(eventCounts, {}); }); it('should be called when aliased `@type` is ' + @@ -1611,9 +1940,9 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -1622,15 +1951,20 @@ describe.only('expansionMap', () => { result: 'http://example.com/relativeIri' }); }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; - await jsonld.expand(doc, {expansionMap}); + await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeIri: 2 } }); + assert.deepStrictEqual(eventCounts, {}); }); it('should be called when `@id` is being ' + @@ -1644,9 +1978,9 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -1657,10 +1991,14 @@ describe.only('expansionMap', () => { }); } }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; - await jsonld.expand(doc, {expansionMap}); + await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeIri: 1 @@ -1669,6 +2007,7 @@ describe.only('expansionMap', () => { 'http://example.com/relativeIri': 1 } }); + assert.deepStrictEqual(eventCounts, {}); }); it('should be called when aliased `@id` ' + @@ -1683,9 +2022,9 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -1696,10 +2035,14 @@ describe.only('expansionMap', () => { }); } }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; - await jsonld.expand(doc, {expansionMap}); + await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeIri: 1 @@ -1708,6 +2051,7 @@ describe.only('expansionMap', () => { 'http://example.com/relativeIri': 1 } }); + assert.deepStrictEqual(eventCounts, {}); }); it('should be called when `@type` is ' + @@ -1721,9 +2065,9 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -1734,10 +2078,14 @@ describe.only('expansionMap', () => { }); } }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; - await jsonld.expand(doc, {expansionMap}); + await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeIri: 1 @@ -1746,6 +2094,13 @@ describe.only('expansionMap', () => { relativeIri: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1, + //FIXME: true + }); }); it('should be called when aliased `@type` is ' + @@ -1760,9 +2115,9 @@ describe.only('expansionMap', () => { } ; - const counts = {}; + const mapCounts = {}; const expansionMap = info => { - addCounts(counts, info); + addCounts(mapCounts, info); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -1773,10 +2128,14 @@ describe.only('expansionMap', () => { }); } }; + const eventCounts = {}; + const eventHandler = ({event}) => { + addEventCounts(eventCounts, event); + }; - await jsonld.expand(doc, {expansionMap}); + await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(mapCounts, { expansionMap: 2, prependedIri: { relativeIri: 1 @@ -1785,6 +2144,13 @@ describe.only('expansionMap', () => { relativeIri: 1 } }); + assert.deepStrictEqual(eventCounts, { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1, + //FIXME: true + }); }); }); }); From 0f18f13d993d91c1c6a1667a15f02f8d77b8e25d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 22:50:19 -0400 Subject: [PATCH 062/181] Event handler improvements. - Add basic event handlers (can be used to construct more complex handlers) - log all events and continue - fail on unhandled events - fail on unacceptable events - log warnings and continue - strict mode to fail on all current warnings - Add default event handler feature. --- lib/events.js | 79 +++++++++++++++++++++++++++++++++++++++++++++------ lib/jsonld.js | 20 +++++++++++++ tests/misc.js | 33 +++++++++++++++++++-- 3 files changed, 121 insertions(+), 11 deletions(-) diff --git a/lib/events.js b/lib/events.js index 0061a9cd..bcdeeaea 100644 --- a/lib/events.js +++ b/lib/events.js @@ -16,6 +16,8 @@ const { const api = {}; module.exports = api; +let _defaultEventHandler = []; + /** * Handle an event. * @@ -42,7 +44,7 @@ api.handleEvent = ({ }) => { const handlers = [].concat( options.eventHandler ? _asArray(options.eventHandler) : [], - _defaultHandler + ..._defaultEventHandler ); _handle({event, handlers}); }; @@ -76,17 +78,76 @@ function _handle({event, handlers}) { return doNext; } -function _defaultHandler({event}) { +// logs all events and continues +api.logEventHandler = function({event, next}) { + console.warn(`EVENT: ${event.message}`, { + code: event.code, + level: event.level, + details: event.details + }); + next(); +}; + +// fallback to throw errors for any unhandled events +api.unhandledEventHandler = function({event}) { + throw new JsonLdError( + 'No handler for event.', + 'jsonld.UnhandledEvent', + {event}); +}; + +// throw with event details +api.throwUnacceptableEventHandler = function({event}) { + throw new JsonLdError( + event.message, + 'jsonld.UnacceptableEvent', + {event}); +}; + +// log 'warning' level events +api.logWarningEventHandler = function({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}); -} +}; + +// strict handler that rejects warning conditions +api.strictModeEventHandler = [ + function({event, next}) { + // fail on all warnings + if(event.level === 'warning') { + throw new JsonLdError( + event.message, + 'jsonld.StrictModeViolationEvent', + {event}); + } + next(); + }, + // fail on unhandled events + // TODO: update when events are added that are acceptable in strict mode + api.unhandledEventHandler +]; + +// log warnings to console or fail +api.basicEventHandler = [ + api.logWarningEventHandler, + api.unhandledEventHandler +]; + +/** + * Set default event handler. + * + * By default, all event are unhandled. It is recommended to pass in an + * eventHandler into each call. However, this call allows using a default + * eventHandler when one is not otherwise provided. + * + * @param {object} options - default handler options: + * {function|object|array} eventHandler - a default event handler. + * falsey to unset. + */ +api.setDefaultEventHandler = function({eventHandler} = {}) { + _defaultEventHandler = eventHandler ? _asArray(eventHandler) : []; +}; diff --git a/lib/jsonld.js b/lib/jsonld.js index f45fc68c..a1e80004 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -80,6 +80,16 @@ const { mergeNodeMaps: _mergeNodeMaps } = require('./nodeMap'); +const { + basicEventHandler: _basicEventHandler, + logEventHandler: _logEventHandler, + logWarningEventHandler: _logWarningEventHandler, + setDefaultEventHandler: _setDefaultEventHandler, + strictModeEventHandler: _strictModeEventHandler, + throwUnacceptableEventHandler: _throwUnacceptableEventHandler, + unhandledEventHandler: _unhandledEventHandler +} = require('./events'); + /* eslint-disable indent */ // attaches jsonld API to the given object const wrapper = function(jsonld) { @@ -997,6 +1007,16 @@ jsonld.registerRDFParser('application/nquads', NQuads.parse); /* URL API */ jsonld.url = require('./url'); +/* Events API and handlers */ +jsonld.setDefaultEventHandler = _setDefaultEventHandler; +jsonld.basicEventHandler = _basicEventHandler; +jsonld.logEventHandler = _logEventHandler; +jsonld.logWarningEventHandler = _logWarningEventHandler; +jsonld.setDefaultEventHandler = _setDefaultEventHandler; +jsonld.strictModeEventHandler = _strictModeEventHandler; +jsonld.throwUnacceptableEventHandler = _throwUnacceptableEventHandler; +jsonld.unhandledEventHandler = _unhandledEventHandler; + /* Utility API */ jsonld.util = util; // backwards compatibility diff --git a/tests/misc.js b/tests/misc.js index 61ce5445..9b6713fd 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -493,8 +493,37 @@ function addEventCounts(counts, event) { } describe.only('events', () => { - // FIXME add default handler tests - // FIXME add object '*' handler tests + // FIXME/TODO add object '*' handler and tests? + + it('check default handler called', async () => { + const d = +{ + "relative": "test" +} +; + const ex = []; + + const counts = {}; + const eventHandler = ({event}) => { + addEventCounts(counts, event); + }; + + jsonld.setDefaultEventHandler({eventHandler}); + + const e = await jsonld.expand(d); + + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(counts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 + }); + + // reset default + jsonld.setDefaultEventHandler(); + }); it('handle warning event with function', async () => { const d = From 61b67ecdca97f1bd45bf01b33d81061a2bd6509c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 22:51:10 -0400 Subject: [PATCH 063/181] Fix error messages. --- lib/events.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/events.js b/lib/events.js index bcdeeaea..4299abc1 100644 --- a/lib/events.js +++ b/lib/events.js @@ -99,7 +99,7 @@ api.unhandledEventHandler = function({event}) { // throw with event details api.throwUnacceptableEventHandler = function({event}) { throw new JsonLdError( - event.message, + 'Unacceptable event occurred.', 'jsonld.UnacceptableEvent', {event}); }; @@ -120,7 +120,7 @@ api.strictModeEventHandler = [ // fail on all warnings if(event.level === 'warning') { throw new JsonLdError( - event.message, + 'Strict mode violation occurred.', 'jsonld.StrictModeViolationEvent', {event}); } From 14f882a0f9a967cc91dae9255b19fcb460cd5e35 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 23:14:45 -0400 Subject: [PATCH 064/181] Track test events. Allow checking log of events. Adds more detailed checking to ensure the proper sequence of events and details occur. --- tests/misc.js | 218 +++++++++++++++++++++++++++++--------------------- 1 file changed, 128 insertions(+), 90 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index 9b6713fd..be440d27 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -492,6 +492,21 @@ function addEventCounts(counts, event) { counts.codes[event.code]++; } +// track event and counts +// use simple count object (don't use tricky test keys!) +function trackEvent({events, event}) { + events.counts = events.counts || {}; + events.log = events.log || []; + + addEventCounts(events.counts, event); + // just log useful comparison details + events.log.push({ + code: event.code, + level: event.level, + details: event.details + }); +} + describe.only('events', () => { // FIXME/TODO add object '*' handler and tests? @@ -969,9 +984,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithNoContent, {expansionMap, eventHandler}); @@ -983,7 +998,7 @@ describe.only('expansionMap', () => { } }); console.error('FIXME'); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should have zero counts with no terms', async () => { @@ -999,9 +1014,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); @@ -1013,7 +1028,7 @@ describe.only('expansionMap', () => { } }); console.error('FIXME'); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); }); @@ -1029,15 +1044,15 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); assert.deepStrictEqual(mapCounts, {}); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should have zero counts with mapped term', async () => { @@ -1054,15 +1069,15 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); assert.deepStrictEqual(mapCounts, {}); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should be called on unmapped term with no context', async () => { @@ -1075,9 +1090,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); @@ -1094,13 +1109,36 @@ describe.only('expansionMap', () => { '__unknown__': 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 2 }, events: 3 }); + assert.deepStrictEqual(events.log, [ + { + code: 'relative IRI after expansion', + details: { + value: 'testUndefined' + }, + level: 'warning' + }, + { + code: 'relative IRI after expansion', + details: { + value: 'testUndefined' + }, + level: 'warning' + }, + { + code: 'invalid property expansion', + details: { + property: 'testUndefined' + }, + level: 'warning' + } + ]); }); it('should be called on unmapped term with context [1]', async () => { @@ -1117,9 +1155,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); @@ -1136,7 +1174,7 @@ describe.only('expansionMap', () => { '__unknown__': 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 2 @@ -1160,9 +1198,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); @@ -1176,7 +1214,7 @@ describe.only('expansionMap', () => { testUndefined: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 2 @@ -1201,9 +1239,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); @@ -1217,7 +1255,7 @@ describe.only('expansionMap', () => { testUndefined: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 2 @@ -1239,9 +1277,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1258,7 +1296,7 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -1278,9 +1316,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1294,7 +1332,7 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -1317,9 +1355,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1333,7 +1371,7 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -1358,9 +1396,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1374,7 +1412,7 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -1398,9 +1436,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1414,7 +1452,7 @@ describe.only('expansionMap', () => { relativeiri: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -1438,9 +1476,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1458,7 +1496,7 @@ describe.only('expansionMap', () => { id: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 4 @@ -1492,9 +1530,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1512,7 +1550,7 @@ describe.only('expansionMap', () => { id: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 4 @@ -1538,9 +1576,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1560,7 +1598,7 @@ describe.only('expansionMap', () => { id: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 5 @@ -1594,9 +1632,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1616,7 +1654,7 @@ describe.only('expansionMap', () => { id: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 5 @@ -1642,9 +1680,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1662,7 +1700,7 @@ describe.only('expansionMap', () => { id: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 4 @@ -1687,9 +1725,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1707,7 +1745,7 @@ describe.only('expansionMap', () => { id: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'invalid property expansion': 1, 'relative IRI after expansion': 4 @@ -1731,9 +1769,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1750,7 +1788,7 @@ describe.only('expansionMap', () => { 'relativeiri': 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -1773,9 +1811,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1792,7 +1830,7 @@ describe.only('expansionMap', () => { '/relativeiri': 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -1815,9 +1853,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1831,7 +1869,7 @@ describe.only('expansionMap', () => { 'relativeiri': 2 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 2 }, @@ -1854,9 +1892,9 @@ describe.only('expansionMap', () => { const expansionMap = info => { addCounts(mapCounts, info); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); @@ -1872,7 +1910,7 @@ describe.only('expansionMap', () => { '/relativeiri': 2 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 3 }, @@ -1903,9 +1941,9 @@ describe.only('expansionMap', () => { result: 'http://example.com/term' }); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(doc, {expansionMap, eventHandler}); @@ -1916,7 +1954,7 @@ describe.only('expansionMap', () => { term: 4 } }); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should be called when `@type` is ' + @@ -1941,9 +1979,9 @@ describe.only('expansionMap', () => { result: 'http://example.com/relativeIri' }); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(doc, {expansionMap, eventHandler}); @@ -1954,7 +1992,7 @@ describe.only('expansionMap', () => { relativeIri: 2 } }); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should be called when aliased `@type` is ' + @@ -1980,9 +2018,9 @@ describe.only('expansionMap', () => { result: 'http://example.com/relativeIri' }); }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(doc, {expansionMap, eventHandler}); @@ -1993,7 +2031,7 @@ describe.only('expansionMap', () => { relativeIri: 2 } }); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should be called when `@id` is being ' + @@ -2020,9 +2058,9 @@ describe.only('expansionMap', () => { }); } }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(doc, {expansionMap, eventHandler}); @@ -2036,7 +2074,7 @@ describe.only('expansionMap', () => { 'http://example.com/relativeIri': 1 } }); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should be called when aliased `@id` ' + @@ -2064,9 +2102,9 @@ describe.only('expansionMap', () => { }); } }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(doc, {expansionMap, eventHandler}); @@ -2080,7 +2118,7 @@ describe.only('expansionMap', () => { 'http://example.com/relativeIri': 1 } }); - assert.deepStrictEqual(eventCounts, {}); + assert.deepStrictEqual(events, {}); }); it('should be called when `@type` is ' + @@ -2107,9 +2145,9 @@ describe.only('expansionMap', () => { }); } }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(doc, {expansionMap, eventHandler}); @@ -2123,7 +2161,7 @@ describe.only('expansionMap', () => { relativeIri: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, @@ -2157,9 +2195,9 @@ describe.only('expansionMap', () => { }); } }; - const eventCounts = {}; + const events = {}; const eventHandler = ({event}) => { - addEventCounts(eventCounts, event); + trackEvent({events, event}); }; await jsonld.expand(doc, {expansionMap, eventHandler}); @@ -2173,7 +2211,7 @@ describe.only('expansionMap', () => { relativeIri: 1 } }); - assert.deepStrictEqual(eventCounts, { + assert.deepStrictEqual(events.counts, { codes: { 'relative IRI after expansion': 1 }, From 06aae7de80bc4f284e0cc03c7a50419b088a87a2 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:09:03 -0400 Subject: [PATCH 065/181] Event updates and add safe mode. - Add `safe` flag to call a special safe mode event handler. - Update events API and usage. - Add `hasEventHandler` for call site optimization. - Update handlers. --- lib/events.js | 110 ++++++++++++++++++++++++++------------------------ lib/jsonld.js | 16 +++++--- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/lib/events.js b/lib/events.js index 4299abc1..eb34bc2d 100644 --- a/lib/events.js +++ b/lib/events.js @@ -16,7 +16,24 @@ const { const api = {}; module.exports = api; -let _defaultEventHandler = []; +// default handler, store as null or an array +// exposed to allow fast external pre-handleEvent() checks +api.defaultEventHandler = null; + +/** + * Check if event handler is in use in options or by default. + * + * This call is used to avoid building event structures and calling the main + * handleEvent() call. It checks if safe mode is on, an event handler is in the + * processing options, or a default handler was set. + * + * @param {object} options - processing options: + * {boolean} [safe] - flag for Safe Mode. + * {function|object|array} [eventHandler] - an event handler. + */ +api.hasEventHandler = options => { + return options.safe || options.eventHandler || api.defaultEventHandler; +}; /** * Handle an event. @@ -28,9 +45,6 @@ let _defaultEventHandler = []; * 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'] @@ -43,8 +57,10 @@ api.handleEvent = ({ options }) => { const handlers = [].concat( + // priority is safe mode handler, options handler, then default handler + options.safe ? api.safeModeEventHandler : [], options.eventHandler ? _asArray(options.eventHandler) : [], - ..._defaultEventHandler + api.defaultEventHandler ? api.defaultEventHandler : [] ); _handle({event, handlers}); }; @@ -78,64 +94,54 @@ function _handle({event, handlers}) { return doNext; } -// logs all events and continues -api.logEventHandler = function({event, next}) { - console.warn(`EVENT: ${event.message}`, { - code: event.code, - level: event.level, - details: event.details - }); +// safe handler that rejects unsafe warning conditions +api.safeModeEventHandler = function({event, next}) { + // fail on all unsafe warnings + if(event.level === 'warning' && event.tags.includes('unsafe')) { + throw new JsonLdError( + 'Safe mode violation.', + 'jsonld.SafeModeViolationEvent', + {event} + ); + } next(); }; -// fallback to throw errors for any unhandled events -api.unhandledEventHandler = function({event}) { - throw new JsonLdError( - 'No handler for event.', - 'jsonld.UnhandledEvent', - {event}); +// strict handler that rejects all warning conditions +api.strictModeEventHandler = function({event, next}) { + // fail on all warnings + if(event.level === 'warning') { + throw new JsonLdError( + 'Strict mode violation.', + 'jsonld.StrictModeViolationEvent', + {event} + ); + } + next(); }; -// throw with event details -api.throwUnacceptableEventHandler = function({event}) { - throw new JsonLdError( - 'Unacceptable event occurred.', - 'jsonld.UnacceptableEvent', - {event}); +// logs all events and continues +api.logEventHandler = function({event, next}) { + console.log(`EVENT: ${event.message}`, {event}); + next(); }; // log 'warning' level events -api.logWarningEventHandler = function({event}) { +api.logWarningEventHandler = function({event, next}) { if(event.level === 'warning') { - console.warn(`WARNING: ${event.message}`, { - code: event.code, - details: event.details - }); + console.warn(`WARNING: ${event.message}`, {event}); } + next(); }; -// strict handler that rejects warning conditions -api.strictModeEventHandler = [ - function({event, next}) { - // fail on all warnings - if(event.level === 'warning') { - throw new JsonLdError( - 'Strict mode violation occurred.', - 'jsonld.StrictModeViolationEvent', - {event}); - } - next(); - }, - // fail on unhandled events - // TODO: update when events are added that are acceptable in strict mode - api.unhandledEventHandler -]; - -// log warnings to console or fail -api.basicEventHandler = [ - api.logWarningEventHandler, - api.unhandledEventHandler -]; +// fallback to throw errors for any unhandled events +api.unhandledEventHandler = function({event}) { + throw new JsonLdError( + 'No handler for event.', + 'jsonld.UnhandledEvent', + {event} + ); +}; /** * Set default event handler. @@ -149,5 +155,5 @@ api.basicEventHandler = [ * falsey to unset. */ api.setDefaultEventHandler = function({eventHandler} = {}) { - _defaultEventHandler = eventHandler ? _asArray(eventHandler) : []; + api.defaultEventHandler = eventHandler ? _asArray(eventHandler) : null; }; diff --git a/lib/jsonld.js b/lib/jsonld.js index a1e80004..502cdd26 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -4,7 +4,7 @@ * @author Dave Longley * * @license BSD 3-Clause License - * Copyright (c) 2011-2019 Digital Bazaar, Inc. + * Copyright (c) 2011-2022 Digital Bazaar, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -81,12 +81,10 @@ const { } = require('./nodeMap'); const { - basicEventHandler: _basicEventHandler, logEventHandler: _logEventHandler, logWarningEventHandler: _logWarningEventHandler, setDefaultEventHandler: _setDefaultEventHandler, strictModeEventHandler: _strictModeEventHandler, - throwUnacceptableEventHandler: _throwUnacceptableEventHandler, unhandledEventHandler: _unhandledEventHandler } = require('./events'); @@ -129,6 +127,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. + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -268,6 +267,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. + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -422,6 +422,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. + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -521,6 +522,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. + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -557,6 +559,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 + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -614,6 +617,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). + * [safe] true to use safe mode. * [eventHandler] handler for events. * * @return a Promise that resolves to the JSON-LD document. @@ -664,6 +668,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. + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -758,6 +763,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. + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -921,6 +927,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. + * [safe] true to use safe mode. * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -1008,13 +1015,10 @@ jsonld.registerRDFParser('application/nquads', NQuads.parse); jsonld.url = require('./url'); /* Events API and handlers */ -jsonld.setDefaultEventHandler = _setDefaultEventHandler; -jsonld.basicEventHandler = _basicEventHandler; jsonld.logEventHandler = _logEventHandler; jsonld.logWarningEventHandler = _logWarningEventHandler; jsonld.setDefaultEventHandler = _setDefaultEventHandler; jsonld.strictModeEventHandler = _strictModeEventHandler; -jsonld.throwUnacceptableEventHandler = _throwUnacceptableEventHandler; jsonld.unhandledEventHandler = _unhandledEventHandler; /* Utility API */ From 81e277553869783f0491e44a929f42b8e53eb934 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:09:49 -0400 Subject: [PATCH 066/181] Add events docs. - Marked as experimental. --- README.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/README.md b/README.md index ca71d8e0..df156978 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,166 @@ It is recommended to set a default `user-agent` header for Node.js applications. The default for the default Node.js document loader is `jsonld.js`. +### Events + +**WARNING**: This feature is **experimental** and the API, events, codes, +levels, and messages may change. + +Various events may occur during processing. The event handler system allows +callers to handle events as appropriate. Use cases can be as simple as logging +warnings, to displaying helpful UI hints, to failing on specific conditions. + +**Note**: By default no event handler is used. This is due to general +performance considerations and the impossibility of providing a default handler +that would work for all use cases. Event construction and the handling system +are avoided by default providing the best performance for use cases where data +quality is known events are unnecessary. + +#### Event Structure + +Events are basic JSON objects with the following properties: + +- **`code`**: A basic string code, similar to existing JSON-LD error codes. +- **`level`**: The severity level. Currently only `warning` is emitted. +- **`tags`**: Optional hints for the type of event. Currently defined: + - **`unsafe`**: Event is considered unsafe. + - **`lossy`**: Event is related to potential data loss. + - **`empty`**: Event is related to empty data structures. +- **`message`**: A human readable message describing the event. +- **`details`**: A JSON object with event specific details. + +#### Event Handlers + +Event handlers are chainable functions, arrays of handlers, objects mapping +codes to handlers, or any mix of these structures. Each function is passed an +object with two properties: + +- **`event`**: The event data. +- **`next`**: A function to call to an `event` and a `next`. + +The event handling system will process the handler structure, calling all +handlers, and continuing onto the next handler if `next()` is called. To stop +processing, throw an error, or return without calling `next()`. + +This design allows for composable handler structures, for instance to handle +some conditions with a custom handler, and default to generic "unknown event" +or logging handler. + +**Note**: Handlers are currently synchronous due to possible performance +issues. This may change to an `async`/`await` design in the future. + +```js +// expand a document with a logging event handler +const expanded = await jsonld.expand(data, { + // simple logging handler + eventHandler: function({event, next}) { + console.log('event', {event}); + } +}); +``` + +```js +function logEventHandler({event, next}) { + console.log('event', {event}); + next(); +} + +function noWarningsEventHandler({event, next}) { + if(event.level === 'warning') { + throw new Error('No warnings!', {event}); + } + next(); +} + +function unknownEventHandler({event, next}) { + throw new Error('Unknown event', {event}); +} + +// expand a document with an array of event handlers +const expanded = await jsonld.expand(data, { + // array of handlers + eventHandler: [ + logEventHandler, + noWarningsEventHandler, + unknownEventHandler + ]} +}); +``` + +```js +const handler = { + 'a mild event code': function({event}) { + console.log('the thing happened', {event}); + }, + 'a serious event code': function({event}) { + throw new Error('the specific thing happened', {event}); + } +}; +// expand a document with a code map event handler +const expanded = await jsonld.expand(data, {eventHandler}); +``` + +#### Safe Mode + +A common use case is to avoid JSON-LD constructs that will result in lossy +behavior. The JSON-LD specifications have notes about when data is dropped. +This can be especially important when calling [`canonize`][] in order to +digitally sign data. The event system can be used to detect and avoid these +situations. A special "safe mode" is available that will inject an initial +event handler that fails on conditions that would result in data loss. More +benign events may fall back to the passed event handler, if any. + +**Note**: This mode is designed to be the common way that digital signing and +similar applications use this library. + +The `safe` options flag set to `true` enables this behavior: + +```js +// expand a document in safe mode +const expanded = await jsonld.expand(data, {safe: true}); +``` + +```js +// expand a document in safe mode, with fallback handler +const expanded = await jsonld.expand(data, { + safe: true + eventHandler: function({event}) { /* ... */ } +}); +``` + +#### Available Handlers + +Some predefined event handlers are available to use alone or as part of a more +complex handler: + +- **safeModeEventHandler**: The handler used when `safe` is `true`. +- **strictModeEventHandler**: A handler that is more strict than the `safe` + handler and also fails on other detectable events related to poor input + structure. +- **logEventHandler**: A debugging handler that outputs to the console. +- **logWarningHandler**: A debugging handler that outputs `warning` level + events to the console. +- **unhandledEventHandler**: Throws on all events not yet handled. + +#### Default Event Handler + +A default event handler can be set. It will be the only handler when not in +safe mode, and the second handler when in safe mode. + +```js +// fail on unknown events +jsonld.setDefaultEventHandler(jsonld.unhandledEventHandler); +// will use safe mode handler, like `{safe: true}` +const expanded = await jsonld.expand(data); +``` + +```js +// always use safe mode event handler, ignore other events +jsonld.setDefaultEventHandler(jsonld.safeModeEventHandler); +// will use safe mode handler, like `{safe: true}` +const expanded = await jsonld.expand(data); +``` + Related Modules --------------- From ba41c1ba7344d0c7ae1b6ddaa464829d9b3127c1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:10:30 -0400 Subject: [PATCH 067/181] Style fix. --- lib/expand.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index 1be64721..4a0b9614 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -263,7 +263,8 @@ api.expand = async ({ insideList, typeKey, typeScopedContext, - expansionMap}); + expansionMap + }); // get property count on expanded output keys = Object.keys(rval); @@ -937,7 +938,8 @@ async function _expandObject({ insideList, typeScopedContext, typeKey, - expansionMap}); + expansionMap + }); } } } From f51f8e5e4b06c689b2526520a7783e4876753893 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:25:20 -0400 Subject: [PATCH 068/181] Add tests. --- tests/misc.js | 159 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index be440d27..d7bfc443 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -507,7 +507,7 @@ function trackEvent({events, event}) { }); } -describe.only('events', () => { +describe('events', () => { // FIXME/TODO add object '*' handler and tests? it('check default handler called', async () => { @@ -932,7 +932,7 @@ describe.only('events', () => { }); }); -describe.only('expansionMap', () => { +describe('expansionMap', () => { // track all the counts // use simple count object (don't use tricky test keys!) function addCounts(counts, info) { @@ -1030,6 +1030,161 @@ describe.only('expansionMap', () => { console.error('FIXME'); assert.deepStrictEqual(events, {}); }); + + it('should notify for @set free-floating scaler', async () => { + const docWithNoTerms = +{ + "@set": [ + "free-floating strings in set objects are removed", + { + "@id": "http://example.com/free-floating-node" + }, + { + "@id": "http://example.com/node", + "urn:property": "nodes with properties are not removed" + } + ] +} +; + const ex = +[ + { + "@id": "http://example.com/node", + "urn:property": [ + { + "@value": "nodes with properties are not removed" + } + ] + } +] +; + + const mapCounts = {}; + const expansionMap = info => { + addCounts(mapCounts, info); + }; + const events = {}; + const eventHandler = ({event}) => { + trackEvent({events, event}); + }; + + const e = await jsonld.expand(docWithNoTerms, { + expansionMap, eventHandler + }); + + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(mapCounts, { + expansionMap: 4, + unmappedValue: { + '__unknown__': 2, + 'http://example.com/free-floating-node': 2 + } + }); + console.error('FIXME'); + assert.deepStrictEqual(events, {}); + }); + + it('should notify for @list free-floating scaler', async () => { + const docWithNoTerms = +{ + "@list": [ + "free-floating strings in list objects are removed", + { + "@id": "http://example.com/free-floating-node" + }, + { + "@id": "http://example.com/node", + "urn:property": "nodes are removed with the @list" + } + ] +} +; + const ex = []; + + const mapCounts = {}; + const expansionMap = info => { + addCounts(mapCounts, info); + }; + const events = {}; + const eventHandler = ({event}) => { + trackEvent({events, event}); + }; + + const e = await jsonld.expand(docWithNoTerms, { + expansionMap, eventHandler + }); + + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(mapCounts, { + expansionMap: 5, + unmappedValue: { + '__unknown__': 3, + 'http://example.com/free-floating-node': 2 + } + }); + console.error('FIXME'); + assert.deepStrictEqual(events, {}); + }); + + it('should notify for null @value', async () => { + const docWithNoTerms = +{ + "urn:property": { + "@value": null + } +} +; + + const mapCounts = {}; + const expansionMap = info => { + addCounts(mapCounts, info); + }; + const events = {}; + const eventHandler = ({event}) => { + trackEvent({events, event}); + }; + + await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); + + assert.deepStrictEqual(mapCounts, { + expansionMap: 3, + unmappedValue: { + '__unknown__': 3 + } + }); + console.error('FIXME'); + assert.deepStrictEqual(events, {}); + }); + + it('should notify for @language alone', async () => { + const docWithNoTerms = +{ + "urn:property": { + "@language": "en" + } +} +; + + const mapCounts = {}; + const expansionMap = info => { + addCounts(mapCounts, info); + }; + const events = {}; + const eventHandler = ({event}) => { + trackEvent({events, event}); + }; + + await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); + + assert.deepStrictEqual(mapCounts, { + expansionMap: 3, + unmappedValue: { + '__unknown__': 2 + } + }); + console.error('FIXME'); + assert.deepStrictEqual(events, {}); + }); }); describe('unmappedProperty', () => { From ebbe3117016797c53ef3f3cf2a4bde73d7687ae6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 2 Apr 2022 17:39:38 -0400 Subject: [PATCH 069/181] Update event and expansionMap tests. Merge both sections to avoid duplication since they do, or will, cover similar situations. --- tests/misc.js | 909 +++++++++++++++++++++++++------------------------- 1 file changed, 462 insertions(+), 447 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index d7bfc443..15638a10 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -479,69 +479,126 @@ describe('literal JSON', () => { }); }); -// track all the event counts -// use simple count object (don't use tricky test keys!) -function addEventCounts(counts, event) { - // overall call counts - counts.events = counts.events || 0; - counts.codes = counts.codes || {}; +// test both events and expansionMaps +describe('events', () => { + // track all the event counts + // use simple count object (don't use tricky test keys!) + function addEventCounts(counts, event) { + // overall call counts + counts.events = counts.events || 0; + counts.codes = counts.codes || {}; - counts.codes[event.code] = counts.codes[event.code] || 0; + counts.codes[event.code] = counts.codes[event.code] || 0; - counts.events++; - counts.codes[event.code]++; -} + counts.events++; + counts.codes[event.code]++; + } -// track event and counts -// use simple count object (don't use tricky test keys!) -function trackEvent({events, event}) { - events.counts = events.counts || {}; - events.log = events.log || []; - - addEventCounts(events.counts, event); - // just log useful comparison details - events.log.push({ - code: event.code, - level: event.level, - details: event.details - }); -} + // track event and counts + // use simple count object (don't use tricky test keys!) + function trackEvent({events, event}) { + events.counts = events.counts || {}; + events.log = events.log || []; + + addEventCounts(events.counts, event); + // just log useful comparison details + events.log.push({ + code: event.code, + level: event.level, + details: event.details + }); + } -describe('events', () => { - // FIXME/TODO add object '*' handler and tests? + // track all the map counts + // use simple count object (don't use tricky test keys!) + function addMapCounts(counts, info) { + // overall call count + counts.expansionMap = counts.expansionMap || 0; + counts.expansionMap++; - it('check default handler called', async () => { - const d = + if(info.unmappedProperty) { + const c = counts.unmappedProperty = counts.unmappedProperty || {}; + const k = info.unmappedProperty; + c[k] = c[k] || 0; + c[k]++; + } + + if(info.unmappedValue) { + const c = counts.unmappedValue = counts.unmappedValue || {}; + const v = info.unmappedValue; + let k; + if(Object.keys(v).length === 1 && '@id' in v) { + k = v['@id']; + } else { + k = '__unknown__'; + } + c[k] = c[k] || 0; + c[k]++; + } + + if(info.relativeIri) { + const c = counts.relativeIri = counts.relativeIri || {}; + const k = info.relativeIri; + c[k] = c[k] || 0; + c[k]++; + } + + if(info.prependedIri) { + const c = counts.prependedIri = counts.prependedIri || {}; + const k = info.prependedIri.value; + c[k] = c[k] || 0; + c[k]++; + } + } + + // track map and counts + // use simple count object (don't use tricky test keys!) + function trackMap({maps, info}) { + maps.counts = maps.counts || {}; + maps.log = maps.log || []; + + addMapCounts(maps.counts, info); + // just log useful comparison details + // FIXME + maps.log.push(info); + //maps.log.push({ + // xxx: info.xxx + //}); + } + + describe('event system', () => { + it('check default handler called', async () => { + const d = { "relative": "test" } ; - const ex = []; + const ex = []; - const counts = {}; - const eventHandler = ({event}) => { - addEventCounts(counts, event); - }; + const counts = {}; + const eventHandler = ({event}) => { + addEventCounts(counts, event); + }; - jsonld.setDefaultEventHandler({eventHandler}); + jsonld.setDefaultEventHandler({eventHandler}); - const e = await jsonld.expand(d); + const e = await jsonld.expand(d); - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 - }, - events: 3 - }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(counts, { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 + }); - // reset default - jsonld.setDefaultEventHandler(); - }); + // reset default + jsonld.setDefaultEventHandler(); + }); - it('handle warning event with function', async () => { - const d = + it('handle warning event with function', async () => { + const d = { "@context": { "@RESERVED": "ex:test-function-handler" @@ -549,26 +606,26 @@ describe('events', () => { "@RESERVED": "test" } ; - const ex = []; + const ex = []; - const counts = {}; - const e = await jsonld.expand(d, { - eventHandler: ({event}) => { - addEventCounts(counts, event); - } - }); - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(counts, { - codes: { - 'invalid property expansion': 1, - 'invalid reserved term': 1 - }, - events: 2 + const counts = {}; + const e = await jsonld.expand(d, { + eventHandler: ({event}) => { + addEventCounts(counts, event); + } + }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(counts, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }); }); - }); - it('cached context event replay', async () => { - const d = + it('cached context event replay', async () => { + const d = { "@context": { "@RESERVED": "ex:test" @@ -576,43 +633,43 @@ describe('events', () => { "@RESERVED": "test" } ; - const ex = []; - - const counts0 = {}; - const counts1 = {}; - const e0 = await jsonld.expand(d, { - eventHandler: { - 'invalid reserved term': ({event}) => { - addEventCounts(counts0, event); + const ex = []; + + const counts0 = {}; + const counts1 = {}; + const e0 = await jsonld.expand(d, { + eventHandler: { + 'invalid reserved term': ({event}) => { + addEventCounts(counts0, event); + } } - } - }); - // FIXME: ensure cache is being used - const e1 = await jsonld.expand(d, { - eventHandler: { - 'invalid reserved term': ({event}) => { - addEventCounts(counts1, event); + }); + // FIXME: ensure cache is being used + const e1 = await jsonld.expand(d, { + eventHandler: { + 'invalid reserved term': ({event}) => { + addEventCounts(counts1, event); + } } - } + }); + assert.deepStrictEqual(e0, ex); + assert.deepStrictEqual(e1, ex); + assert.deepStrictEqual(counts0, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts 0'); + assert.deepStrictEqual(counts1, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts 1'); }); - assert.deepStrictEqual(e0, ex); - assert.deepStrictEqual(e1, ex); - assert.deepStrictEqual(counts0, { - codes: { - 'invalid reserved term': 1 - }, - events: 1 - }, 'counts 0'); - assert.deepStrictEqual(counts1, { - codes: { - 'invalid reserved term': 1 - }, - events: 1 - }, 'counts 1'); - }); - it('handle warning event with array of functions', async () => { - const d = + it('handle warning event with array of functions', async () => { + const d = { "@context": { "@RESERVED": "ex:test-function-array-handler" @@ -620,52 +677,52 @@ describe('events', () => { "@RESERVED": "test" } ; - const ex = []; - - const handlerCounts0 = {}; - const handlerCounts1 = {}; - const handledCounts = {}; - const e = await jsonld.expand(d, { - eventHandler: [ - ({event, next}) => { - addEventCounts(handlerCounts0, event); - // skip to next handler - next(); - }, - ({event}) => { - addEventCounts(handlerCounts1, event); - if(event.code === 'invalid reserved term') { - addEventCounts(handledCounts, event); - return; + const ex = []; + + const handlerCounts0 = {}; + const handlerCounts1 = {}; + const handledCounts = {}; + const e = await jsonld.expand(d, { + eventHandler: [ + ({event, next}) => { + addEventCounts(handlerCounts0, event); + // skip to next handler + next(); + }, + ({event}) => { + addEventCounts(handlerCounts1, event); + if(event.code === 'invalid reserved term') { + addEventCounts(handledCounts, event); + return; + } } - } - ] + ] + }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(handlerCounts0, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 0'); + assert.deepStrictEqual(handlerCounts1, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 1'); + assert.deepStrictEqual(handledCounts, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts handled'); }); - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(handlerCounts0, { - codes: { - 'invalid property expansion': 1, - 'invalid reserved term': 1 - }, - events: 2 - }, 'counts handler 0'); - assert.deepStrictEqual(handlerCounts1, { - codes: { - 'invalid property expansion': 1, - 'invalid reserved term': 1 - }, - events: 2 - }, 'counts handler 1'); - assert.deepStrictEqual(handledCounts, { - codes: { - 'invalid reserved term': 1 - }, - events: 1 - }, 'counts handled'); - }); - it('handle warning event early with array of functions', async () => { - const d = + it('handle warning event early with array of functions', async () => { + const d = { "@context": { "@RESERVED": "ex:test-function-array-handler" @@ -673,40 +730,40 @@ describe('events', () => { "@RESERVED": "test" } ; - const ex = []; - - const handlerCounts0 = {}; - const handlerCounts1 = {}; - const handledCounts = {}; - const e = await jsonld.expand(d, { - eventHandler: [ - ({event}) => { - addEventCounts(handlerCounts0, event); - // don't skip to next handler - }, - ({event}) => { - addEventCounts(handlerCounts1, event); - if(event.code === 'invalid reserved term') { - addEventCounts(handledCounts, event); - return; + const ex = []; + + const handlerCounts0 = {}; + const handlerCounts1 = {}; + const handledCounts = {}; + const e = await jsonld.expand(d, { + eventHandler: [ + ({event}) => { + addEventCounts(handlerCounts0, event); + // don't skip to next handler + }, + ({event}) => { + addEventCounts(handlerCounts1, event); + if(event.code === 'invalid reserved term') { + addEventCounts(handledCounts, event); + return; + } } - } - ] + ] + }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(handlerCounts0, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 0'); + assert.deepStrictEqual(handlerCounts1, {}, 'counts handler 1'); + assert.deepStrictEqual(handledCounts, {}, 'counts handled'); }); - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(handlerCounts0, { - codes: { - 'invalid property expansion': 1, - 'invalid reserved term': 1 - }, - events: 2 - }, 'counts handler 0'); - assert.deepStrictEqual(handlerCounts1, {}, 'counts handler 1'); - assert.deepStrictEqual(handledCounts, {}, 'counts handled'); - }); - it('handle warning event with code:function object', async () => { - const d = + it('handle warning event with code:function object', async () => { + const d = { "@context": { "@RESERVED": "ex:test-object-handler" @@ -714,28 +771,28 @@ describe('events', () => { "@RESERVED": "test" } ; - const ex = []; + const ex = []; - const counts = {}; - const e = await jsonld.expand(d, { - eventHandler: { - 'invalid reserved term': ({event}) => { - addEventCounts(counts, event); - assert.strictEqual(event.details.term, '@RESERVED'); + const counts = {}; + const e = await jsonld.expand(d, { + eventHandler: { + 'invalid reserved term': ({event}) => { + addEventCounts(counts, event); + assert.strictEqual(event.details.term, '@RESERVED'); + } } - } + }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(counts, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts'); }); - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(counts, { - codes: { - 'invalid reserved term': 1 - }, - events: 1 - }, 'counts'); - }); - it('handle warning event with complex handler', async () => { - const d = + it('handle warning event with complex handler', async () => { + const d = { "@context": { "@RESERVED": "ex:test-complex-handler" @@ -743,70 +800,70 @@ describe('events', () => { "@RESERVED": "test" } ; - const ex = []; - - const handlerCounts0 = {}; - const handlerCounts1 = {}; - const handlerCounts2 = {}; - const handlerCounts3 = {}; - const e = await jsonld.expand(d, { - eventHandler: [ - ({event, next}) => { - addEventCounts(handlerCounts0, event); - next(); - }, - [ + const ex = []; + + const handlerCounts0 = {}; + const handlerCounts1 = {}; + const handlerCounts2 = {}; + const handlerCounts3 = {}; + const e = await jsonld.expand(d, { + eventHandler: [ ({event, next}) => { - addEventCounts(handlerCounts1, event); + addEventCounts(handlerCounts0, event); + next(); + }, + [ + ({event, next}) => { + addEventCounts(handlerCounts1, event); + next(); + }, + { + 'bogus code': () => {} + } + ], + ({event, next}) => { + addEventCounts(handlerCounts2, event); next(); }, { - 'bogus code': () => {} + 'invalid reserved term': ({event}) => { + addEventCounts(handlerCounts3, event); + } } - ], - ({event, next}) => { - addEventCounts(handlerCounts2, event); - next(); + ] + }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(handlerCounts0, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 }, - { - 'invalid reserved term': ({event}) => { - addEventCounts(handlerCounts3, event); - } - } - ] + events: 2 + }, 'counts handler 0'); + assert.deepStrictEqual(handlerCounts1, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 1'); + assert.deepStrictEqual(handlerCounts2, { + codes: { + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 2 + }, 'counts handler 2'); + assert.deepStrictEqual(handlerCounts3, { + codes: { + 'invalid reserved term': 1 + }, + events: 1 + }, 'counts handler 3'); }); - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(handlerCounts0, { - codes: { - 'invalid property expansion': 1, - 'invalid reserved term': 1 - }, - events: 2 - }, 'counts handler 0'); - assert.deepStrictEqual(handlerCounts1, { - codes: { - 'invalid property expansion': 1, - 'invalid reserved term': 1 - }, - events: 2 - }, 'counts handler 1'); - assert.deepStrictEqual(handlerCounts2, { - codes: { - 'invalid property expansion': 1, - 'invalid reserved term': 1 - }, - events: 2 - }, 'counts handler 2'); - assert.deepStrictEqual(handlerCounts3, { - codes: { - 'invalid reserved term': 1 - }, - events: 1 - }, 'counts handler 3'); - }); - it('handle known warning events', async () => { - const d = + it('handle known warning events', async () => { + const d = { "@context": { "id-at": {"@id": "@test"}, @@ -819,7 +876,7 @@ describe('events', () => { } } ; - const ex = + const ex = [ { "ex:language": [ @@ -832,47 +889,47 @@ describe('events', () => { ] ; - const handledReservedTermCounts = {}; - const handledReservedValueCounts = {}; - const handledLanguageCounts = {}; - const e = await jsonld.expand(d, { - eventHandler: { - 'invalid reserved term': ({event}) => { - addEventCounts(handledReservedTermCounts, event); + const handledReservedTermCounts = {}; + const handledReservedValueCounts = {}; + const handledLanguageCounts = {}; + const e = await jsonld.expand(d, { + eventHandler: { + 'invalid reserved term': ({event}) => { + addEventCounts(handledReservedTermCounts, event); + }, + 'invalid reserved value': ({event}) => { + addEventCounts(handledReservedValueCounts, event); + }, + 'invalid @language value': ({event}) => { + addEventCounts(handledLanguageCounts, event); + } + } + }); + assert.deepStrictEqual(e, ex); + assert.deepStrictEqual(handledReservedTermCounts, { + codes: { + 'invalid reserved term': 1 }, - 'invalid reserved value': ({event}) => { - addEventCounts(handledReservedValueCounts, event); + events: 1 + }, 'handled reserved term counts'); + assert.deepStrictEqual(handledReservedValueCounts, { + codes: { + 'invalid reserved value': 1 }, - 'invalid @language value': ({event}) => { - addEventCounts(handledLanguageCounts, event); - } - } - }); - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(handledReservedTermCounts, { - codes: { - 'invalid reserved term': 1 - }, - events: 1 - }, 'handled reserved term counts'); - assert.deepStrictEqual(handledReservedValueCounts, { - codes: { - 'invalid reserved value': 1 - }, - events: 1 - }, 'handled reserved value counts'); - assert.deepStrictEqual(handledLanguageCounts, { - codes: { - 'invalid @language value': 1 - }, - events: 1 - }, 'handled language counts'); - - // dataset with invalid language tag - // Equivalent N-Quads: - // "..."^^ .' - // Using JSON dataset to bypass N-Quads parser checks. - const d2 = + events: 1 + }, 'handled reserved value counts'); + assert.deepStrictEqual(handledLanguageCounts, { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, 'handled language counts'); + + // dataset with invalid language tag + // Equivalent N-Quads: + // "..."^^ .' + // Using JSON dataset to bypass N-Quads parser checks. + const d2 = [ { "subject": { @@ -898,7 +955,7 @@ describe('events', () => { } ] ; - const ex2 = + const ex2 = [ { "@id": "ex:s", @@ -913,76 +970,33 @@ describe('events', () => { ] ; - const handledLanguageCounts2 = {}; - const e2 = await jsonld.fromRDF(d2, { - rdfDirection: 'i18n-datatype', - eventHandler: { - 'invalid @language value': ({event}) => { - addEventCounts(handledLanguageCounts2, event); + const handledLanguageCounts2 = {}; + const e2 = await jsonld.fromRDF(d2, { + rdfDirection: 'i18n-datatype', + eventHandler: { + 'invalid @language value': ({event}) => { + addEventCounts(handledLanguageCounts2, event); + } } - } + }); + assert.deepStrictEqual(e2, ex2); + assert.deepStrictEqual(handledLanguageCounts2, { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, 'handled language counts'); }); - assert.deepStrictEqual(e2, ex2); - assert.deepStrictEqual(handledLanguageCounts2, { - codes: { - 'invalid @language value': 1 - }, - events: 1 - }, 'handled language counts'); }); -}); - -describe('expansionMap', () => { - // track all the counts - // use simple count object (don't use tricky test keys!) - function addCounts(counts, info) { - // overall call count - counts.expansionMap = counts.expansionMap || 0; - counts.expansionMap++; - - if(info.unmappedProperty) { - const c = counts.unmappedProperty = counts.unmappedProperty || {}; - const k = info.unmappedProperty; - c[k] = c[k] || 0; - c[k]++; - } - - if(info.unmappedValue) { - const c = counts.unmappedValue = counts.unmappedValue || {}; - const v = info.unmappedValue; - let k; - if(Object.keys(v).length === 1 && '@id' in v) { - k = v['@id']; - } else { - k = '__unknown__'; - } - c[k] = c[k] || 0; - c[k]++; - } - - if(info.relativeIri) { - const c = counts.relativeIri = counts.relativeIri || {}; - const k = info.relativeIri; - c[k] = c[k] || 0; - c[k]++; - } - - if(info.prependedIri) { - const c = counts.prependedIri = counts.prependedIri || {}; - const k = info.prependedIri.value; - c[k] = c[k] || 0; - c[k]++; - } - } describe('unmappedValue', () => { // FIXME move to value section it('should have zero counts with empty input', async () => { const docWithNoContent = {}; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -991,7 +1005,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithNoContent, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 1, unmappedValue: { '__unknown__': 1 @@ -1010,9 +1024,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1021,7 +1035,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 1, unmappedValue: { '__unknown__': 1 @@ -1059,9 +1073,9 @@ describe('expansionMap', () => { ] ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1073,7 +1087,7 @@ describe('expansionMap', () => { }); assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 4, unmappedValue: { '__unknown__': 2, @@ -1101,9 +1115,9 @@ describe('expansionMap', () => { ; const ex = []; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1115,7 +1129,7 @@ describe('expansionMap', () => { }); assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 5, unmappedValue: { '__unknown__': 3, @@ -1135,9 +1149,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1146,7 +1160,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, unmappedValue: { '__unknown__': 3 @@ -1165,9 +1179,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1176,7 +1190,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, unmappedValue: { '__unknown__': 2 @@ -1195,9 +1209,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1206,7 +1220,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, {}); + assert.deepStrictEqual(maps, {}); assert.deepStrictEqual(events, {}); }); @@ -1220,9 +1234,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1231,19 +1245,20 @@ describe('expansionMap', () => { await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, {}); + assert.deepStrictEqual(maps, {}); assert.deepStrictEqual(events, {}); }); + // XXX it('should be called on unmapped term with no context', async () => { const docWithUnMappedTerm = { "testUndefined": "is undefined" }; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1252,7 +1267,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 4, relativeIri: { testUndefined: 2 @@ -1306,9 +1321,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1317,7 +1332,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 4, relativeIri: { testUndefined: 2 @@ -1349,9 +1364,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1360,7 +1375,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, relativeIri: { testUndefined: 2 @@ -1390,9 +1405,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1401,7 +1416,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, relativeIri: { testUndefined: 2 @@ -1428,9 +1443,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1439,7 +1454,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, prependedIri: { relativeiri: 1 @@ -1467,9 +1482,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1478,7 +1493,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeiri: 1 @@ -1506,9 +1521,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1517,7 +1532,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeiri: 1 @@ -1547,9 +1562,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1558,7 +1573,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeiri: 1 @@ -1587,9 +1602,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1598,7 +1613,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeiri: 1 @@ -1627,9 +1642,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1638,7 +1653,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1681,9 +1696,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1692,7 +1707,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1727,9 +1742,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1738,7 +1753,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 8, prependedIri: { anotherRelativeiri: 1, @@ -1783,9 +1798,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1794,7 +1809,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 8, prependedIri: { anotherRelativeiri: 1, @@ -1831,9 +1846,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1842,7 +1857,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1876,9 +1891,9 @@ describe('expansionMap', () => { "definedTerm": "is defined" }; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1887,7 +1902,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 6, prependedIri: { relativeiri: 1 @@ -1920,9 +1935,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1931,7 +1946,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, prependedIri: { 'relativeiri': 1 @@ -1962,9 +1977,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -1973,7 +1988,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, prependedIri: { relativeiri: 1 @@ -2004,9 +2019,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -2015,7 +2030,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 3, prependedIri: { relativeiri: 1 @@ -2043,9 +2058,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); }; const events = {}; const eventHandler = ({event}) => { @@ -2054,7 +2069,7 @@ describe('expansionMap', () => { await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 6, prependedIri: { './': 1, @@ -2085,9 +2100,9 @@ describe('expansionMap', () => { "term": "termValue" }; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -2103,7 +2118,7 @@ describe('expansionMap', () => { await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 4, prependedIri: { term: 4 @@ -2123,9 +2138,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -2141,7 +2156,7 @@ describe('expansionMap', () => { await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeIri: 2 @@ -2162,9 +2177,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); assert.deepStrictEqual(info.prependedIri, { type: '@vocab', vocab: 'http://example.com/', @@ -2180,7 +2195,7 @@ describe('expansionMap', () => { await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeIri: 2 @@ -2200,9 +2215,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -2220,7 +2235,7 @@ describe('expansionMap', () => { await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeIri: 1 @@ -2244,9 +2259,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -2264,7 +2279,7 @@ describe('expansionMap', () => { await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeIri: 1 @@ -2287,9 +2302,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -2307,7 +2322,7 @@ describe('expansionMap', () => { await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeIri: 1 @@ -2337,9 +2352,9 @@ describe('expansionMap', () => { } ; - const mapCounts = {}; + const maps = {}; const expansionMap = info => { - addCounts(mapCounts, info); + trackMap({maps, info}); if(info.prependedIri) { assert.deepStrictEqual(info.prependedIri, { type: '@base', @@ -2357,7 +2372,7 @@ describe('expansionMap', () => { await jsonld.expand(doc, {expansionMap, eventHandler}); - assert.deepStrictEqual(mapCounts, { + assert.deepStrictEqual(maps.counts, { expansionMap: 2, prependedIri: { relativeIri: 1 From fc29ecd0add3acb7e39c2b0057859d708977a4bc Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 2 Apr 2022 18:41:54 -0400 Subject: [PATCH 070/181] Improve event handling. - Setup eventHandler defaults once at top level. - Use simple existence check to optimize if handlers need to be called. - Better naming for event capture handler. --- lib/context.js | 122 ++++++++++++++++++++++++++----------------------- lib/events.js | 36 +++++++++------ lib/expand.js | 50 ++++++++++---------- lib/fromRdf.js | 24 +++++----- lib/jsonld.js | 9 +++- 5 files changed, 135 insertions(+), 106 deletions(-) diff --git a/lib/context.js b/lib/context.js index 38b35b64..2f740bd9 100644 --- a/lib/context.js +++ b/lib/context.js @@ -67,7 +67,7 @@ api.process = async ({ // event handler for capturing events to replay when using a cached context const events = []; - const eventHandler = [ + const eventCaptureHandler = [ ({event, next}) => { events.push(event); next(); @@ -75,12 +75,12 @@ api.process = async ({ ]; // chain to original handler if(options.eventHandler) { - eventHandler.push(options.eventHandler); + eventCaptureHandler.push(options.eventHandler); } // store original options to use when replaying events const originalOptions = options; - // shallow clone options with custom event handler - options = {...options, eventHandler}; + // shallow clone options with event capture handler + options = {...options, eventHandler: eventCaptureHandler}; // resolve contexts const resolved = await options.contextResolver.resolve({ @@ -133,9 +133,11 @@ api.process = async ({ // get processed context from cache if available const processed = resolvedContext.getProcessed(activeCtx); if(processed) { - // replay events with original non-capturing options - for(const event of processed.events) { - _handleEvent({event, options: originalOptions}); + if(originalOptions.eventHandler) { + // replay events with original non-capturing options + for(const event of processed.events) { + _handleEvent({event, options: originalOptions}); + } } rval = activeCtx = processed.context; @@ -474,18 +476,20 @@ api.createTermDefinition = ({ 'jsonld.SyntaxError', {code: 'keyword redefinition', context: localCtx, term}); } else if(term.match(KEYWORD_PATTERN)) { - _handleEvent({ - event: { - code: 'invalid reserved term', - level: 'warning', - message: - 'Terms beginning with "@" are reserved for future use and ignored.', - details: { - term - } - }, - options - }); + if(options.eventHandler) { + _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( @@ -566,19 +570,21 @@ api.createTermDefinition = ({ } 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(options.eventHandler) { + _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 { @@ -612,19 +618,21 @@ api.createTermDefinition = ({ // reserve a null term, which may be protected mapping['@id'] = null; } else if(!api.isKeyword(id) && id.match(KEYWORD_PATTERN)) { - _handleEvent({ - event: { - code: 'invalid reserved value', - level: 'warning', - message: - 'Values beginning with "@" are reserved for future use and' + - ' ignored.', - details: { - id - } - }, - options - }); + if(options.eventHandler) { + _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 { @@ -1144,17 +1152,19 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { if(expandedResult !== undefined) { value = expandedResult; } else { - _handleEvent({ - event: { - code: 'relative IRI after expansion', - level: 'warning', - message: 'Expansion resulted in a relative IRI.', - details: { - value - } - }, - options - }); + if(options.eventHandler) { + _handleEvent({ + event: { + code: 'relative IRI after expansion', + level: 'warning', + message: 'Expansion resulted in a relative IRI.', + details: { + value + } + }, + options + }); + } } } diff --git a/lib/events.js b/lib/events.js index eb34bc2d..ceadbd61 100644 --- a/lib/events.js +++ b/lib/events.js @@ -21,18 +21,25 @@ module.exports = api; api.defaultEventHandler = null; /** - * Check if event handler is in use in options or by default. + * Setup event handler. * - * This call is used to avoid building event structures and calling the main - * handleEvent() call. It checks if safe mode is on, an event handler is in the - * processing options, or a default handler was set. + * Return an array event handler constructed from an optional safe mode + * handler, an optional options event handler, and an optional default handler. * - * @param {object} options - processing options: - * {boolean} [safe] - flag for Safe Mode. + * @param {object} options - processing options * {function|object|array} [eventHandler] - an event handler. + * + * @return an array event handler. */ -api.hasEventHandler = options => { - return options.safe || options.eventHandler || api.defaultEventHandler; +api.setupEventHandler = ({options = {}}) => { + // build in priority order + const eventHandler = [].concat( + options.safe ? api.safeModeEventHandler : [], + options.eventHandler ? _asArray(options.eventHandler) : [], + api.defaultEventHandler ? api.defaultEventHandler : [] + ); + // null if no handlers + return eventHandler.length === 0 ? null : eventHandler; }; /** @@ -45,24 +52,23 @@ api.hasEventHandler = options => { * handlers should process the event as appropriate. The 'next()' function * should be called to let the next handler process the event. * + * NOTE: Only call this function if options.eventHandler is set and is an + * array of hanlers. This is an optimization. Callers are expected to check + * for an event handler before constructing events and calling this function. + * * @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 + * {array} eventHandler - an event handler array. */ api.handleEvent = ({ event, options }) => { - const handlers = [].concat( - // priority is safe mode handler, options handler, then default handler - options.safe ? api.safeModeEventHandler : [], - options.eventHandler ? _asArray(options.eventHandler) : [], - api.defaultEventHandler ? api.defaultEventHandler : [] - ); - _handle({event, handlers}); + _handle({event, handlers: options.eventHandler}); }; function _handle({event, handlers}) { diff --git a/lib/expand.js b/lib/expand.js index 4a0b9614..06f34912 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -465,18 +465,20 @@ async function _expandObject({ expandedParent }); if(expandedProperty === undefined) { - _handleEvent({ - event: { - code: 'invalid property expansion', - level: 'warning', - message: 'Invalid expansion for property.', - details: { - // FIXME: include expandedProperty before mapping - property: key - } - }, - options - }); + if(options.eventHandler) { + _handleEvent({ + event: { + code: 'invalid property expansion', + level: 'warning', + message: 'Invalid expansion for property.', + details: { + // FIXME: include expandedProperty before mapping + property: key + } + }, + options + }); + } continue; } } @@ -628,17 +630,19 @@ async function _expandObject({ // ensure language tag matches BCP47 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 - }); + if(options.eventHandler) { + _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 509bde0b..70965328 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -341,17 +341,19 @@ function _RDFToObject(o, useNativeTypes, rdfDirection, options) { if(language.length > 0) { rval['@language'] = language; if(!language.match(REGEX_BCP47)) { - _handleEvent({ - event: { - code: 'invalid @language value', - level: 'warning', - message: '@language value must be valid BCP47.', - details: { - language - } - }, - options - }); + if(options.eventHandler) { + _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 502cdd26..f16b255d 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -84,6 +84,7 @@ const { logEventHandler: _logEventHandler, logWarningEventHandler: _logWarningEventHandler, setDefaultEventHandler: _setDefaultEventHandler, + setupEventHandler: _setupEventHandler, strictModeEventHandler: _strictModeEventHandler, unhandledEventHandler: _unhandledEventHandler } = require('./events'); @@ -1042,7 +1043,13 @@ function _setDefaults(options, { documentLoader = jsonld.documentLoader, ...defaults }) { - return Object.assign({}, {documentLoader}, defaults, options); + return Object.assign( + {}, + {documentLoader}, + defaults, + options, + {eventHandler: _setupEventHandler({options})} + ); } // end of jsonld API `wrapper` factory From b1c87881ca9f7f53623173263804b57af6964f8e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sun, 3 Apr 2022 01:28:16 -0400 Subject: [PATCH 071/181] Add function names. --- lib/events.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/events.js b/lib/events.js index ceadbd61..61c5ef11 100644 --- a/lib/events.js +++ b/lib/events.js @@ -101,7 +101,7 @@ function _handle({event, handlers}) { } // safe handler that rejects unsafe warning conditions -api.safeModeEventHandler = function({event, next}) { +api.safeModeEventHandler = function safeModeEventHandler({event, next}) { // fail on all unsafe warnings if(event.level === 'warning' && event.tags.includes('unsafe')) { throw new JsonLdError( @@ -114,7 +114,7 @@ api.safeModeEventHandler = function({event, next}) { }; // strict handler that rejects all warning conditions -api.strictModeEventHandler = function({event, next}) { +api.strictModeEventHandler = function strictModeEventHandler({event, next}) { // fail on all warnings if(event.level === 'warning') { throw new JsonLdError( @@ -127,13 +127,13 @@ api.strictModeEventHandler = function({event, next}) { }; // logs all events and continues -api.logEventHandler = function({event, next}) { +api.logEventHandler = function logEventHandler({event, next}) { console.log(`EVENT: ${event.message}`, {event}); next(); }; // log 'warning' level events -api.logWarningEventHandler = function({event, next}) { +api.logWarningEventHandler = function logWarningEventHandler({event, next}) { if(event.level === 'warning') { console.warn(`WARNING: ${event.message}`, {event}); } @@ -141,7 +141,7 @@ api.logWarningEventHandler = function({event, next}) { }; // fallback to throw errors for any unhandled events -api.unhandledEventHandler = function({event}) { +api.unhandledEventHandler = function unhandledEventHandler({event}) { throw new JsonLdError( 'No handler for event.', 'jsonld.UnhandledEvent', From d3b87540d058727e3992d339816e1ba8443f9014 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sun, 3 Apr 2022 01:29:08 -0400 Subject: [PATCH 072/181] Fix safe event check. --- lib/events.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/events.js b/lib/events.js index 61c5ef11..8d3745aa 100644 --- a/lib/events.js +++ b/lib/events.js @@ -103,7 +103,8 @@ function _handle({event, handlers}) { // safe handler that rejects unsafe warning conditions api.safeModeEventHandler = function safeModeEventHandler({event, next}) { // fail on all unsafe warnings - if(event.level === 'warning' && event.tags.includes('unsafe')) { + if(event.level === 'warning' && + event.tags && event.tags.includes('unsafe')) { throw new JsonLdError( 'Safe mode violation.', 'jsonld.SafeModeViolationEvent', From ebd4a1f28de5d05b48d1dbb551b89d99001a087a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 4 Apr 2022 16:08:25 -0400 Subject: [PATCH 073/181] Add generic event testing wrapper. - Reduce boilerplate for common testing pattern. - Pass in params and parts to test. --- tests/misc.js | 1946 +++++++++++++++++++++++++++---------------------- 1 file changed, 1059 insertions(+), 887 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index 15638a10..2eeb021c 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -566,6 +566,96 @@ describe('events', () => { //}); } + // test different apis + // use appropriate options + async function _test({ + // expand, compact, frame, fromRdf, toRdf, etc + type, + input, + expected, + exception, + mapCounts, + mapLog, + eventCounts, + eventLog, + options + }) { + const maps = {counts: {}, log: []}; + const expansionMap = info => { + trackMap({maps, info}); + }; + const events = {counts: {}, log: []}; + const eventHandler = ({event}) => { + trackEvent({events, event}); + }; + + let result; + let error; + const opts = {...options}; + if(mapCounts || mapLog) { + opts.expansionMap = expansionMap; + } + if(eventCounts || eventLog) { + opts.eventHandler = eventHandler; + } + if(!['expand'].includes(type)) { + throw new Error(`Unknown test type: "${type}"`); + } + try { + if(type === 'expand') { + result = await jsonld.expand(input, opts); + } + } catch(e) { + error = e; + } + + if(exception) { + assert(error); + assert.equal(error.name, exception); + } + if(!exception && error) { + throw error; + } + if(expected) { + assert.deepStrictEqual(result, expected); + } + if(mapCounts) { + assert.deepStrictEqual(maps.counts, mapCounts); + } + if(mapLog) { + assert.deepStrictEqual(maps.log, mapLog); + } + if(eventCounts) { + assert.deepStrictEqual(events.counts, eventCounts); + } + if(mapLog) { + assert.deepStrictEqual(events.log, eventLog); + } + } + + // test passes with safe=true + async function _testSafe({ + type, + input + }) { + await _test({type, input, options: {safe: true}}); + } + + // test fails with safe=true + async function _testUnsafe({ + type, + input + }) { + let error; + try { + await _test({type, input, options: {safe: true}}); + } catch(e) { + error = e; + } + + assert(error); + } + describe('event system', () => { it('check default handler called', async () => { const d = @@ -990,29 +1080,24 @@ describe('events', () => { }); describe('unmappedValue', () => { - // FIXME move to value section it('should have zero counts with empty input', async () => { const docWithNoContent = {}; + const expected = []; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithNoContent, {expansionMap, eventHandler}); + console.error('FIXME'); - assert.deepStrictEqual(maps.counts, { - expansionMap: 1, - unmappedValue: { - '__unknown__': 1 - } + await _test({ + type: 'expand', + input: docWithNoContent, + expected, + mapCounts: { + expansionMap: 1, + unmappedValue: { + '__unknown__': 1 + } + }, + eventCounts: {} }); - console.error('FIXME'); - assert.deepStrictEqual(events, {}); }); it('should have zero counts with no terms', async () => { @@ -1023,30 +1108,25 @@ describe('events', () => { } } ; + const expected = []; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 1, - unmappedValue: { - '__unknown__': 1 - } - }); console.error('FIXME'); - assert.deepStrictEqual(events, {}); + await _test({ + type: 'expand', + input: docWithNoTerms, + expected, + mapCounts: { + expansionMap: 1, + unmappedValue: { + '__unknown__': 1 + } + }, + eventCounts: {} + }); }); it('should notify for @set free-floating scaler', async () => { - const docWithNoTerms = + const input = { "@set": [ "free-floating strings in set objects are removed", @@ -1060,7 +1140,7 @@ describe('events', () => { ] } ; - const ex = + const expected = [ { "@id": "http://example.com/node", @@ -1073,33 +1153,24 @@ describe('events', () => { ] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - const e = await jsonld.expand(docWithNoTerms, { - expansionMap, eventHandler - }); - - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(maps.counts, { - expansionMap: 4, - unmappedValue: { - '__unknown__': 2, - 'http://example.com/free-floating-node': 2 - } - }); console.error('FIXME'); - assert.deepStrictEqual(events, {}); + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 4, + unmappedValue: { + '__unknown__': 2, + 'http://example.com/free-floating-node': 2 + } + }, + eventCounts: {} + }); }); it('should notify for @list free-floating scaler', async () => { - const docWithNoTerms = + const input = { "@list": [ "free-floating strings in list objects are removed", @@ -1113,206 +1184,194 @@ describe('events', () => { ] } ; - const ex = []; - - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - const e = await jsonld.expand(docWithNoTerms, { - expansionMap, eventHandler - }); + const expected = []; - assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(maps.counts, { - expansionMap: 5, - unmappedValue: { - '__unknown__': 3, - 'http://example.com/free-floating-node': 2 - } - }); console.error('FIXME'); - assert.deepStrictEqual(events, {}); + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 5, + unmappedValue: { + '__unknown__': 3, + 'http://example.com/free-floating-node': 2 + } + }, + eventCounts: {} + }); }); it('should notify for null @value', async () => { - const docWithNoTerms = + const input = { "urn:property": { "@value": null } } ; + const expected = []; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - unmappedValue: { - '__unknown__': 3 - } - }); console.error('FIXME'); - assert.deepStrictEqual(events, {}); + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + unmappedValue: { + '__unknown__': 3 + } + }, + eventCounts: {} + }); }); it('should notify for @language alone', async () => { - const docWithNoTerms = + const input = { "urn:property": { "@language": "en" } } ; + const expected = []; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithNoTerms, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - unmappedValue: { - '__unknown__': 2 - } - }); console.error('FIXME'); - assert.deepStrictEqual(events, {}); + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + unmappedValue: { + '__unknown__': 2 + } + }, + eventCounts: {} + }); }); }); describe('unmappedProperty', () => { it('should have zero counts with absolute term', async () => { - const docWithMappedTerm = + const input = { "urn:definedTerm": "is defined" } +; + const expected = +[ + { + "urn:definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps, {}); - assert.deepStrictEqual(events, {}); + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: {} + }); }); it('should have zero counts with mapped term', async () => { - const docWithMappedTerm = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" }, "definedTerm": "is defined" } +; + const expected = +[ + { + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithMappedTerm, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps, {}); - assert.deepStrictEqual(events, {}); + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: {} + }); }); // XXX it('should be called on unmapped term with no context', async () => { - const docWithUnMappedTerm = + const input = { "testUndefined": "is undefined" -}; - - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 4, - relativeIri: { - testUndefined: 2 - }, - unmappedProperty: { - testUndefined: 1 - }, - unmappedValue: { - '__unknown__': 1 - } - }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 - }, - events: 3 - }); - assert.deepStrictEqual(events.log, [ - { - code: 'relative IRI after expansion', - details: { - value: 'testUndefined' +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 4, + relativeIri: { + testUndefined: 2 }, - level: 'warning' + unmappedProperty: { + testUndefined: 1 + }, + unmappedValue: { + '__unknown__': 1 + } }, - { - code: 'relative IRI after expansion', - details: { - value: 'testUndefined' + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 }, - level: 'warning' + events: 3 }, - { - code: 'invalid property expansion', - details: { - property: 'testUndefined' + eventLog: [ + { + code: 'relative IRI after expansion', + details: { + value: 'testUndefined' + }, + level: 'warning' }, - level: 'warning' - } - ]); + { + code: 'relative IRI after expansion', + details: { + value: 'testUndefined' + }, + level: 'warning' + }, + { + code: 'invalid property expansion', + details: { + property: 'testUndefined' + }, + level: 'warning' + } + ] + }); + await _testUnsafe({type: 'expand', input}); }); it('should be called on unmapped term with context [1]', async () => { - const docWithUnMappedTerm = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1320,41 +1379,36 @@ describe('events', () => { "testUndefined": "is undefined" } ; - - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 4, - relativeIri: { - testUndefined: 2 - }, - unmappedProperty: { - testUndefined: 1 + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 4, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + }, + unmappedValue: { + '__unknown__': 1 + } }, - unmappedValue: { - '__unknown__': 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 - }, - events: 3 - }); }); it('should be called on unmapped term with context [2]', async () => { - const docWithUnMappedTerm = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1362,39 +1416,44 @@ describe('events', () => { "definedTerm": "is defined", "testUndefined": "is undefined" } +; + const expected = +[ + { + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - relativeIri: { - testUndefined: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + } }, - unmappedProperty: { - testUndefined: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 - }, - events: 3 - }); }); it('should be called on nested unmapped term', async () => { - const docWithUnMappedTerm = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1403,115 +1462,120 @@ describe('events', () => { "testUndefined": "is undefined" } } +; + const expected = +[ + { + "https://example.com#definedTerm": [ + {} + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithUnMappedTerm, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - relativeIri: { - testUndefined: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + } }, - unmappedProperty: { - testUndefined: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 2 + }, + events: 3 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 - }, - events: 3 - }); }); }); + // FIXME naming describe('relativeIri', () => { it('should be called on relative IRI for id term [1]', async () => { - const docWithRelativeIriId = + const input = { "@id": "relativeiri" } ; - - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - relativeiri: 1 + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + }, + unmappedValue: { + relativeiri: 1 + } }, - unmappedValue: { - relativeiri: 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1 - }); }); it('should be called on relative IRI for id term [2]', async () => { - const docWithRelativeIriId = + const input = { "@id": "relativeiri", "urn:test": "value" } +; + const expected = +[ + { + "@id": "relativeiri", + "urn:test": [ + { + "@value": "value" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeiri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } }, - relativeIri: { - relativeiri: 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1 - }); }); it('should be called on relative IRI for id term [3]', async () => { - const docWithRelativeIriId = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1519,38 +1583,44 @@ describe('events', () => { "@id": "relativeiri", "definedTerm": "is defined" } +; + const expected = +[ + { + "@id": "relativeiri", + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeiri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } }, - relativeIri: { - relativeiri: 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1 - }); }); it('should be called on relative IRI for id term (nested)', async () => { - const docWithRelativeIriId = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1560,38 +1630,44 @@ describe('events', () => { "@id": "relativeiri" } } +; + const expected = +[ + { + "@id": "urn:absoluteIri", + "https://example.com#definedTerm": [ + { + "@id": "relativeiri" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeiri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } }, - relativeIri: { - relativeiri: 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1 - }); }); it('should be called on relative IRI for aliased id term', async () => { - const docWithRelativeIriId = + const input = { "@context": { "id": "@id", @@ -1600,38 +1676,44 @@ describe('events', () => { "id": "relativeiri", "definedTerm": "is defined" } +; + const expected = +[ + { + "@id": "relativeiri", + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeiri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + relativeiri: 1 + } }, - relativeIri: { - relativeiri: 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1 - }); }); it('should be called on relative IRI for type term', async () => { - const docWithRelativeIriId = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1640,44 +1722,52 @@ describe('events', () => { "@type": "relativeiri", "definedTerm": "is defined" } +; + const expected = +[ + { + "@type": [ + "relativeiri" + ], + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } }, - unmappedProperty: { - id: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 4 - }, - events: 5 - }); }); it('should be called on relative IRI for type ' + 'term in scoped context', async () => { - const docWithRelativeIriId = + const input = { "@context": { "definedType": { @@ -1694,44 +1784,54 @@ describe('events', () => { "@type": "relativeiri" } } +; + const expected = +[ + { + "@type": [ + "https://example.com#definedType" + ], + "https://example.com#definedTerm": [ + { + "@type": [ + "relativeiri" + ] + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } }, - unmappedProperty: { - id: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 4 - }, - events: 5 - }); }); it('should be called on relative IRI for ' + 'type term with multiple relative IRI types', async () => { - const docWithRelativeIriId = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1740,47 +1840,56 @@ describe('events', () => { "@type": ["relativeiri", "anotherRelativeiri"], "definedTerm": "is defined" } +; + const expected = +[ + { + "@type": [ + "relativeiri", + "anotherRelativeiri" + ], + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 8, - prependedIri: { - anotherRelativeiri: 1, - relativeiri: 1 - }, - relativeIri: { - anotherRelativeiri: 1, - id: 2, - relativeiri: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 8, + prependedIri: { + anotherRelativeiri: 1, + relativeiri: 1 + }, + relativeIri: { + anotherRelativeiri: 1, + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } }, - unmappedProperty: { - id: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 5 + }, + events: 6 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 5 - }, - events: 6 - }); }); it('should be called on relative IRI for ' + 'type term with multiple relative IRI types in scoped context' + '', async () => { - const docWithRelativeIriId = + const input = { "@context": { "definedType": { @@ -1796,46 +1905,57 @@ describe('events', () => { "@type": ["relativeiri", "anotherRelativeiri" ] } } +; + const expected = +[ + { + "@type": [ + "https://example.com#definedType" + ], + "https://example.com#definedTerm": [ + { + "@type": [ + "relativeiri", + "anotherRelativeiri" + ] + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 8, - prependedIri: { - anotherRelativeiri: 1, - relativeiri: 1 - }, - relativeIri: { - anotherRelativeiri: 1, - id: 2, - relativeiri: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 8, + prependedIri: { + anotherRelativeiri: 1, + relativeiri: 1 + }, + relativeIri: { + anotherRelativeiri: 1, + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } }, - unmappedProperty: { - id: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 5 + }, + events: 6 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 5 - }, - events: 6 - }); }); it('should be called on relative IRI for ' + 'type term with multiple types', async () => { - const docWithRelativeIriId = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1844,43 +1964,52 @@ describe('events', () => { "@type": ["relativeiri", "definedTerm"], "definedTerm": "is defined" } +; + const expected = +[ + { + "@type": [ + "relativeiri", + "https://example.com#definedTerm" + ], + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } }, - unmappedProperty: { - id: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 4 - }, - events: 5 - }); }); it('should be called on relative IRI for aliased type term', async () => { - const docWithRelativeIriId = + const input = { "@context": { "type": "@type", @@ -1890,284 +2019,310 @@ describe('events', () => { "type": "relativeiri", "definedTerm": "is defined" }; + const expected = +[ + { + "@type": [ + "relativeiri" + ], + "https://example.com#definedTerm": [ + { + "@value": "is defined" + } + ] + } +] +; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 6, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + id: 2, + relativeiri: 2 + }, + unmappedProperty: { + id: 1 + } }, - unmappedProperty: { - id: 1 + eventCounts: { + codes: { + 'invalid property expansion': 1, + 'relative IRI after expansion': 4 + }, + events: 5 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 4 - }, - events: 5 - }); }); it('should be called on relative IRI when ' + '@base value is `null`', async () => { - const docWithRelativeIriId = + const input = { "@context": { "@base": null }, "@id": "relativeiri" } +; + const expected = +[ +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - prependedIri: { - 'relativeiri': 1 - }, - relativeIri: { - 'relativeiri': 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + prependedIri: { + 'relativeiri': 1 + }, + relativeIri: { + 'relativeiri': 1 + }, + unmappedValue: { + 'relativeiri': 1 + } }, - unmappedValue: { - 'relativeiri': 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1 - }); }); it('should be called on relative IRI when ' + '@base value is `./`', async () => { - const docWithRelativeIriId = + const input = { "@context": { "@base": "./" }, "@id": "relativeiri" } +; + const expected = +[ +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - '/relativeiri': 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + '/relativeiri': 1 + }, + unmappedValue: { + '/relativeiri': 1 + } }, - unmappedValue: { - '/relativeiri': 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1 - }); }); it('should be called on relative IRI when ' + '`@vocab` value is `null`', async () => { - const docWithRelativeIriId = + const input = { "@context": { "@vocab": null }, "@type": "relativeiri" } +; + const expected = +[ + { + "@type": [ + "relativeiri" + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 3, - prependedIri: { - relativeiri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + prependedIri: { + relativeiri: 1 + }, + relativeIri: { + 'relativeiri': 2 + } }, - relativeIri: { - 'relativeiri': 2 + eventCounts: { + codes: { + 'relative IRI after expansion': 2 + }, + events: 2 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 2 - }, - events: 2 - }); }); it('should be called on relative IRI when ' + '`@vocab` value is `./`', async () => { - const docWithRelativeIriId = + const input = { "@context": { "@vocab": "./" }, "@type": "relativeiri" } +; + const expected = +[ + { + "@type": [ + "/relativeiri" + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(docWithRelativeIriId, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 6, - prependedIri: { - './': 1, - relativeiri: 2 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 6, + prependedIri: { + './': 1, + relativeiri: 2 + }, + relativeIri: { + '/': 1, + '/relativeiri': 2 + } }, - relativeIri: { - '/': 1, - '/relativeiri': 2 + eventCounts: { + codes: { + 'relative IRI after expansion': 3 + }, + events: 3 } }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 3 - }, - events: 3 - }); }); }); describe('prependedIri', () => { it('should be called when property is ' + 'being expanded with `@vocab`', async () => { - const doc = + const input = { "@context": { "@vocab": "http://example.com/" }, "term": "termValue" }; + const expected = +[ + { + "http://example.com/term": [ + { + "@value": "termValue" + } + ] + } +] +; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - assert.deepStrictEqual(info.prependedIri, { - type: '@vocab', - vocab: 'http://example.com/', - value: 'term', - typeExpansion: false, - result: 'http://example.com/term' - }); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(doc, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 4, - prependedIri: { - term: 4 - } + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 4, + prependedIri: { + term: 4 + } + }, + eventCounts: {}, + eventLog: [ + { + prependedIri: { + type: '@vocab', + vocab: 'http://example.com/', + value: 'term', + typeExpansion: false, + result: 'http://example.com/term' + } + } + ] }); - assert.deepStrictEqual(events, {}); }); it('should be called when `@type` is ' + 'being expanded with `@vocab`', async () => { - const doc = + const input = { "@context": { "@vocab": "http://example.com/" }, "@type": "relativeIri" } +; + const expected = +[ + { + "@type": [ + "http://example.com/relativeIri" + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - assert.deepStrictEqual(info.prependedIri, { - type: '@vocab', - vocab: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - }); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(doc, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 2 - } + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeIri: 2 + } + }, + eventCounts: {}, + eventLog: [ + { + prependedIri: { + type: '@vocab', + vocab: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + } + } + ] }); - assert.deepStrictEqual(events, {}); }); it('should be called when aliased `@type` is ' + 'being expanded with `@vocab`', async () => { - const doc = + const input = { "@context": { "@vocab": "http://example.com/", @@ -2175,81 +2330,88 @@ describe('events', () => { }, "type": "relativeIri" } +; + const expected = +[ + { + "@type": [ + "http://example.com/relativeIri" + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - assert.deepStrictEqual(info.prependedIri, { - type: '@vocab', - vocab: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - }); - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(doc, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 2 - } + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeIri: 2 + } + }, + eventCounts: {}, + eventLog: [ + { + prependedIri: { + type: '@vocab', + vocab: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + } + } + ] }); - assert.deepStrictEqual(events, {}); }); it('should be called when `@id` is being ' + 'expanded with `@base`', async () => { - const doc = + const input = { "@context": { "@base": "http://example.com/" }, "@id": "relativeIri" } +; + const expected = +[ +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - if(info.prependedIri) { - assert.deepStrictEqual(info.prependedIri, { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: false, - result: 'http://example.com/relativeIri' - }); - } - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(doc, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + unmappedValue: { + 'http://example.com/relativeIri': 1 + } }, - unmappedValue: { - 'http://example.com/relativeIri': 1 - } + eventCounts: {}, + eventMap: [ + { + prependedIri: { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: false, + result: 'http://example.com/relativeIri' + } + } + ] }); - assert.deepStrictEqual(events, {}); }); it('should be called when aliased `@id` ' + 'is being expanded with `@base`', async () => { - const doc = + const input = { "@context": { "@base": "http://example.com/", @@ -2257,92 +2419,97 @@ describe('events', () => { }, "id": "relativeIri" } +; + const expected = +[ +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - if(info.prependedIri) { - assert.deepStrictEqual(info.prependedIri, { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: false, - result: 'http://example.com/relativeIri' - }); - } - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(doc, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + unmappedValue: { + 'http://example.com/relativeIri': 1 + } }, - unmappedValue: { - 'http://example.com/relativeIri': 1 - } + eventCounts: {}, + eventLog: [ + { + prependedIri: { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: false, + result: 'http://example.com/relativeIri' + } + } + ] }); - assert.deepStrictEqual(events, {}); }); it('should be called when `@type` is ' + 'being expanded with `@base`', async () => { - const doc = + const input = { "@context": { "@base": "http://example.com/" }, "@type": "relativeIri" } +; + const expected = +[ + { + "@type": [ + "http://example.com/relativeIri" + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - if(info.prependedIri) { - assert.deepStrictEqual(info.prependedIri, { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - }); - } - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(doc, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + relativeIri: { + relativeIri: 1 + } }, - relativeIri: { - relativeIri: 1 - } - }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1, + //FIXME: true }, - events: 1, - //FIXME: true + eventLog: [ + { + prependedIri: { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + } + } + ] }); }); it('should be called when aliased `@type` is ' + 'being expanded with `@base`', async () => { - const doc = + const input = { "@context": { "@base": "http://example.com/", @@ -2350,43 +2517,48 @@ describe('events', () => { }, "type": "relativeIri" } +; + const expected = +[ + { + "@type": [ + "http://example.com/relativeIri" + ] + } +] ; - const maps = {}; - const expansionMap = info => { - trackMap({maps, info}); - if(info.prependedIri) { - assert.deepStrictEqual(info.prependedIri, { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - }); - } - }; - const events = {}; - const eventHandler = ({event}) => { - trackEvent({events, event}); - }; - - await jsonld.expand(doc, {expansionMap, eventHandler}); - - assert.deepStrictEqual(maps.counts, { - expansionMap: 2, - prependedIri: { - relativeIri: 1 + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + prependedIri: { + relativeIri: 1 + }, + relativeIri: { + relativeIri: 1 + } }, - relativeIri: { - relativeIri: 1 - } - }); - assert.deepStrictEqual(events.counts, { - codes: { - 'relative IRI after expansion': 1 + eventCounts: { + codes: { + 'relative IRI after expansion': 1 + }, + events: 1, + //FIXME: true }, - events: 1, - //FIXME: true + eventLog: [ + { + prependedIri: { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + } + } + ] }); }); }); From e050ad8d534eed6d6d754e8570989bdf201cb88b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 15:40:20 -0400 Subject: [PATCH 074/181] Update events and validation modes. - Rename handlers. - Add strict handler docs. - Add `JsonLdEvent` type to events. - Update `safeEventHandler` to handle new event codes. - Add more event emitting points. - Update event testing and tests. - Add safe/strict testing flags. - Fix bugs and cleanup. --- README.md | 35 +++++--- lib/context.js | 4 + lib/events.js | 36 +++++--- lib/expand.js | 98 +++++++++++++++++++++- lib/fromRdf.js | 1 + lib/jsonld.js | 6 +- tests/misc.js | 217 +++++++++++++++++++++++++++++++++++++------------ 7 files changed, 321 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index df156978..3c395374 100644 --- a/README.md +++ b/README.md @@ -363,14 +363,11 @@ quality is known events are unnecessary. #### Event Structure -Events are basic JSON objects with the following properties: +Events are JSON objects with the following properties: +- **`type`**: ['JsonLdEvent'] and optionally an array with others. - **`code`**: A basic string code, similar to existing JSON-LD error codes. - **`level`**: The severity level. Currently only `warning` is emitted. -- **`tags`**: Optional hints for the type of event. Currently defined: - - **`unsafe`**: Event is considered unsafe. - - **`lossy`**: Event is related to potential data loss. - - **`empty`**: Event is related to empty data structures. - **`message`**: A human readable message describing the event. - **`details`**: A JSON object with event specific details. @@ -445,7 +442,7 @@ const handler = { const expanded = await jsonld.expand(data, {eventHandler}); ``` -#### Safe Mode +#### Safe Validation A common use case is to avoid JSON-LD constructs that will result in lossy behavior. The JSON-LD specifications have notes about when data is dropped. @@ -473,15 +470,29 @@ const expanded = await jsonld.expand(data, { }); ``` +#### Strict Validation + +Some data may be valid and "safe" but still have issues that could indicate +data problems. A "strict" validation mode is available that handles more issues +that the "safe" validation mode. This mode may cause false positives so may be +best suited for JSON-LD authoring tools. + +```js +// expand a document in strict mode +const expanded = await jsonld.expand(data, { + eventHandler: jsonld.strictEventHandler +}); +``` + #### Available Handlers Some predefined event handlers are available to use alone or as part of a more complex handler: -- **safeModeEventHandler**: The handler used when `safe` is `true`. -- **strictModeEventHandler**: A handler that is more strict than the `safe` - handler and also fails on other detectable events related to poor input - structure. +- **safeEventHandler**: The handler used when `safe` is `true`. +- **strictEventHandler**: A handler that is more strict than the `safe` + handler and also fails on other detectable events related to possible input + issues. - **logEventHandler**: A debugging handler that outputs to the console. - **logWarningHandler**: A debugging handler that outputs `warning` level events to the console. @@ -495,13 +506,13 @@ safe mode, and the second handler when in safe mode. ```js // fail on unknown events jsonld.setDefaultEventHandler(jsonld.unhandledEventHandler); -// will use safe mode handler, like `{safe: true}` +// will use unhandled event handler by default const expanded = await jsonld.expand(data); ``` ```js // always use safe mode event handler, ignore other events -jsonld.setDefaultEventHandler(jsonld.safeModeEventHandler); +jsonld.setDefaultEventHandler(jsonld.safeEventHandler); // will use safe mode handler, like `{safe: true}` const expanded = await jsonld.expand(data); ``` diff --git a/lib/context.js b/lib/context.js index 2f740bd9..0165ed11 100644 --- a/lib/context.js +++ b/lib/context.js @@ -479,6 +479,7 @@ api.createTermDefinition = ({ if(options.eventHandler) { _handleEvent({ event: { + type: ['JsonLdEvent'], code: 'invalid reserved term', level: 'warning', message: @@ -573,6 +574,7 @@ api.createTermDefinition = ({ if(options.eventHandler) { _handleEvent({ event: { + type: ['JsonLdEvent'], code: 'invalid reserved value', level: 'warning', message: @@ -621,6 +623,7 @@ api.createTermDefinition = ({ if(options.eventHandler) { _handleEvent({ event: { + type: ['JsonLdEvent'], code: 'invalid reserved value', level: 'warning', message: @@ -1155,6 +1158,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { if(options.eventHandler) { _handleEvent({ event: { + type: ['JsonLdEvent'], code: 'relative IRI after expansion', level: 'warning', message: 'Expansion resulted in a relative IRI.', diff --git a/lib/events.js b/lib/events.js index 8d3745aa..68e0fce8 100644 --- a/lib/events.js +++ b/lib/events.js @@ -34,7 +34,7 @@ api.defaultEventHandler = null; api.setupEventHandler = ({options = {}}) => { // build in priority order const eventHandler = [].concat( - options.safe ? api.safeModeEventHandler : [], + options.safe ? api.safeEventHandler : [], options.eventHandler ? _asArray(options.eventHandler) : [], api.defaultEventHandler ? api.defaultEventHandler : [] ); @@ -100,27 +100,43 @@ function _handle({event, handlers}) { return doNext; } +const _notSafeEventCodes = new Set([ + 'dropping object with only @id', + 'dropping object with only @list', + 'dropping object with only @value', + 'dropping empty object', + 'dropping free-floating scalar', + 'invalid @language value', + 'invalid property expansion', + 'no value after expansion', + 'relative IRI after expansion' +]); + // safe handler that rejects unsafe warning conditions -api.safeModeEventHandler = function safeModeEventHandler({event, next}) { +api.safeEventHandler = function safeEventHandler({event, next}) { // fail on all unsafe warnings - if(event.level === 'warning' && - event.tags && event.tags.includes('unsafe')) { + if(event.level === 'warning' && _notSafeEventCodes.has(event.code)) { throw new JsonLdError( - 'Safe mode violation.', - 'jsonld.SafeModeViolationEvent', + 'Safe mode validation error.', + 'jsonld.ValidationError', {event} ); } next(); }; +const _notStrictEventCodes = new Set([ + ..._notSafeEventCodes, + 'invalid reserved term' +]); + // strict handler that rejects all warning conditions -api.strictModeEventHandler = function strictModeEventHandler({event, next}) { +api.strictEventHandler = function strictEventHandler({event, next}) { // fail on all warnings - if(event.level === 'warning') { + if(event.level === 'warning' && _notStrictEventCodes.has(event.code)) { throw new JsonLdError( - 'Strict mode violation.', - 'jsonld.StrictModeViolationEvent', + 'Strict mode validation error.', + 'jsonld.ValidationError', {event} ); } diff --git a/lib/expand.js b/lib/expand.js index 06f34912..9ce4df33 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -106,6 +106,21 @@ api.expand = async ({ insideList }); if(mapped === undefined) { + // FIXME + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'dropping free-floating scalar', + level: 'warning', + message: 'Dropping a free-floating scalar not in a list.', + details: { + value: element + } + }, + options + }); + } return null; } return mapped; @@ -148,6 +163,21 @@ api.expand = async ({ insideList }); if(e === undefined) { + // FIXME name, desc + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'no value after expansion', + level: 'warning', + message: 'Expansion did not result in any value.', + details: { + value: element[i] + } + }, + options + }); + } continue; } } @@ -317,6 +347,21 @@ api.expand = async ({ if(mapped !== undefined) { rval = mapped; } else { + // FIXME + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'no value after expansion', + level: 'warning', + message: 'Dropping null value from expansion.', + details: { + value: rval + } + }, + options + }); + } rval = null; } } else if(!values.every(v => (_isString(v) || _isEmptyObject(v))) && @@ -365,6 +410,21 @@ api.expand = async ({ if(mapped !== undefined) { rval = mapped; } else { + // FIXME + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'dropping object with only @language', + level: 'warning', + message: 'Dropping object with only @language.', + details: { + value: rval + } + }, + options + }); + } rval = null; } } @@ -388,6 +448,37 @@ api.expand = async ({ if(mapped !== undefined) { rval = mapped; } else { + // FIXME + if(options.eventHandler) { + // FIXME: one event or diff event for empty, @v/@l, {@id}? + let code; + let message; + if(count === 0) { + code = 'dropping empty object'; + message = 'Dropping empty object.'; + } else if('@value' in rval) { + code = 'dropping object with only @value'; + message = 'Dropping object with only @value.'; + } else if('@list' in rval) { + code = 'dropping object with only @list'; + message = 'Dropping object with only @list.'; + } else if(count === 1 && '@id' in rval) { + code = 'dropping object with only @id'; + message = 'Dropping object with only @id.'; + } + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code, + level: 'warning', + message, + details: { + value: rval + } + }, + options + }); + } rval = null; } } @@ -454,6 +545,7 @@ async function _expandObject({ if(expandedProperty === null || !(_isAbsoluteIri(expandedProperty) || _isKeyword(expandedProperty))) { // TODO: use `await` to support async + const _expandedProperty = expandedProperty; expandedProperty = expansionMap({ unmappedProperty: key, activeCtx, @@ -468,12 +560,13 @@ async function _expandObject({ if(options.eventHandler) { _handleEvent({ event: { + type: ['JsonLdEvent'], code: 'invalid property expansion', level: 'warning', message: 'Invalid expansion for property.', details: { - // FIXME: include expandedProperty before mapping - property: key + property: key, + expandedProperty: _expandedProperty } }, options @@ -633,6 +726,7 @@ async function _expandObject({ if(options.eventHandler) { _handleEvent({ event: { + type: ['JsonLdEvent'], code: 'invalid @language value', level: 'warning', message: '@language value must be valid BCP47.', diff --git a/lib/fromRdf.js b/lib/fromRdf.js index 70965328..554ef532 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -344,6 +344,7 @@ function _RDFToObject(o, useNativeTypes, rdfDirection, options) { if(options.eventHandler) { _handleEvent({ event: { + type: ['JsonLdEvent'], code: 'invalid @language value', level: 'warning', message: '@language value must be valid BCP47.', diff --git a/lib/jsonld.js b/lib/jsonld.js index f16b255d..38c39abb 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -83,9 +83,10 @@ const { const { logEventHandler: _logEventHandler, logWarningEventHandler: _logWarningEventHandler, + safeEventHandler: _safeEventHandler, setDefaultEventHandler: _setDefaultEventHandler, setupEventHandler: _setupEventHandler, - strictModeEventHandler: _strictModeEventHandler, + strictEventHandler: _strictEventHandler, unhandledEventHandler: _unhandledEventHandler } = require('./events'); @@ -1018,8 +1019,9 @@ jsonld.url = require('./url'); /* Events API and handlers */ jsonld.logEventHandler = _logEventHandler; jsonld.logWarningEventHandler = _logWarningEventHandler; +jsonld.safeEventHandler = _safeEventHandler; jsonld.setDefaultEventHandler = _setDefaultEventHandler; -jsonld.strictModeEventHandler = _strictModeEventHandler; +jsonld.strictEventHandler = _strictEventHandler; jsonld.unhandledEventHandler = _unhandledEventHandler; /* Utility API */ diff --git a/tests/misc.js b/tests/misc.js index 2eeb021c..d496484e 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -578,7 +578,12 @@ describe('events', () => { mapLog, eventCounts, eventLog, - options + options, + testSafe, + testNotSafe, + testStrict, + testNotStrict, + verbose }) { const maps = {counts: {}, log: []}; const expansionMap = info => { @@ -609,6 +614,17 @@ describe('events', () => { error = e; } + if(verbose) { + console.log(JSON.stringify({ + type, + input, + options, + expected, + result, + maps, + events + }, null, 2)); + } if(exception) { assert(error); assert.equal(error.name, exception); @@ -631,29 +647,44 @@ describe('events', () => { if(mapLog) { assert.deepStrictEqual(events.log, eventLog); } - } - - // test passes with safe=true - async function _testSafe({ - type, - input - }) { - await _test({type, input, options: {safe: true}}); - } + // test passes with safe=true + if(testSafe) { + await _test({type, input, options: {...options, safe: true}}); + } + // test fails with safe=true + if(testNotSafe) { + let error; + try { + await _test({type, input, options: {...options, safe: true}}); + } catch(e) { + error = e; + } - // test fails with safe=true - async function _testUnsafe({ - type, - input - }) { - let error; - try { - await _test({type, input, options: {safe: true}}); - } catch(e) { - error = e; + assert(error); + } + // test passes with strict event handler + if(testStrict) { + await _test({ + type, input, options: { + eventHandler: jsonld.strictEventHandler + } + }); } + // test fails with strict event handler + if(testNotStrict) { + let error; + try { + await _test({ + type, input, options: { + eventHandler: jsonld.strictEventHandler + } + }); + } catch(e) { + error = e; + } - assert(error); + assert(error); + } } describe('event system', () => { @@ -677,10 +708,11 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(counts, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'relative IRI after expansion': 2 }, - events: 3 + events: 4 }); // reset default @@ -707,10 +739,11 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(counts, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'invalid reserved term': 1 }, - events: 2 + events: 3 }); }); @@ -791,17 +824,19 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(handlerCounts0, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'invalid reserved term': 1 }, - events: 2 + events: 3 }, 'counts handler 0'); assert.deepStrictEqual(handlerCounts1, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'invalid reserved term': 1 }, - events: 2 + events: 3 }, 'counts handler 1'); assert.deepStrictEqual(handledCounts, { codes: { @@ -843,10 +878,11 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(handlerCounts0, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'invalid reserved term': 1 }, - events: 2 + events: 3 }, 'counts handler 0'); assert.deepStrictEqual(handlerCounts1, {}, 'counts handler 1'); assert.deepStrictEqual(handledCounts, {}, 'counts handled'); @@ -925,24 +961,27 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(handlerCounts0, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'invalid reserved term': 1 }, - events: 2 + events: 3 }, 'counts handler 0'); assert.deepStrictEqual(handlerCounts1, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'invalid reserved term': 1 }, - events: 2 + events: 3 }, 'counts handler 1'); assert.deepStrictEqual(handlerCounts2, { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'invalid reserved term': 1 }, - events: 2 + events: 3 }, 'counts handler 2'); assert.deepStrictEqual(handlerCounts3, { codes: { @@ -1080,15 +1119,34 @@ describe('events', () => { }); describe('unmappedValue', () => { - it('should have zero counts with empty input', async () => { - const docWithNoContent = {}; + it('should have zero counts with empty list', async () => { + const input = []; const expected = []; console.error('FIXME'); await _test({ type: 'expand', - input: docWithNoContent, + input, + expected, + mapCounts: {}, + // FIXME + eventCounts: {}, + // FIXME + testSafe: true, + testStrict: true + }); + }); + + it('should have zero counts with empty object', async () => { + const input = {}; + const expected = []; + + console.error('FIXME'); + + await _test({ + type: 'expand', + input, expected, mapCounts: { expansionMap: 1, @@ -1096,12 +1154,19 @@ describe('events', () => { '__unknown__': 1 } }, - eventCounts: {} + eventCounts: { + codes: { + 'dropping empty object': 1 + }, + events: 1 + }, + testNotSafe: true, + testNotStrict: true }); }); it('should have zero counts with no terms', async () => { - const docWithNoTerms = + const input = { "@context": { "definedTerm": "https://example.com#definedTerm" @@ -1113,7 +1178,7 @@ describe('events', () => { console.error('FIXME'); await _test({ type: 'expand', - input: docWithNoTerms, + input, expected, mapCounts: { expansionMap: 1, @@ -1121,7 +1186,14 @@ describe('events', () => { '__unknown__': 1 } }, - eventCounts: {} + eventCounts: { + codes: { + 'dropping empty object': 1 + }, + events: 1 + }, + testNotSafe: true, + testNotStrict: false }); }); @@ -1165,7 +1237,14 @@ describe('events', () => { 'http://example.com/free-floating-node': 2 } }, - eventCounts: {} + eventCounts: { + codes: { + 'dropping free-floating scalar': 1, + 'dropping object with only @id': 1, + 'no value after expansion': 2 + }, + events: 4 + } }); }); @@ -1198,7 +1277,15 @@ describe('events', () => { 'http://example.com/free-floating-node': 2 } }, - eventCounts: {} + eventCounts: { + codes: { + 'dropping free-floating scalar': 1, + 'dropping object with only @id': 1, + 'dropping object with only @list': 1, + 'no value after expansion': 2 + }, + events: 5 + } }); }); @@ -1223,7 +1310,13 @@ describe('events', () => { '__unknown__': 3 } }, - eventCounts: {} + eventCounts: { + codes: { + 'dropping empty object': 1, + 'no value after expansion': 1 + }, + events: 2 + } }); }); @@ -1248,7 +1341,13 @@ describe('events', () => { '__unknown__': 2 } }, - eventCounts: {} + eventCounts: { + codes: { + 'dropping empty object': 1, + 'dropping object with only @language': 1, + }, + events: 2 + } }); }); }); @@ -1338,10 +1437,11 @@ describe('events', () => { }, eventCounts: { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'relative IRI after expansion': 2 }, - events: 3 + events: 4 }, eventLog: [ { @@ -1365,9 +1465,10 @@ describe('events', () => { }, level: 'warning' } - ] + ], + testNotSafe: true, + testNotStrict: true }); - await _testUnsafe({type: 'expand', input}); }); it('should be called on unmapped term with context [1]', async () => { @@ -1399,10 +1500,11 @@ describe('events', () => { }, eventCounts: { codes: { + 'dropping empty object': 1, 'invalid property expansion': 1, 'relative IRI after expansion': 2 }, - events: 3 + events: 4 } }); }); @@ -1525,9 +1627,10 @@ describe('events', () => { }, eventCounts: { codes: { + 'dropping object with only @id': 1, 'relative IRI after expansion': 1 }, - events: 1 + events: 2 } }); }); @@ -2094,9 +2197,10 @@ describe('events', () => { }, eventCounts: { codes: { + 'dropping object with only @id': 1, 'relative IRI after expansion': 1 }, - events: 1 + events: 2 } }); }); @@ -2134,9 +2238,10 @@ describe('events', () => { }, eventCounts: { codes: { + 'dropping object with only @id': 1, 'relative IRI after expansion': 1 }, - events: 1 + events: 2 } }); }); @@ -2394,7 +2499,12 @@ describe('events', () => { 'http://example.com/relativeIri': 1 } }, - eventCounts: {}, + eventCounts: { + codes: { + 'dropping object with only @id': 1 + }, + events: 1 + }, eventMap: [ { prependedIri: { @@ -2438,7 +2548,12 @@ describe('events', () => { 'http://example.com/relativeIri': 1 } }, - eventCounts: {}, + eventCounts: { + codes: { + 'dropping object with only @id': 1 + }, + events: 1 + }, eventLog: [ { prependedIri: { @@ -2449,7 +2564,9 @@ describe('events', () => { result: 'http://example.com/relativeIri' } } - ] + ], + testNotSafe: true, + testNotStrict: true }); }); From 0d006f8bb4843c0a8e74119e18d3c89f62a2fb8c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 21:05:20 -0400 Subject: [PATCH 075/181] Fix tests typo. - Fix log typo. - Update tests. --- tests/misc.js | 90 ++++----------------------------------------------- 1 file changed, 7 insertions(+), 83 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index d496484e..d9f4ec68 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -644,7 +644,7 @@ describe('events', () => { if(eventCounts) { assert.deepStrictEqual(events.counts, eventCounts); } - if(mapLog) { + if(eventLog) { assert.deepStrictEqual(events.log, eventLog); } // test passes with safe=true @@ -2365,18 +2365,7 @@ describe('events', () => { term: 4 } }, - eventCounts: {}, - eventLog: [ - { - prependedIri: { - type: '@vocab', - vocab: 'http://example.com/', - value: 'term', - typeExpansion: false, - result: 'http://example.com/term' - } - } - ] + eventCounts: {} }); }); @@ -2410,18 +2399,7 @@ describe('events', () => { relativeIri: 2 } }, - eventCounts: {}, - eventLog: [ - { - prependedIri: { - type: '@vocab', - vocab: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - } - } - ] + eventCounts: {} }); }); @@ -2456,18 +2434,7 @@ describe('events', () => { relativeIri: 2 } }, - eventCounts: {}, - eventLog: [ - { - prependedIri: { - type: '@vocab', - vocab: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - } - } - ] + eventCounts: {} }); }); @@ -2504,18 +2471,7 @@ describe('events', () => { 'dropping object with only @id': 1 }, events: 1 - }, - eventMap: [ - { - prependedIri: { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: false, - result: 'http://example.com/relativeIri' - } - } - ] + } }); }); @@ -2554,17 +2510,6 @@ describe('events', () => { }, events: 1 }, - eventLog: [ - { - prependedIri: { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: false, - result: 'http://example.com/relativeIri' - } - } - ], testNotSafe: true, testNotStrict: true }); @@ -2609,18 +2554,7 @@ describe('events', () => { }, events: 1, //FIXME: true - }, - eventLog: [ - { - prependedIri: { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - } - } - ] + } }); }); @@ -2665,17 +2599,7 @@ describe('events', () => { events: 1, //FIXME: true }, - eventLog: [ - { - prependedIri: { - type: '@base', - base: 'http://example.com/', - value: 'relativeIri', - typeExpansion: true, - result: 'http://example.com/relativeIri' - } - } - ] + eventLog: [] }); }); }); From d2ef05d9339cae831243290bdb717ec1a7f3cae0 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 21:09:42 -0400 Subject: [PATCH 076/181] Avoid _expandIri event during `@json` type check. - Add event filter to avoid reletive reference event while expanding to check if a value is `@json`. - Update tests. - Add safe/strict testing flags. --- lib/expand.js | 14 +++- tests/misc.js | 173 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 127 insertions(+), 60 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index 9ce4df33..cc2c88bd 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -527,7 +527,19 @@ async function _expandObject({ const isJsonType = element[typeKey] && _expandIri(activeCtx, (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), - {vocab: true}, {...options, typeExpansion: true}) === '@json'; + {vocab: true}, { + ...options, + typeExpansion: true, + eventHandler: [ + // filter to avoid relative reference events + ({event, next}) => { + if(event.code !== 'relative IRI after expansion') { + next(); + } + }, + options.eventHandler + ] + }) === '@json'; for(const key of keys) { let value = element[key]; diff --git a/tests/misc.js b/tests/misc.js index d9f4ec68..93f7a3e3 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1193,7 +1193,7 @@ describe('events', () => { events: 1 }, testNotSafe: true, - testNotStrict: false + testNotStrict: true }); }); @@ -1244,7 +1244,9 @@ describe('events', () => { 'no value after expansion': 2 }, events: 4 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1285,7 +1287,9 @@ describe('events', () => { 'no value after expansion': 2 }, events: 5 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1316,7 +1320,9 @@ describe('events', () => { 'no value after expansion': 1 }, events: 2 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1347,7 +1353,9 @@ describe('events', () => { 'dropping object with only @language': 1, }, events: 2 - } + }, + testNotSafe: true, + testNotStrict: true }); }); }); @@ -1376,7 +1384,9 @@ describe('events', () => { input, expected, mapCounts: {}, - eventCounts: {} + eventCounts: {}, + testSafe: true, + testStrict: true }); }); @@ -1406,7 +1416,9 @@ describe('events', () => { input, expected, mapCounts: {}, - eventCounts: {} + eventCounts: {}, + testSafe: true, + testStrict: true }); }); @@ -1461,9 +1473,17 @@ describe('events', () => { { code: 'invalid property expansion', details: { + expandedProperty: 'testUndefined', property: 'testUndefined' }, level: 'warning' + }, + { + code: 'dropping empty object', + level: 'warning', + details: { + value: {} + } } ], testNotSafe: true, @@ -1505,7 +1525,9 @@ describe('events', () => { 'relative IRI after expansion': 2 }, events: 4 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1550,7 +1572,9 @@ describe('events', () => { 'relative IRI after expansion': 2 }, events: 3 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1594,7 +1618,9 @@ describe('events', () => { 'relative IRI after expansion': 2 }, events: 3 - } + }, + testNotSafe: true, + testNotStrict: true }); }); }); @@ -1631,7 +1657,9 @@ describe('events', () => { 'relative IRI after expansion': 1 }, events: 2 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1673,7 +1701,9 @@ describe('events', () => { 'relative IRI after expansion': 1 }, events: 1 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1718,7 +1748,9 @@ describe('events', () => { 'relative IRI after expansion': 1 }, events: 1 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1765,7 +1797,9 @@ describe('events', () => { 'relative IRI after expansion': 1 }, events: 1 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1811,7 +1845,9 @@ describe('events', () => { 'relative IRI after expansion': 1 }, events: 1 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1861,10 +1897,12 @@ describe('events', () => { eventCounts: { codes: { 'invalid property expansion': 1, - 'relative IRI after expansion': 4 + 'relative IRI after expansion': 3 }, - events: 5 - } + events: 4 + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1925,10 +1963,12 @@ describe('events', () => { eventCounts: { codes: { 'invalid property expansion': 1, - 'relative IRI after expansion': 4 + 'relative IRI after expansion': 3 }, - events: 5 - } + events: 4 + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -1982,10 +2022,12 @@ describe('events', () => { eventCounts: { codes: { 'invalid property expansion': 1, - 'relative IRI after expansion': 5 + 'relative IRI after expansion': 4 }, - events: 6 - } + events: 5 + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2049,10 +2091,12 @@ describe('events', () => { eventCounts: { codes: { 'invalid property expansion': 1, - 'relative IRI after expansion': 5 + 'relative IRI after expansion': 4 }, - events: 6 - } + events: 5 + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2104,10 +2148,12 @@ describe('events', () => { eventCounts: { codes: { 'invalid property expansion': 1, - 'relative IRI after expansion': 4 + 'relative IRI after expansion': 3 }, - events: 5 - } + events: 4 + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2157,10 +2203,12 @@ describe('events', () => { eventCounts: { codes: { 'invalid property expansion': 1, - 'relative IRI after expansion': 4 + 'relative IRI after expansion': 3 }, - events: 5 - } + events: 4 + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2201,7 +2249,9 @@ describe('events', () => { 'relative IRI after expansion': 1 }, events: 2 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2242,7 +2292,9 @@ describe('events', () => { 'relative IRI after expansion': 1 }, events: 2 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2281,10 +2333,12 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 2 + 'relative IRI after expansion': 1 }, - events: 2 - } + events: 1 + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2325,10 +2379,12 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 3 + 'relative IRI after expansion': 2 }, - events: 3 - } + events: 2 + }, + testNotSafe: true, + testNotStrict: true }); }); }); @@ -2365,7 +2421,9 @@ describe('events', () => { term: 4 } }, - eventCounts: {} + eventCounts: {}, + testSafe: true, + testStrict: true }); }); @@ -2399,7 +2457,9 @@ describe('events', () => { relativeIri: 2 } }, - eventCounts: {} + eventCounts: {}, + testSafe: true, + testStrict: true }); }); @@ -2434,7 +2494,9 @@ describe('events', () => { relativeIri: 2 } }, - eventCounts: {} + eventCounts: {}, + testSafe: true, + testStrict: true }); }); @@ -2471,7 +2533,9 @@ describe('events', () => { 'dropping object with only @id': 1 }, events: 1 - } + }, + testNotSafe: true, + testNotStrict: true }); }); @@ -2548,13 +2612,10 @@ describe('events', () => { relativeIri: 1 } }, - eventCounts: { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1, - //FIXME: true - } + eventCounts: {}, + // FIXME + testSafe: true, + testStrict: true }); }); @@ -2592,13 +2653,7 @@ describe('events', () => { relativeIri: 1 } }, - eventCounts: { - codes: { - 'relative IRI after expansion': 1 - }, - events: 1, - //FIXME: true - }, + eventCounts: {}, eventLog: [] }); }); From 4a20b6f5eeb96a276e4171ed1af8200a4666525f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 22:09:47 -0400 Subject: [PATCH 077/181] Check default `@language` BCP47 format. - Emit event when invalid. - Add tests. --- lib/context.js | 17 +++++++++++ lib/expand.js | 2 +- lib/util.js | 2 ++ tests/misc.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/lib/context.js b/lib/context.js index 0165ed11..6c7dec21 100644 --- a/lib/context.js +++ b/lib/context.js @@ -24,6 +24,7 @@ const { } = require('./events'); const { + REGEX_BCP47, asArray: _asArray, compareShortestLeast: _compareShortestLeast } = require('./util'); @@ -243,6 +244,22 @@ api.process = async ({ 'jsonld.SyntaxError', {code: 'invalid default language', context: ctx}); } else { + if(!value.match(REGEX_BCP47)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'invalid @language value', + level: 'warning', + message: '@language value must be valid BCP47.', + details: { + language: value + } + }, + options + }); + } + } rval['@language'] = value.toLowerCase(); } defined.set('@language', true); diff --git a/lib/expand.js b/lib/expand.js index cc2c88bd..b87def0b 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -33,6 +33,7 @@ const { } = require('./url'); const { + REGEX_BCP47, addValue: _addValue, asArray: _asArray, getValues: _getValues, @@ -45,7 +46,6 @@ const { const api = {}; module.exports = api; -const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; /** * Recursively expands an element using the given context. Any context in diff --git a/lib/util.js b/lib/util.js index 1458005a..dea63cec 100644 --- a/lib/util.js +++ b/lib/util.js @@ -10,6 +10,7 @@ const IdentifierIssuer = require('rdf-canonize').IdentifierIssuer; const JsonLdError = require('./JsonLdError'); // constants +const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; const REGEX_LINK_HEADERS = /(?:<[^>]*?>|"[^"]*?"|[^,])+/g; const REGEX_LINK_HEADER = /\s*<([^>]*?)>\s*(?:;\s*(.*))?/; const REGEX_LINK_HEADER_PARAMS = @@ -24,6 +25,7 @@ const DEFAULTS = { const api = {}; module.exports = api; api.IdentifierIssuer = IdentifierIssuer; +api.REGEX_BCP47 = REGEX_BCP47; /** * Clones an object, array, Map, Set, or string/number. If a typed JavaScript diff --git a/tests/misc.js b/tests/misc.js index 93f7a3e3..e574da66 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1358,6 +1358,84 @@ describe('events', () => { testNotStrict: true }); }); + + it('should emit for invalid @language value', async () => { + const input = +{ + "urn:property": { + "@language": "en_us", + "@value": "test" + } +} +; + const expected = +[ + { + "urn:property": [ + { + "@language": "en_us", + "@value": "test" + } + ] + } +] +; + + console.error('FIXME'); + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, + testNotSafe: true, + testNotStrict: true + }); + }); + + it('should emit for invalid default @language value', async () => { + const input = +{ + "@context": { + "@language": "en_us" + }, + "urn:property": "value" +} +; + const expected = +[ + { + "urn:property": [ + { + "@language": "en_us", + "@value": "value" + } + ] + } +] +; + + console.error('FIXME'); + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, + testNotSafe: true, + testNotStrict: true + }); + }); }); describe('unmappedProperty', () => { From 9017c62a43a4487b0d19029aec01d37d02cf646f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 22:10:23 -0400 Subject: [PATCH 078/181] Add full even test for reserved terms. --- tests/misc.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/misc.js b/tests/misc.js index e574da66..db67fc9c 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1701,6 +1701,43 @@ describe('events', () => { testNotStrict: true }); }); + + it('should be called on invalid reserved term', async () => { + const input = +{ + "@context": { + "@RESERVED": "ex:test-function-handler" + }, + "@RESERVED": "test" +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + unmappedProperty: { + '@RESERVED': 1 + }, + unmappedValue: { + '__unknown__': 1 + } + }, + eventCounts: { + codes: { + 'dropping empty object': 1, + 'invalid property expansion': 1, + 'invalid reserved term': 1 + }, + events: 3 + }, + testNotSafe: true, + testNotStrict: true + }); + }); }); // FIXME naming From 03994f76717fb50d126988c6d872e43c38d751d5 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 22:23:13 -0400 Subject: [PATCH 079/181] Clean up events. - Move 'invalid reserved term' to safe validator. - Improve asserts. - Update test names. --- lib/events.js | 6 +++--- tests/misc.js | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/events.js b/lib/events.js index 68e0fce8..7761b43d 100644 --- a/lib/events.js +++ b/lib/events.js @@ -101,13 +101,14 @@ function _handle({event, handlers}) { } const _notSafeEventCodes = new Set([ + 'dropping empty object', + 'dropping free-floating scalar', 'dropping object with only @id', 'dropping object with only @list', 'dropping object with only @value', - 'dropping empty object', - 'dropping free-floating scalar', 'invalid @language value', 'invalid property expansion', + 'invalid reserved term', 'no value after expansion', 'relative IRI after expansion' ]); @@ -127,7 +128,6 @@ api.safeEventHandler = function safeEventHandler({event, next}) { const _notStrictEventCodes = new Set([ ..._notSafeEventCodes, - 'invalid reserved term' ]); // strict handler that rejects all warning conditions diff --git a/tests/misc.js b/tests/misc.js index db67fc9c..c2e01e47 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -660,7 +660,7 @@ describe('events', () => { error = e; } - assert(error); + assert(error, 'missing safe validation error'); } // test passes with strict event handler if(testStrict) { @@ -683,7 +683,7 @@ describe('events', () => { error = e; } - assert(error); + assert(error, 'missing strict validation error'); } } @@ -1118,7 +1118,7 @@ describe('events', () => { }); }); - describe('unmappedValue', () => { + describe('values', () => { it('should have zero counts with empty list', async () => { const input = []; const expected = []; @@ -1197,7 +1197,7 @@ describe('events', () => { }); }); - it('should notify for @set free-floating scaler', async () => { + it('should emit for @set free-floating scaler', async () => { const input = { "@set": [ @@ -1250,7 +1250,7 @@ describe('events', () => { }); }); - it('should notify for @list free-floating scaler', async () => { + it('should emit for @list free-floating scaler', async () => { const input = { "@list": [ @@ -1293,7 +1293,7 @@ describe('events', () => { }); }); - it('should notify for null @value', async () => { + it('should emit for null @value', async () => { const input = { "urn:property": { @@ -1326,7 +1326,7 @@ describe('events', () => { }); }); - it('should notify for @language alone', async () => { + it('should emit for @language alone', async () => { const input = { "urn:property": { @@ -1438,7 +1438,7 @@ describe('events', () => { }); }); - describe('unmappedProperty', () => { + describe('properties', () => { it('should have zero counts with absolute term', async () => { const input = { From c77ca7c1c17f04b48087123497b48b235fa0f906 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 23:32:02 -0400 Subject: [PATCH 080/181] Add language map BCP47 event and test. --- lib/expand.js | 29 +++++++++++++++++++++++- tests/misc.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index b87def0b..046bad8b 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -1139,7 +1139,18 @@ function _expandLanguageMap(activeCtx, languageMap, direction, options) { const rval = []; const keys = Object.keys(languageMap).sort(); for(const key of keys) { - const expandedKey = _expandIri(activeCtx, key, {vocab: true}, options); + const expandedKey = _expandIri(activeCtx, key, {vocab: true}, { + ...options, + eventHandler: [ + // filter to avoid relative reference events + ({event, next}) => { + if(event.code !== 'relative IRI after expansion') { + next(); + } + }, + options.eventHandler + ] + }); let val = languageMap[key]; if(!_isArray(val)) { val = [val]; @@ -1157,6 +1168,22 @@ function _expandLanguageMap(activeCtx, languageMap, direction, options) { } const val = {'@value': item}; if(expandedKey !== '@none') { + if(!key.match(REGEX_BCP47)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'invalid @language value', + level: 'warning', + message: '@language value must be valid BCP47.', + details: { + language: key + } + }, + options + }); + } + } val['@language'] = key.toLowerCase(); } if(direction) { diff --git a/tests/misc.js b/tests/misc.js index c2e01e47..38fa90d9 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1363,7 +1363,7 @@ describe('events', () => { const input = { "urn:property": { - "@language": "en_us", + "@language": "en_bad", "@value": "test" } } @@ -1373,7 +1373,7 @@ describe('events', () => { { "urn:property": [ { - "@language": "en_us", + "@language": "en_bad", "@value": "test" } ] @@ -1402,7 +1402,7 @@ describe('events', () => { const input = { "@context": { - "@language": "en_us" + "@language": "en_bad" }, "urn:property": "value" } @@ -1412,7 +1412,7 @@ describe('events', () => { { "urn:property": [ { - "@language": "en_us", + "@language": "en_bad", "@value": "value" } ] @@ -1436,6 +1436,60 @@ describe('events', () => { testNotStrict: true }); }); + + it('should emit for invalid @language map value', async () => { + const input = +{ + "@context": { + "urn:property": { + "@container": "@language" + } + }, + "urn:property": { + "en_bad": "en", + "de": "de" + } +} +; + const expected = +[ + { + "urn:property": [ + { + "@language": "de", + "@value": "de" + }, + { + "@language": "en_bad", + "@value": "en" + } + ] + } +] +; + + console.error('FIXME'); + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 2, + relativeIri: { + de: 1, + en_bad: 1 + } + }, + eventCounts: { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, + testNotSafe: true, + testNotStrict: true + }); + }); }); describe('properties', () => { From 91a25c609b9863f3adb56f4ca5aac8b221cb69fe Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 23:39:46 -0400 Subject: [PATCH 081/181] Remove "strict" mode support. - Unused as everything moved into "safe" mode. --- README.md | 17 ------ lib/events.js | 17 ------ tests/misc.js | 142 ++++++++++++++------------------------------------ 3 files changed, 40 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index 3c395374..0dafc939 100644 --- a/README.md +++ b/README.md @@ -470,29 +470,12 @@ const expanded = await jsonld.expand(data, { }); ``` -#### Strict Validation - -Some data may be valid and "safe" but still have issues that could indicate -data problems. A "strict" validation mode is available that handles more issues -that the "safe" validation mode. This mode may cause false positives so may be -best suited for JSON-LD authoring tools. - -```js -// expand a document in strict mode -const expanded = await jsonld.expand(data, { - eventHandler: jsonld.strictEventHandler -}); -``` - #### Available Handlers Some predefined event handlers are available to use alone or as part of a more complex handler: - **safeEventHandler**: The handler used when `safe` is `true`. -- **strictEventHandler**: A handler that is more strict than the `safe` - handler and also fails on other detectable events related to possible input - issues. - **logEventHandler**: A debugging handler that outputs to the console. - **logWarningHandler**: A debugging handler that outputs `warning` level events to the console. diff --git a/lib/events.js b/lib/events.js index 7761b43d..b76d422d 100644 --- a/lib/events.js +++ b/lib/events.js @@ -126,23 +126,6 @@ api.safeEventHandler = function safeEventHandler({event, next}) { next(); }; -const _notStrictEventCodes = new Set([ - ..._notSafeEventCodes, -]); - -// strict handler that rejects all warning conditions -api.strictEventHandler = function strictEventHandler({event, next}) { - // fail on all warnings - if(event.level === 'warning' && _notStrictEventCodes.has(event.code)) { - throw new JsonLdError( - 'Strict mode validation error.', - 'jsonld.ValidationError', - {event} - ); - } - next(); -}; - // logs all events and continues api.logEventHandler = function logEventHandler({event, next}) { console.log(`EVENT: ${event.message}`, {event}); diff --git a/tests/misc.js b/tests/misc.js index 38fa90d9..51c69a9c 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -581,8 +581,6 @@ describe('events', () => { options, testSafe, testNotSafe, - testStrict, - testNotStrict, verbose }) { const maps = {counts: {}, log: []}; @@ -662,29 +660,6 @@ describe('events', () => { assert(error, 'missing safe validation error'); } - // test passes with strict event handler - if(testStrict) { - await _test({ - type, input, options: { - eventHandler: jsonld.strictEventHandler - } - }); - } - // test fails with strict event handler - if(testNotStrict) { - let error; - try { - await _test({ - type, input, options: { - eventHandler: jsonld.strictEventHandler - } - }); - } catch(e) { - error = e; - } - - assert(error, 'missing strict validation error'); - } } describe('event system', () => { @@ -1133,8 +1108,7 @@ describe('events', () => { // FIXME eventCounts: {}, // FIXME - testSafe: true, - testStrict: true + testSafe: true }); }); @@ -1160,8 +1134,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1192,8 +1165,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1245,8 +1217,7 @@ describe('events', () => { }, events: 4 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1288,8 +1259,7 @@ describe('events', () => { }, events: 5 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1321,8 +1291,7 @@ describe('events', () => { }, events: 2 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1354,8 +1323,7 @@ describe('events', () => { }, events: 2 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1393,8 +1361,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1432,8 +1399,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1486,8 +1452,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); }); @@ -1517,8 +1482,7 @@ describe('events', () => { expected, mapCounts: {}, eventCounts: {}, - testSafe: true, - testStrict: true + testSafe: true }); }); @@ -1549,8 +1513,7 @@ describe('events', () => { expected, mapCounts: {}, eventCounts: {}, - testSafe: true, - testStrict: true + testSafe: true }); }); @@ -1618,8 +1581,7 @@ describe('events', () => { } } ], - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1658,8 +1620,7 @@ describe('events', () => { }, events: 4 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1705,8 +1666,7 @@ describe('events', () => { }, events: 3 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1751,8 +1711,7 @@ describe('events', () => { }, events: 3 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1788,8 +1747,7 @@ describe('events', () => { }, events: 3 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); }); @@ -1827,8 +1785,7 @@ describe('events', () => { }, events: 2 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1871,8 +1828,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1918,8 +1874,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -1967,8 +1922,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2015,8 +1969,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2070,8 +2023,7 @@ describe('events', () => { }, events: 4 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2136,8 +2088,7 @@ describe('events', () => { }, events: 4 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2195,8 +2146,7 @@ describe('events', () => { }, events: 5 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2264,8 +2214,7 @@ describe('events', () => { }, events: 5 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2321,8 +2270,7 @@ describe('events', () => { }, events: 4 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2376,8 +2324,7 @@ describe('events', () => { }, events: 4 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2419,8 +2366,7 @@ describe('events', () => { }, events: 2 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2462,8 +2408,7 @@ describe('events', () => { }, events: 2 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2506,8 +2451,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2552,8 +2496,7 @@ describe('events', () => { }, events: 2 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); }); @@ -2591,8 +2534,7 @@ describe('events', () => { } }, eventCounts: {}, - testSafe: true, - testStrict: true + testSafe: true }); }); @@ -2627,8 +2569,7 @@ describe('events', () => { } }, eventCounts: {}, - testSafe: true, - testStrict: true + testSafe: true }); }); @@ -2664,8 +2605,7 @@ describe('events', () => { } }, eventCounts: {}, - testSafe: true, - testStrict: true + testSafe: true }); }); @@ -2703,8 +2643,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2743,8 +2682,7 @@ describe('events', () => { }, events: 1 }, - testNotSafe: true, - testNotStrict: true + testNotSafe: true }); }); @@ -2783,8 +2721,7 @@ describe('events', () => { }, eventCounts: {}, // FIXME - testSafe: true, - testStrict: true + testSafe: true }); }); @@ -2823,7 +2760,8 @@ describe('events', () => { } }, eventCounts: {}, - eventLog: [] + eventLog: [], + testSafe: true }); }); }); From 2ea461344c46090928ef40e64b4f44e4d07b9dcd Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Apr 2022 00:27:58 -0400 Subject: [PATCH 082/181] Add and test fromRDF event support. - Add another langage check. - Add tests. --- lib/fromRdf.js | 32 +++++++++++---- tests/misc.js | 107 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 8 deletions(-) diff --git a/lib/fromRdf.js b/lib/fromRdf.js index 554ef532..afddfdb9 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -6,7 +6,11 @@ const JsonLdError = require('./JsonLdError'); const graphTypes = require('./graphTypes'); const types = require('./types'); -const util = require('./util'); + +const { + REGEX_BCP47, + addValue: _addValue +} = require('./util'); const { handleEvent: _handleEvent @@ -33,8 +37,6 @@ const { XSD_STRING, } = require('./constants'); -const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; - const api = {}; module.exports = api; @@ -88,12 +90,12 @@ api.fromRDF = async ( } if(p === RDF_TYPE && !useRdfType && objectIsNode) { - util.addValue(node, '@type', o.value, {propertyIsArray: true}); + _addValue(node, '@type', o.value, {propertyIsArray: true}); continue; } const value = _RDFToObject(o, useNativeTypes, rdfDirection, options); - util.addValue(node, p, value, {propertyIsArray: true}); + _addValue(node, p, value, {propertyIsArray: true}); // object may be an RDF list/partial list node but we can't know easily // until all triples are read @@ -152,12 +154,12 @@ api.fromRDF = async ( } if(p === RDF_TYPE && !useRdfType && objectIsId) { - util.addValue(node, '@type', o.value, {propertyIsArray: true}); + _addValue(node, '@type', o.value, {propertyIsArray: true}); continue; } const value = _RDFToObject(o, useNativeTypes); - util.addValue(node, p, value, {propertyIsArray: true}); + _addValue(node, p, value, {propertyIsArray: true}); // object may be an RDF list/partial list node but we can't know easily // until all triples are read @@ -296,6 +298,22 @@ function _RDFToObject(o, useNativeTypes, rdfDirection, options) { // add language if(o.language) { + if(!o.language.match(REGEX_BCP47)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'invalid @language value', + level: 'warning', + message: '@language value must be valid BCP47.', + details: { + language: o.language + } + }, + options + }); + } + } rval['@language'] = o.language; } else { let type = o.datatype.value; diff --git a/tests/misc.js b/tests/misc.js index 51c69a9c..031b4303 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -601,13 +601,16 @@ describe('events', () => { if(eventCounts || eventLog) { opts.eventHandler = eventHandler; } - if(!['expand'].includes(type)) { + if(!['expand', 'fromRDF'].includes(type)) { throw new Error(`Unknown test type: "${type}"`); } try { if(type === 'expand') { result = await jsonld.expand(input, opts); } + if(type === 'fromRDF') { + result = await jsonld.fromRDF(input, opts); + } } catch(e) { error = e; } @@ -2765,4 +2768,106 @@ describe('events', () => { }); }); }); + + describe('fromRDF', () => { + it('should emit for invalid N-Quads @language value', async () => { + // N-Quads with invalid language tag (too long) + // FIXME: should N-Quads parser catch this instead? + const input = +'_:b0 "test"@abcdefghi .' +; + const expected = +[ + { + "@id": "_:b0", + "urn:property": [ + { + "@language": "abcdefghi", + "@value": "test" + } + ] + } +] +; + + console.error('FIXME'); + await _test({ + type: 'fromRDF', + input, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, + testNotSafe: true + }); + }); + + it('should emit for invalid Dataset @language value', async () => { + // dataset with invalid language tag (too long) + // Equivalent N-Quads: + // ' "test"^^ .' + // Using JSON dataset to bypass N-Quads parser checks. + const input = +[ + { + "subject": { + "termType": "NamedNode", + "value": "ex:s" + }, + "predicate": { + "termType": "NamedNode", + "value": "ex:p" + }, + "object": { + "termType": "Literal", + "value": "test", + "datatype": { + "termType": "NamedNode", + "value": "https://www.w3.org/ns/i18n#abcdefghi_rtl" + } + }, + "graph": { + "termType": "DefaultGraph", + "value": "" + } + } +] +; + const expected = +[ + { + "@id": "ex:s", + "ex:p": [ + { + "@value": "test", + "@language": "abcdefghi", + "@direction": "rtl" + } + ] + } +] +; + + await _test({ + type: 'fromRDF', + input, + options: { + rdfDirection: 'i18n-datatype', + }, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'invalid @language value': 1 + }, + events: 1 + }, + testNotSafe: true + }); + }); + }); }); From 2b3240e23d31ae1c71f71404bf754a6fc1fe071a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 8 Apr 2022 01:53:46 -0400 Subject: [PATCH 083/181] Move keword regex to util.js. --- lib/context.js | 10 +++++----- lib/util.js | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/context.js b/lib/context.js index 6c7dec21..a8d41210 100644 --- a/lib/context.js +++ b/lib/context.js @@ -25,13 +25,13 @@ const { const { REGEX_BCP47, + REGEX_KEYWORD, asArray: _asArray, compareShortestLeast: _compareShortestLeast } = require('./util'); const INITIAL_CONTEXT_CACHE = new Map(); const INITIAL_CONTEXT_CACHE_MAX_SIZE = 10000; -const KEYWORD_PATTERN = /^@[a-zA-Z]+$/; const api = {}; module.exports = api; @@ -492,7 +492,7 @@ api.createTermDefinition = ({ 'Invalid JSON-LD syntax; keywords cannot be overridden.', 'jsonld.SyntaxError', {code: 'keyword redefinition', context: localCtx, term}); - } else if(term.match(KEYWORD_PATTERN)) { + } else if(term.match(REGEX_KEYWORD)) { if(options.eventHandler) { _handleEvent({ event: { @@ -587,7 +587,7 @@ api.createTermDefinition = ({ 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } - if(reverse.match(KEYWORD_PATTERN)) { + if(reverse.match(REGEX_KEYWORD)) { if(options.eventHandler) { _handleEvent({ event: { @@ -636,7 +636,7 @@ api.createTermDefinition = ({ if(id === null) { // reserve a null term, which may be protected mapping['@id'] = null; - } else if(!api.isKeyword(id) && id.match(KEYWORD_PATTERN)) { + } else if(!api.isKeyword(id) && id.match(REGEX_KEYWORD)) { if(options.eventHandler) { _handleEvent({ event: { @@ -1019,7 +1019,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { } // ignore non-keyword things that look like a keyword - if(value.match(KEYWORD_PATTERN)) { + if(value.match(REGEX_KEYWORD)) { return null; } diff --git a/lib/util.js b/lib/util.js index dea63cec..57bf9f74 100644 --- a/lib/util.js +++ b/lib/util.js @@ -15,6 +15,7 @@ const REGEX_LINK_HEADERS = /(?:<[^>]*?>|"[^"]*?"|[^,])+/g; const REGEX_LINK_HEADER = /\s*<([^>]*?)>\s*(?:;\s*(.*))?/; const REGEX_LINK_HEADER_PARAMS = /(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/g; +const REGEX_KEYWORD = /^@[a-zA-Z]+$/; const DEFAULTS = { headers: { @@ -26,6 +27,7 @@ const api = {}; module.exports = api; api.IdentifierIssuer = IdentifierIssuer; api.REGEX_BCP47 = REGEX_BCP47; +api.REGEX_KEYWORD = REGEX_KEYWORD; /** * Clones an object, array, Map, Set, or string/number. If a typed JavaScript From facda81237fc33c8838d01633c0978b318b4eb72 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 12 Apr 2022 00:34:18 -0400 Subject: [PATCH 084/181] Update events and tests. - Update event code naming. Similar to JSON-LD error code style. Code name is just the condition detected, not the action that might be taken. - Relative reference events now at call sites. - No-value events now at call sites. - Remove some special-case event handlers. Not needed when events happen outside `_expandIri`. - **BREAKING**: Handle special non-normaitve spec edge case where value for `@id` can expand into null. Behavior changing to *not* output invalid JSON-LD. This can change triples in odd edge cases. - Add eventCodeLog test feature to test sequence of events codes. - Update tests. --- CHANGELOG.md | 26 ++++- lib/context.js | 52 +++++---- lib/events.js | 25 ++-- lib/expand.js | 170 ++++++++++++++++++--------- tests/misc.js | 306 ++++++++++++++++++++++--------------------------- 5 files changed, 318 insertions(+), 261 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 374ef062..75a7fda1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,32 @@ # jsonld ChangeLog +### Changed +- Change EARL Assertor to Digital Bazaar, Inc. +- Update eslint dependencies. +- **BREAKING**: Handle spec edge case where value for `@id` can expand into + null. Behavior is changing to *not* output invalid JSON-LD and *potentially* + outputing an extra blank node. The input in question is something like + `{"@id":"@RESERVED", ...}` where `@RESERVED` will expand into `null`. Rather + than outputing invalid `{"@id": null, ...}`, the new behavior will drop + `@id`. When going to RDF this can cause slightly different output. + Specifically, a `{}` value may exist and create a blank node. Please file an + issue if this behavior causes issues. It is expected to be better addressed + in a future spec. + - Related [issue](https://github.com/w3c/json-ld-api/issues/480). + - Normative [toRdf test case](https://w3c.github.io/json-ld-api/tests/toRdf-manifest.html#te122) + *that now fails*. + - Non-normative [expand test case](https://w3c.github.io/json-ld-api/tests/expand-manifest.html#t0122) + that now fails. + ### Added - Support benchmarks in Karma tests. - 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 +- 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. -- Update eslint dependencies. - ## 6.0.0 - 2022-06-06 ### Changed diff --git a/lib/context.js b/lib/context.js index a8d41210..13411c99 100644 --- a/lib/context.js +++ b/lib/context.js @@ -226,8 +226,25 @@ api.process = async ({ '@context must be an absolute IRI.', 'jsonld.SyntaxError', {code: 'invalid vocab mapping', context: ctx}); } else { - rval['@vocab'] = _expandIri(rval, value, {vocab: true, base: true}, + const vocab = _expandIri(rval, value, {vocab: true, base: true}, undefined, undefined, options); + if(!_isAbsoluteIri(vocab)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative @vocab reference', + level: 'warning', + message: 'Relative @vocab reference found.', + details: { + vocab + } + }, + options + }); + } + } + rval['@vocab'] = vocab; } defined.set('@vocab', true); } @@ -497,10 +514,11 @@ api.createTermDefinition = ({ _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'invalid reserved term', + code: 'reserved term', level: 'warning', message: - 'Terms beginning with "@" are reserved for future use and ignored.', + 'Terms beginning with "@" are ' + + 'reserved for future use and dropped.', details: { term } @@ -592,11 +610,11 @@ api.createTermDefinition = ({ _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'invalid reserved value', + code: 'reserved @reverse value', level: 'warning', message: - 'Values beginning with "@" are reserved for future use and' + - ' ignored.', + '@reverse values beginning with "@" are ' + + 'reserved for future use and dropped.', details: { reverse } @@ -641,11 +659,11 @@ api.createTermDefinition = ({ _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'invalid reserved value', + code: 'reserved @id value', level: 'warning', message: - 'Values beginning with "@" are reserved for future use and' + - ' ignored.', + '@id values beginning with "@" are ' + + 'reserved for future use and dropped.', details: { id } @@ -1171,22 +1189,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { }); if(expandedResult !== undefined) { value = expandedResult; - } else { - if(options.eventHandler) { - _handleEvent({ - event: { - type: ['JsonLdEvent'], - code: 'relative IRI after expansion', - level: 'warning', - message: 'Expansion resulted in a relative IRI.', - details: { - value - } - }, - options - }); - } } + // NOTE: relative reference events emitted at calling sites as needed } return value; diff --git a/lib/events.js b/lib/events.js index b76d422d..237f4e0e 100644 --- a/lib/events.js +++ b/lib/events.js @@ -101,16 +101,23 @@ function _handle({event, handlers}) { } const _notSafeEventCodes = new Set([ - 'dropping empty object', - 'dropping free-floating scalar', - 'dropping object with only @id', - 'dropping object with only @list', - 'dropping object with only @value', + 'empty object', + 'free-floating scalar', 'invalid @language value', - 'invalid property expansion', - 'invalid reserved term', - 'no value after expansion', - 'relative IRI after expansion' + 'invalid property', + // NOTE: spec edge case + 'null @id value', + 'null @value value', + 'object with only @id', + 'object with only @language', + 'object with only @list', + 'object with only @value', + 'relative @id reference', + 'relative @type reference', + 'relative @vocab reference', + 'reserved @id value', + 'reserved @reverse value', + 'reserved term' ]); // safe handler that rejects unsafe warning conditions diff --git a/lib/expand.js b/lib/expand.js index 046bad8b..b3236182 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -34,6 +34,7 @@ const { const { REGEX_BCP47, + REGEX_KEYWORD, addValue: _addValue, asArray: _asArray, getValues: _getValues, @@ -111,9 +112,9 @@ api.expand = async ({ _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'dropping free-floating scalar', + code: 'free-floating scalar', level: 'warning', - message: 'Dropping a free-floating scalar not in a list.', + message: 'Dropping free-floating scalar not in a list.', details: { value: element } @@ -163,21 +164,7 @@ api.expand = async ({ insideList }); if(e === undefined) { - // FIXME name, desc - if(options.eventHandler) { - _handleEvent({ - event: { - type: ['JsonLdEvent'], - code: 'no value after expansion', - level: 'warning', - message: 'Expansion did not result in any value.', - details: { - value: element[i] - } - }, - options - }); - } + // NOTE: no-value events emitted at calling sites as needed continue; } } @@ -352,9 +339,9 @@ api.expand = async ({ _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'no value after expansion', + code: 'null @value value', level: 'warning', - message: 'Dropping null value from expansion.', + message: 'Dropping null @value value.', details: { value: rval } @@ -415,7 +402,7 @@ api.expand = async ({ _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'dropping object with only @language', + code: 'object with only @language', level: 'warning', message: 'Dropping object with only @language.', details: { @@ -454,16 +441,16 @@ api.expand = async ({ let code; let message; if(count === 0) { - code = 'dropping empty object'; + code = 'empty object'; message = 'Dropping empty object.'; } else if('@value' in rval) { - code = 'dropping object with only @value'; + code = 'object with only @value'; message = 'Dropping object with only @value.'; } else if('@list' in rval) { - code = 'dropping object with only @list'; + code = 'object with only @list'; message = 'Dropping object with only @list.'; } else if(count === 1 && '@id' in rval) { - code = 'dropping object with only @id'; + code = 'object with only @id'; message = 'Dropping object with only @id.'; } _handleEvent({ @@ -529,16 +516,7 @@ async function _expandObject({ (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), {vocab: true}, { ...options, - typeExpansion: true, - eventHandler: [ - // filter to avoid relative reference events - ({event, next}) => { - if(event.code !== 'relative IRI after expansion') { - next(); - } - }, - options.eventHandler - ] + typeExpansion: true }) === '@json'; for(const key of keys) { @@ -573,9 +551,10 @@ async function _expandObject({ _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'invalid property expansion', + code: 'invalid property', level: 'warning', - message: 'Invalid expansion for property.', + message: 'Dropping property that did not expand into an ' + + 'absolute IRI or keyword.', details: { property: key, expandedProperty: _expandedProperty @@ -638,8 +617,61 @@ async function _expandObject({ _addValue( expandedParent, '@id', - _asArray(value).map(v => - _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v), + _asArray(value).map(v => { + if(_isString(v)) { + const ve = _expandIri(activeCtx, v, {base: true}, options); + if(options.eventHandler) { + if(ve === null) { + // NOTE: spec edge case + // See https://github.com/w3c/json-ld-api/issues/480 + if(v === null) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'null @id value', + level: 'warning', + message: 'Null @id found.', + details: { + id: v + } + }, + options + }); + } else { + // matched KEYWORD regex + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'reserved @id value', + level: 'warning', + message: 'Reserved @id found.', + details: { + id: v + } + }, + options + }); + } + } else if(!_isAbsoluteIri(ve)) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative @id reference', + level: 'warning', + message: 'Relative @id reference found.', + details: { + id: v, + expandedId: ve + } + }, + options + }); + } + } + return ve; + } + return v; + }).filter(v => v !== null), {propertyIsArray: options.isFrame}); continue; } @@ -659,11 +691,31 @@ async function _expandObject({ _validateTypeValue(value, options.isFrame); _addValue( expandedParent, '@type', - _asArray(value).map(v => - _isString(v) ? - _expandIri(typeScopedContext, v, + _asArray(value).map(v => { + if(_isString(v)) { + const ve = _expandIri(typeScopedContext, v, {base: true, vocab: true}, - {...options, typeExpansion: true}) : v), + {...options, typeExpansion: true}); + if(!_isAbsoluteIri(ve)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative @type reference', + level: 'warning', + message: 'Relative @type reference found.', + details: { + type: v + } + }, + options + }); + } + } + return ve; + } + return v; + }), {propertyIsArray: options.isFrame}); continue; } @@ -1086,7 +1138,26 @@ function _expandValue({activeCtx, activeProperty, value, options}) { // do @id expansion (automatic for @graph) if((type === '@id' || expandedProperty === '@graph') && _isString(value)) { - return {'@id': _expandIri(activeCtx, value, {base: true}, options)}; + const expandedValue = _expandIri(activeCtx, value, {base: true}, options); + // NOTE: handle spec edge case and avoid invalid {"@id": null} + if(expandedValue === null && value.match(REGEX_KEYWORD)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'reserved @id value', + level: 'warning', + message: 'Reserved @id found.', + details: { + id: activeProperty + } + }, + options + }); + } + return {}; + } + return {'@id': expandedValue}; } // do @id expansion w/vocab if(type === '@vocab' && _isString(value)) { @@ -1139,18 +1210,7 @@ function _expandLanguageMap(activeCtx, languageMap, direction, options) { const rval = []; const keys = Object.keys(languageMap).sort(); for(const key of keys) { - const expandedKey = _expandIri(activeCtx, key, {vocab: true}, { - ...options, - eventHandler: [ - // filter to avoid relative reference events - ({event, next}) => { - if(event.code !== 'relative IRI after expansion') { - next(); - } - }, - options.eventHandler - ] - }); + const expandedKey = _expandIri(activeCtx, key, {vocab: true}, options); let val = languageMap[key]; if(!_isArray(val)) { val = [val]; diff --git a/tests/misc.js b/tests/misc.js index 031b4303..3a811c72 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -572,13 +572,14 @@ describe('events', () => { // expand, compact, frame, fromRdf, toRdf, etc type, input, + options, expected, exception, mapCounts, mapLog, eventCounts, eventLog, - options, + eventCodeLog, testSafe, testNotSafe, verbose @@ -598,7 +599,7 @@ describe('events', () => { if(mapCounts || mapLog) { opts.expansionMap = expansionMap; } - if(eventCounts || eventLog) { + if(eventCounts || eventLog || eventCodeLog) { opts.eventHandler = eventHandler; } if(!['expand', 'fromRDF'].includes(type)) { @@ -648,6 +649,9 @@ describe('events', () => { if(eventLog) { assert.deepStrictEqual(events.log, eventLog); } + if(eventCodeLog) { + assert.deepStrictEqual(events.log.map(e => e.code), eventCodeLog); + } // test passes with safe=true if(testSafe) { await _test({type, input, options: {...options, safe: true}}); @@ -686,11 +690,10 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(counts, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 + 'empty object': 1, + 'invalid property': 1 }, - events: 4 + events: 2 }); // reset default @@ -717,9 +720,9 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(counts, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }); @@ -740,7 +743,7 @@ describe('events', () => { const counts1 = {}; const e0 = await jsonld.expand(d, { eventHandler: { - 'invalid reserved term': ({event}) => { + 'reserved term': ({event}) => { addEventCounts(counts0, event); } } @@ -748,7 +751,7 @@ describe('events', () => { // FIXME: ensure cache is being used const e1 = await jsonld.expand(d, { eventHandler: { - 'invalid reserved term': ({event}) => { + 'reserved term': ({event}) => { addEventCounts(counts1, event); } } @@ -757,13 +760,13 @@ describe('events', () => { assert.deepStrictEqual(e1, ex); assert.deepStrictEqual(counts0, { codes: { - 'invalid reserved term': 1 + 'reserved term': 1 }, events: 1 }, 'counts 0'); assert.deepStrictEqual(counts1, { codes: { - 'invalid reserved term': 1 + 'reserved term': 1 }, events: 1 }, 'counts 1'); @@ -792,7 +795,7 @@ describe('events', () => { }, ({event}) => { addEventCounts(handlerCounts1, event); - if(event.code === 'invalid reserved term') { + if(event.code === 'reserved term') { addEventCounts(handledCounts, event); return; } @@ -802,23 +805,23 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(handlerCounts0, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }, 'counts handler 0'); assert.deepStrictEqual(handlerCounts1, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }, 'counts handler 1'); assert.deepStrictEqual(handledCounts, { codes: { - 'invalid reserved term': 1 + 'reserved term': 1 }, events: 1 }, 'counts handled'); @@ -846,7 +849,7 @@ describe('events', () => { }, ({event}) => { addEventCounts(handlerCounts1, event); - if(event.code === 'invalid reserved term') { + if(event.code === 'reserved term') { addEventCounts(handledCounts, event); return; } @@ -856,9 +859,9 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(handlerCounts0, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }, 'counts handler 0'); @@ -880,7 +883,7 @@ describe('events', () => { const counts = {}; const e = await jsonld.expand(d, { eventHandler: { - 'invalid reserved term': ({event}) => { + 'reserved term': ({event}) => { addEventCounts(counts, event); assert.strictEqual(event.details.term, '@RESERVED'); } @@ -889,7 +892,7 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(counts, { codes: { - 'invalid reserved term': 1 + 'reserved term': 1 }, events: 1 }, 'counts'); @@ -930,7 +933,7 @@ describe('events', () => { next(); }, { - 'invalid reserved term': ({event}) => { + 'reserved term': ({event}) => { addEventCounts(handlerCounts3, event); } } @@ -939,31 +942,31 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(handlerCounts0, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }, 'counts handler 0'); assert.deepStrictEqual(handlerCounts1, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }, 'counts handler 1'); assert.deepStrictEqual(handlerCounts2, { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }, 'counts handler 2'); assert.deepStrictEqual(handlerCounts3, { codes: { - 'invalid reserved term': 1 + 'reserved term': 1 }, events: 1 }, 'counts handler 3'); @@ -997,15 +1000,15 @@ describe('events', () => { ; const handledReservedTermCounts = {}; - const handledReservedValueCounts = {}; + const handledReservedIdValueCounts = {}; const handledLanguageCounts = {}; const e = await jsonld.expand(d, { eventHandler: { - 'invalid reserved term': ({event}) => { + 'reserved term': ({event}) => { addEventCounts(handledReservedTermCounts, event); }, - 'invalid reserved value': ({event}) => { - addEventCounts(handledReservedValueCounts, event); + 'reserved @id value': ({event}) => { + addEventCounts(handledReservedIdValueCounts, event); }, 'invalid @language value': ({event}) => { addEventCounts(handledLanguageCounts, event); @@ -1015,13 +1018,13 @@ describe('events', () => { assert.deepStrictEqual(e, ex); assert.deepStrictEqual(handledReservedTermCounts, { codes: { - 'invalid reserved term': 1 + 'reserved term': 1 }, events: 1 }, 'handled reserved term counts'); - assert.deepStrictEqual(handledReservedValueCounts, { + assert.deepStrictEqual(handledReservedIdValueCounts, { codes: { - 'invalid reserved value': 1 + 'reserved @id value': 1 }, events: 1 }, 'handled reserved value counts'); @@ -1101,26 +1104,20 @@ describe('events', () => { const input = []; const expected = []; - console.error('FIXME'); - await _test({ type: 'expand', input, expected, mapCounts: {}, - // FIXME eventCounts: {}, - // FIXME testSafe: true }); }); - it('should have zero counts with empty object', async () => { + it('should count empty top-level object', async () => { const input = {}; const expected = []; - console.error('FIXME'); - await _test({ type: 'expand', input, @@ -1133,7 +1130,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping empty object': 1 + 'empty object': 1 }, events: 1 }, @@ -1141,7 +1138,7 @@ describe('events', () => { }); }); - it('should have zero counts with no terms', async () => { + it('should count empty top-level object with only context', async () => { const input = { "@context": { @@ -1172,14 +1169,10 @@ describe('events', () => { }); }); - it('should emit for @set free-floating scaler', async () => { + it('should not emit for ok @graph', async () => { const input = { - "@set": [ - "free-floating strings in set objects are removed", - { - "@id": "http://example.com/free-floating-node" - }, + "@graph": [ { "@id": "http://example.com/node", "urn:property": "nodes with properties are not removed" @@ -1200,67 +1193,68 @@ describe('events', () => { ] ; - console.error('FIXME'); await _test({ type: 'expand', input, expected, - mapCounts: { - expansionMap: 4, - unmappedValue: { - '__unknown__': 2, - 'http://example.com/free-floating-node': 2 - } - }, - eventCounts: { - codes: { - 'dropping free-floating scalar': 1, - 'dropping object with only @id': 1, - 'no value after expansion': 2 - }, - events: 4 - }, - testNotSafe: true + mapCounts: {}, + eventCounts: {}, + testSafe: true }); }); - it('should emit for @list free-floating scaler', async () => { + it('should emit for @graph free-floating scaler', async () => { const input = { - "@list": [ - "free-floating strings in list objects are removed", + "@graph": [ + "free-floating strings in set objects are removed", + {}, + { + "@value": "v" + }, { - "@id": "http://example.com/free-floating-node" + "@list": [{ + "urn:p": "lv" + }] }, { "@id": "http://example.com/node", - "urn:property": "nodes are removed with the @list" + "urn:property": "nodes with properties are not removed" } ] } ; - const expected = []; + const expected = +[ + { + "@id": "http://example.com/node", + "urn:property": [ + { + "@value": "nodes with properties are not removed" + } + ] + } +] +; - console.error('FIXME'); await _test({ type: 'expand', input, expected, mapCounts: { - expansionMap: 5, + expansionMap: 8, unmappedValue: { - '__unknown__': 3, - 'http://example.com/free-floating-node': 2 + '__unknown__': 8 } }, eventCounts: { codes: { - 'dropping free-floating scalar': 1, - 'dropping object with only @id': 1, - 'dropping object with only @list': 1, - 'no value after expansion': 2 + 'empty object': 1, + 'free-floating scalar': 1, + 'object with only @list': 1, + 'object with only @value': 1 }, - events: 5 + events: 4 }, testNotSafe: true }); @@ -1289,8 +1283,8 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping empty object': 1, - 'no value after expansion': 1 + 'empty object': 1, + 'null @value value': 1 }, events: 2 }, @@ -1308,7 +1302,6 @@ describe('events', () => { ; const expected = []; - console.error('FIXME'); await _test({ type: 'expand', input, @@ -1321,8 +1314,8 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping empty object': 1, - 'dropping object with only @language': 1, + 'empty object': 1, + 'object with only @language': 1, }, events: 2 }, @@ -1352,7 +1345,6 @@ describe('events', () => { ] ; - console.error('FIXME'); await _test({ type: 'expand', input, @@ -1390,7 +1382,6 @@ describe('events', () => { ] ; - console.error('FIXME'); await _test({ type: 'expand', input, @@ -1437,7 +1428,6 @@ describe('events', () => { ] ; - console.error('FIXME'); await _test({ type: 'expand', input, @@ -1520,7 +1510,6 @@ describe('events', () => { }); }); - // XXX it('should be called on unmapped term with no context', async () => { const input = { @@ -1547,29 +1536,14 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 + 'empty object': 1, + 'invalid property': 1 }, - events: 4 + events: 2 }, eventLog: [ { - code: 'relative IRI after expansion', - details: { - value: 'testUndefined' - }, - level: 'warning' - }, - { - code: 'relative IRI after expansion', - details: { - value: 'testUndefined' - }, - level: 'warning' - }, - { - code: 'invalid property expansion', + code: 'invalid property', details: { expandedProperty: 'testUndefined', property: 'testUndefined' @@ -1577,7 +1551,7 @@ describe('events', () => { level: 'warning' }, { - code: 'dropping empty object', + code: 'empty object', level: 'warning', details: { value: {} @@ -1617,11 +1591,10 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 + 'empty object': 1, + 'invalid property': 1 }, - events: 4 + events: 2 }, testNotSafe: true }); @@ -1664,10 +1637,9 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 + 'invalid property': 1 }, - events: 3 + events: 1 }, testNotSafe: true }); @@ -1709,16 +1681,15 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 2 + 'invalid property': 1 }, - events: 3 + events: 1 }, testNotSafe: true }); }); - it('should be called on invalid reserved term', async () => { + it('should be called on reserved term', async () => { const input = { "@context": { @@ -1744,9 +1715,9 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping empty object': 1, - 'invalid property expansion': 1, - 'invalid reserved term': 1 + 'empty object': 1, + 'invalid property': 1, + 'reserved term': 1 }, events: 3 }, @@ -1783,8 +1754,8 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping object with only @id': 1, - 'relative IRI after expansion': 1 + 'object with only @id': 1, + 'relative @id reference': 1 }, events: 2 }, @@ -1827,7 +1798,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 1 + 'relative @id reference': 1 }, events: 1 }, @@ -1873,7 +1844,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 1 + 'relative @id reference': 1 }, events: 1 }, @@ -1921,7 +1892,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 1 + 'relative @id reference': 1 }, events: 1 }, @@ -1968,7 +1939,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 1 + 'relative @id reference': 1 }, events: 1 }, @@ -2021,10 +1992,10 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 3 + 'invalid property': 1, + 'relative @type reference': 1 }, - events: 4 + events: 2 }, testNotSafe: true }); @@ -2086,10 +2057,10 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 3 + 'invalid property': 1, + 'relative @type reference': 1 }, - events: 4 + events: 2 }, testNotSafe: true }); @@ -2144,10 +2115,10 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 4 + 'invalid property': 1, + 'relative @type reference': 2 }, - events: 5 + events: 3 }, testNotSafe: true }); @@ -2212,10 +2183,10 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 4 + 'invalid property': 1, + 'relative @type reference': 2 }, - events: 5 + events: 3 }, testNotSafe: true }); @@ -2268,10 +2239,10 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 3 + 'invalid property': 1, + 'relative @type reference': 1 }, - events: 4 + events: 2 }, testNotSafe: true }); @@ -2322,10 +2293,10 @@ describe('events', () => { }, eventCounts: { codes: { - 'invalid property expansion': 1, - 'relative IRI after expansion': 3 + 'invalid property': 1, + 'relative @type reference': 1 }, - events: 4 + events: 2 }, testNotSafe: true }); @@ -2364,8 +2335,8 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping object with only @id': 1, - 'relative IRI after expansion': 1 + 'object with only @id': 1, + 'relative @id reference': 1 }, events: 2 }, @@ -2406,8 +2377,8 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping object with only @id': 1, - 'relative IRI after expansion': 1 + 'object with only @id': 1, + 'relative @id reference': 1 }, events: 2 }, @@ -2450,7 +2421,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 1 + 'relative @type reference': 1 }, events: 1 }, @@ -2495,7 +2466,8 @@ describe('events', () => { }, eventCounts: { codes: { - 'relative IRI after expansion': 2 + 'relative @type reference': 1, + 'relative @vocab reference': 1 }, events: 2 }, @@ -2642,7 +2614,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping object with only @id': 1 + 'object with only @id': 1 }, events: 1 }, @@ -2681,7 +2653,7 @@ describe('events', () => { }, eventCounts: { codes: { - 'dropping object with only @id': 1 + 'object with only @id': 1 }, events: 1 }, From e1e678248c41adb5f834b1421ad563ec01fac037 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 12 Apr 2022 00:56:52 -0400 Subject: [PATCH 085/181] Update tests. - More tests updates and fixes. --- tests/misc.js | 671 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 659 insertions(+), 12 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index 3a811c72..63716cde 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1099,6 +1099,330 @@ describe('events', () => { }); }); + describe('reserved', () => { + it('should handle reserved context @id values [1]', async () => { + const input = +{ + "@context": { + "resId": {"@id": "@RESERVED"} + }, + "@id": "ex:id", + "resId": "resIdValue", + "ex:p": "v" +} +; + const expected = +[ + { + "@id": "ex:id", + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + relativeIri: { + resId: 2 + }, + unmappedProperty: { + resId: 1 + } + }, + eventCodeLog: [ + 'reserved @id value', + 'invalid property' + ], + testUnSafe: true + }); + }); + + it('should handle reserved context @id values [2]', async () => { + const input = +{ + "@context": { + "resId": "@RESERVED" + }, + "@id": "ex:id", + "resId": "resIdValue", + "ex:p": "v" +} +; + const expected = +[ + { + "@id": "ex:id", + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + relativeIri: { + resId: 2 + }, + unmappedProperty: { + resId: 1 + } + }, + eventCounts: { + codes: { + 'invalid property': 1, + 'reserved @id value': 1 + }, + events: 2 + }, + testUnSafe: true + }); + }); + + it('should handle reserved content @id values', async () => { + const input = +{ + "@id": "@RESERVED", + "ex:p": "v" +} +; + const expected = +[ + { + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'reserved @id value': 1 + }, + events: 1 + }, + testUnSafe: true + }); + }); + + it('should handle reserved content id values [1]', async () => { + const input = +{ + "@context": { + "p": {"@id": "ex:idp", "@type": "@id"} + }, + "p": "@RESERVED", + "ex:p": "v" +} +; + const expected = +[ + { + "ex:idp": [{}], + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'reserved @id value': 1 + }, + events: 1 + }, + testUnSafe: true + }); + }); + + it('should handle reserved content id values [2]', async () => { + const input = +{ + "@context": { + "id": "@id" + }, + "id": "@RESERVED", + "ex:p": "v" +} +; + const expected = +[ + { + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'reserved @id value': 1 + }, + events: 1 + }, + testUnSafe: true + }); + }); + + it('should handle reserved content id values [3]', async () => { + const input = +{ + "@context": { + "p": {"@id": "ex:idp", "@type": "@id"} + }, + "p": {"@id": "@RESERVED"}, + "ex:p": "v" +} +; + const expected = +[ + { + "ex:idp": [{}], + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: { + codes: { + 'reserved @id value': 1 + }, + events: 1 + }, + testUnSafe: true + }); + }); + + it('should handle reserved context terms', async () => { + const input = +{ + "@context": { + "@RESERVED": "ex:test" + }, + "@RESERVED": "test", + "ex:p": "v" +} +; + const expected = +[ + { + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 1, + unmappedProperty: { + '@RESERVED': 1 + } + }, + eventCounts: { + codes: { + 'invalid property': 1, + 'reserved term': 1 + }, + events: 2 + }, + testUnSafe: true + }); + }); + + it('should handle reserved content terms', async () => { + const input = +{ + "@RESERVED": "test", + "ex:p": "v" +} +; + const expected = +[ + { + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 1, + unmappedProperty: { + '@RESERVED': 1 + } + }, + eventCounts: { + codes: { + 'invalid property': 1, + }, + events: 1 + }, + testUnSafe: true + }); + }); + }); + describe('values', () => { it('should have zero counts with empty list', async () => { const input = []; @@ -1114,8 +1438,155 @@ describe('events', () => { }); }); - it('should count empty top-level object', async () => { - const input = {}; + it('should count empty top-level object', async () => { + const input = {}; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 1, + unmappedValue: { + '__unknown__': 1 + } + }, + eventCounts: { + codes: { + 'empty object': 1 + }, + events: 1 + }, + testNotSafe: true + }); + }); + + it('should count empty top-level object with only context', async () => { + const input = +{ + "@context": { + "definedTerm": "https://example.com#definedTerm" + } +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 1, + unmappedValue: { + '__unknown__': 1 + } + }, + eventCounts: { + codes: { + 'empty object': 1 + }, + events: 1 + }, + testNotSafe: true + }); + }); + + it('should not emit for ok @set', async () => { + const input = +{ + "@set": [ + { + "@id": "http://example.com/node", + "urn:property": "nodes with properties are not removed" + } + ] +} +; + const expected = +[ + { + "@id": "http://example.com/node", + "urn:property": [ + { + "@value": "nodes with properties are not removed" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: {}, + eventCounts: {}, + testSafe: true + }); + }); + + it('should emit for @set free-floating scaler', async () => { + const input = +{ + "@set": [ + "free-floating strings in set objects are removed", + { + "@id": "http://example.com/free-floating-node" + }, + { + "@id": "http://example.com/node", + "urn:property": "nodes with properties are not removed" + } + ] +} +; + const expected = +[ + { + "@id": "http://example.com/node", + "urn:property": [ + { + "@value": "nodes with properties are not removed" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 4, + unmappedValue: { + '__unknown__': 2, + 'http://example.com/free-floating-node': 2 + } + }, + eventCounts: { + codes: { + 'free-floating scalar': 1, + 'object with only @id': 1 + }, + events: 2 + }, + testNotSafe: true + }); + }); + + it('should emit for only @list', async () => { + const input = +{ + "@list": [ + { + "@id": "http://example.com/node", + "urn:property": "nodes are removed with the @list" + } + ] +} +; const expected = []; await _test({ @@ -1125,12 +1596,12 @@ describe('events', () => { mapCounts: { expansionMap: 1, unmappedValue: { - '__unknown__': 1 + '__unknown__': 1, } }, eventCounts: { codes: { - 'empty object': 1 + 'object with only @list': 1 }, events: 1 }, @@ -1138,12 +1609,19 @@ describe('events', () => { }); }); - it('should count empty top-level object with only context', async () => { + it('should emit for @list free-floating scaler', async () => { const input = { - "@context": { - "definedTerm": "https://example.com#definedTerm" - } + "@list": [ + "free-floating strings in list objects are removed", + { + "@id": "http://example.com/free-floating-node" + }, + { + "@id": "http://example.com/node", + "urn:property": "nodes are removed with the @list" + } + ] } ; const expected = []; @@ -1154,16 +1632,19 @@ describe('events', () => { input, expected, mapCounts: { - expansionMap: 1, + expansionMap: 5, unmappedValue: { - '__unknown__': 1 + '__unknown__': 3, + 'http://example.com/free-floating-node': 2 } }, eventCounts: { codes: { - 'dropping empty object': 1 + 'free-floating scalar': 1, + 'object with only @id': 1, + 'object with only @list': 1 }, - events: 1 + events: 3 }, testNotSafe: true }); @@ -1448,6 +1929,52 @@ describe('events', () => { testNotSafe: true }); }); + + it('should emit for reserved @reverse value', async () => { + const input = +{ + "@context": { + "children": { + "@reverse": "@RESERVED" + } + }, + "@id": "ex:parent", + "children": [ + { + "@id": "ex:child" + } + ] +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 4, + relativeIri: { + children: 2 + }, + unmappedProperty: { + children: 1 + }, + unmappedValue: { + 'ex:parent': 1 + } + }, + eventCounts: { + codes: { + 'invalid property': 1, + 'object with only @id': 1, + 'reserved @reverse value': 1 + }, + events: 3 + }, + testNotSafe: true + }); + }); }); describe('properties', () => { @@ -1562,6 +2089,85 @@ describe('events', () => { }); }); + it('should be called only on top unmapped term', async () => { + // value of undefined property is dropped and not checked + const input = +{ + "testUndefined": { + "subUndefined": "undefined" + } +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 4, + relativeIri: { + testUndefined: 2 + }, + unmappedProperty: { + testUndefined: 1 + }, + unmappedValue: { + '__unknown__': 1 + } + }, + eventCounts: { + codes: { + 'empty object': 1, + 'invalid property': 1 + }, + events: 2 + }, + testNotSafe: true + }); + }); + + it('should be called on sub unmapped term', async () => { + const input = +{ + "ex:defined": { + "testundefined": "undefined" + } +} +; + const expected = +[ + { + "ex:defined": [ + {} + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + mapCounts: { + expansionMap: 3, + relativeIri: { + testundefined: 2 + }, + unmappedProperty: { + testundefined: 1 + } + }, + eventCounts: { + codes: { + 'invalid property': 1 + }, + events: 1 + }, + testNotSafe: true + }); + }); + it('should be called on unmapped term with context [1]', async () => { const input = { @@ -2584,6 +3190,47 @@ describe('events', () => { }); }); + it('should handle scoped relative `@vocab`', async () => { + const input = +{ + "@context": { + "@vocab": "urn:abs/" + }, + "@type": "ta", + "e:a": { + "@context": { + "@vocab": "rel/" + }, + "@type": "tb" + } +} +; + const expected = +[ + { + "@type": [ + "urn:abs/ta" + ], + "e:a": [ + { + "@type": [ + "urn:abs/rel/tb" + ] + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCounts: {}, + testSafe: true + }); + }); + it('should be called when `@id` is being ' + 'expanded with `@base`', async () => { const input = From 227fccb1c6f83d72995fb934634ff5b2dca0cbac Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 12 Apr 2022 01:06:49 -0400 Subject: [PATCH 086/181] Add start of normative test flag support. - Checking for flag. - Unsure how to use, but some code exists for future use. --- tests/test-common.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test-common.js b/tests/test-common.js index 8d86fdbe..7f94df17 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -520,6 +520,8 @@ function addTest(manifest, test, tests) { } const testOptions = getJsonLdValues(test, 'option'); + // allow special handling in case of normative test failures + let normativeTest = true; testOptions.forEach(function(opt) { const processingModes = getJsonLdValues(opt, 'processingMode'); @@ -555,6 +557,13 @@ function addTest(manifest, test, tests) { }); }); + testOptions.forEach(function(opt) { + const normative = getJsonLdValues(opt, 'normative'); + normative.forEach(function(n) { + normativeTest = normativeTest && n; + }); + }); + const fn = testInfo.fn; const params = testInfo.params.map(param => param(test)); // resolve test data @@ -608,6 +617,16 @@ function addTest(manifest, test, tests) { }); } } catch(err) { + // FIXME: improve handling of non-normative errors + // FIXME: for now, explicitly disabling tests. + //if(!normativeTest) { + // // failure ok + // if(options.verboseSkip) { + // console.log('Skipping non-normative test due to failure:', + // {id: test['@id'], name: test.name}); + // } + // self.skip(); + //} if(options.bailOnError) { if(err.name !== 'AssertionError') { console.error('\nError: ', JSON.stringify(err, null, 2)); From 91fe9d1c1a0126b406c55e11ef12c6154e4b43c1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 20:28:59 -0400 Subject: [PATCH 087/181] Keep former non-normative edge case behavior. - Leave comments about the `{"@id": null}` edge case tests but still test with current behavior. Will address the issue later. --- CHANGELOG.md | 14 -------------- tests/test-common.js | 12 ++++++++++++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75a7fda1..c050f8a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,20 +3,6 @@ ### Changed - Change EARL Assertor to Digital Bazaar, Inc. - Update eslint dependencies. -- **BREAKING**: Handle spec edge case where value for `@id` can expand into - null. Behavior is changing to *not* output invalid JSON-LD and *potentially* - outputing an extra blank node. The input in question is something like - `{"@id":"@RESERVED", ...}` where `@RESERVED` will expand into `null`. Rather - than outputing invalid `{"@id": null, ...}`, the new behavior will drop - `@id`. When going to RDF this can cause slightly different output. - Specifically, a `{}` value may exist and create a blank node. Please file an - issue if this behavior causes issues. It is expected to be better addressed - in a future spec. - - Related [issue](https://github.com/w3c/json-ld-api/issues/480). - - Normative [toRdf test case](https://w3c.github.io/json-ld-api/tests/toRdf-manifest.html#te122) - *that now fails*. - - Non-normative [expand test case](https://w3c.github.io/json-ld-api/tests/expand-manifest.html#t0122) - that now fails. ### Added - Support benchmarks in Karma tests. diff --git a/tests/test-common.js b/tests/test-common.js index 7f94df17..093b2355 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -62,6 +62,12 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ + // spec issues + // Unclear how to handle {"@id": null} edge case + // See https://github.com/w3c/json-ld-api/issues/480 + // non-normative test, also see toRdf-manifest#te122 + ///expand-manifest#t0122$/, + // misc /expand-manifest#tc037$/, /expand-manifest#tc038$/, @@ -187,6 +193,12 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ + // spec issues + // Unclear how to handle {"@id": null} edge case + // See https://github.com/w3c/json-ld-api/issues/480 + // normative test, also see expand-manifest#t0122 + ///toRdf-manifest#te122$/, + // misc /toRdf-manifest#tc037$/, /toRdf-manifest#tc038$/, From 07d41d431ebd895c0a08be87c6515b97d236685d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:07:41 -0400 Subject: [PATCH 088/181] Fix null handling. - Updated handling of edge case null values due to previous changes. --- lib/expand.js | 3 +-- tests/misc.js | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index b3236182..ff407368 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -671,7 +671,7 @@ async function _expandObject({ return ve; } return v; - }).filter(v => v !== null), + }), {propertyIsArray: options.isFrame}); continue; } @@ -1155,7 +1155,6 @@ function _expandValue({activeCtx, activeProperty, value, options}) { options }); } - return {}; } return {'@id': expandedValue}; } diff --git a/tests/misc.js b/tests/misc.js index 63716cde..63ddb425 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1203,6 +1203,7 @@ describe('events', () => { const expected = [ { + "@id": null, "ex:p": [ { "@value": "v" @@ -1240,7 +1241,11 @@ describe('events', () => { const expected = [ { - "ex:idp": [{}], + "ex:idp": [ + { + "@id": null + } + ], "ex:p": [ { "@value": "v" @@ -1278,6 +1283,7 @@ describe('events', () => { const expected = [ { + "@id": null, "ex:p": [ { "@value": "v" @@ -1315,7 +1321,11 @@ describe('events', () => { const expected = [ { - "ex:idp": [{}], + "ex:idp": [ + { + "@id": null + } + ], "ex:p": [ { "@value": "v" From b3475c9e39696f860f216de1c3e89aeba0fa3348 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:31:33 -0400 Subject: [PATCH 089/181] Handle non-string id blank node edge case. In the case where an `@id` is not a string, consider an object a blank node. This is needed to handle a crashing error where the code assumes ids are valid strings. The error could occur with direct invalid input, or in the edge case where non-normative invalid intermediate expanded JSON-LD is generated. --- lib/graphTypes.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/graphTypes.js b/lib/graphTypes.js index ea8ef26b..3d06d6cf 100644 --- a/lib/graphTypes.js +++ b/lib/graphTypes.js @@ -106,11 +106,12 @@ api.isSimpleGraph = v => { api.isBlankNode = v => { // Note: A value is a blank node if all of these hold true: // 1. It is an Object. - // 2. If it has an @id key its value begins with '_:'. + // 2. If it has an @id key that is not a string OR begins with '_:'. // 3. It has no keys OR is not a @value, @set, or @list. if(types.isObject(v)) { if('@id' in v) { - return (v['@id'].indexOf('_:') === 0); + const id = v['@id']; + return !types.isString(id) || id.indexOf('_:') === 0; } return (Object.keys(v).length === 0 || !(('@value' in v) || ('@set' in v) || ('@list' in v))); From 7011161f591355f9841b5029429229b2f19958e3 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:45:05 -0400 Subject: [PATCH 090/181] Add safe mode default to docs. --- lib/jsonld.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/jsonld.js b/lib/jsonld.js index 38c39abb..ebb51d01 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -129,7 +129,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. - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -269,7 +269,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. - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -424,7 +424,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. - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -524,7 +524,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. - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -561,7 +561,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 - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -619,7 +619,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). - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * * @return a Promise that resolves to the JSON-LD document. @@ -670,7 +670,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. - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -765,7 +765,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. - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * @@ -929,7 +929,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. - * [safe] true to use safe mode. + * [safe] true to use safe mode. (default: false) * [eventHandler] handler for events. * [contextResolver] internal use only. * From 7ad86dc14d58d16e989cc5b987ceed82ca0480eb Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:53:01 -0400 Subject: [PATCH 091/181] Fix tests. - Better `expected` checking. - Fix test flag typos. --- tests/misc.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index 63ddb425..5e1d8fe6 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -634,7 +634,7 @@ describe('events', () => { if(!exception && error) { throw error; } - if(expected) { + if(expected !== undefined) { assert.deepStrictEqual(result, expected); } if(mapCounts) { @@ -1141,7 +1141,7 @@ describe('events', () => { 'reserved @id value', 'invalid property' ], - testUnSafe: true + testNotSafe: true }); }); @@ -1189,7 +1189,7 @@ describe('events', () => { }, events: 2 }, - testUnSafe: true + testNotSafe: true }); }); @@ -1224,7 +1224,7 @@ describe('events', () => { }, events: 1 }, - testUnSafe: true + testNotSafe: true }); }); @@ -1266,7 +1266,7 @@ describe('events', () => { }, events: 1 }, - testUnSafe: true + testNotSafe: true }); }); @@ -1304,7 +1304,7 @@ describe('events', () => { }, events: 1 }, - testUnSafe: true + testNotSafe: true }); }); @@ -1346,7 +1346,7 @@ describe('events', () => { }, events: 1 }, - testUnSafe: true + testNotSafe: true }); }); @@ -1389,7 +1389,7 @@ describe('events', () => { }, events: 2 }, - testUnSafe: true + testNotSafe: true }); }); @@ -1428,7 +1428,7 @@ describe('events', () => { }, events: 1 }, - testUnSafe: true + testNotSafe: true }); }); }); From 59fc87b7721ea5553bd84d135a72271894bde636 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 5 Aug 2022 00:06:21 -0400 Subject: [PATCH 092/181] Add toRDF events and tests. - Add various toRDF relative reference events. - Update test support. - Add tests. --- lib/events.js | 8 +- lib/toRdf.js | 98 ++++++++++++++-- tests/misc.js | 314 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 409 insertions(+), 11 deletions(-) diff --git a/lib/events.js b/lib/events.js index 237f4e0e..939885a1 100644 --- a/lib/events.js +++ b/lib/events.js @@ -117,7 +117,13 @@ const _notSafeEventCodes = new Set([ 'relative @vocab reference', 'reserved @id value', 'reserved @reverse value', - 'reserved term' + 'reserved term', + // toRDF + 'blank node predicate', + 'relative graph reference', + 'relative property reference', + 'relative subject reference', + 'relative type reference' ]); // safe handler that rejects unsafe warning conditions diff --git a/lib/toRdf.js b/lib/toRdf.js index d19980c1..eac6bad3 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -10,6 +10,10 @@ const jsonCanonicalize = require('canonicalize'); const types = require('./types'); const util = require('./util'); +const { + handleEvent: _handleEvent +} = require('./events'); + const { // RDF, // RDF_LIST, @@ -66,6 +70,20 @@ api.toRDF = (input, options) => { graphTerm.value = graphName; } else { // skip relative IRIs (not valid RDF) + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative graph reference', + level: 'warning', + message: 'Relative graph reference found.', + details: { + graph: graphName + } + }, + options + }); + } continue; } _graphToRDF(dataset, nodeMap[graphName], graphTerm, issuer, options); @@ -107,6 +125,20 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { // skip relative IRI subjects (not valid RDF) if(!_isAbsoluteIri(id)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative subject reference', + level: 'warning', + message: 'Relative subject reference found.', + details: { + subject: id + } + }, + options + }); + } continue; } @@ -118,18 +150,48 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { // skip relative IRI predicates (not valid RDF) if(!_isAbsoluteIri(property)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative property reference', + level: 'warning', + message: 'Relative property reference found.', + details: { + property + } + }, + options + }); + } continue; } // skip blank node predicates unless producing generalized RDF if(predicate.termType === 'BlankNode' && !options.produceGeneralizedRdf) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'blank node predicate', + level: 'warning', + message: 'Dropping blank node predicate.', + details: { + // FIXME: add better issuer API to get reverse mapping + property: issuer.getOldIds() + .find(key => issuer.getId(key) === property) + } + }, + options + }); + } continue; } // convert list, value or node object to triple - const object = - _objectToRDF(item, issuer, dataset, graphTerm, options.rdfDirection); + const object = _objectToRDF( + item, issuer, dataset, graphTerm, options.rdfDirection, options); // skip null objects (they are relative IRIs) if(object) { dataset.push({ @@ -152,10 +214,11 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { * @param issuer a IdentifierIssuer for assigning blank node names. * @param dataset the array of quads to append to. * @param graphTerm the graph term for each quad. + * @param options the RDF serialization options. * * @return the head of the list. */ -function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection) { +function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection, options) { const first = {termType: 'NamedNode', value: RDF_FIRST}; const rest = {termType: 'NamedNode', value: RDF_REST}; const nil = {termType: 'NamedNode', value: RDF_NIL}; @@ -166,7 +229,8 @@ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection) { let subject = result; for(const item of list) { - const object = _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection); + const object = _objectToRDF( + item, issuer, dataset, graphTerm, rdfDirection, options); const next = {termType: 'BlankNode', value: issuer.getId()}; dataset.push({ subject, @@ -185,7 +249,8 @@ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection) { // Tail of list if(last) { - const object = _objectToRDF(last, issuer, dataset, graphTerm, rdfDirection); + const object = _objectToRDF( + last, issuer, dataset, graphTerm, rdfDirection, options); dataset.push({ subject, predicate: first, @@ -211,10 +276,13 @@ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection) { * @param issuer a IdentifierIssuer for assigning blank node names. * @param dataset the dataset to append RDF quads to. * @param graphTerm the graph term for each quad. + * @param options the RDF serialization options. * * @return the RDF literal or RDF resource. */ -function _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection) { +function _objectToRDF( + item, issuer, dataset, graphTerm, rdfDirection, options +) { const object = {}; // convert value object to RDF @@ -260,8 +328,8 @@ function _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection) { object.datatype.value = datatype || XSD_STRING; } } else if(graphTypes.isList(item)) { - const _list = - _listToRDF(item['@list'], issuer, dataset, graphTerm, rdfDirection); + const _list = _listToRDF( + item['@list'], issuer, dataset, graphTerm, rdfDirection, options); object.termType = _list.termType; object.value = _list.value; } else { @@ -273,6 +341,20 @@ function _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection) { // skip relative IRIs, not valid RDF if(object.termType === 'NamedNode' && !_isAbsoluteIri(object.value)) { + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative type reference', + level: 'warning', + message: 'Relative type reference found.', + details: { + type: object.value + } + }, + options + }); + } return null; } diff --git a/tests/misc.js b/tests/misc.js index 5e1d8fe6..02a75cdc 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -569,7 +569,7 @@ describe('events', () => { // test different apis // use appropriate options async function _test({ - // expand, compact, frame, fromRdf, toRdf, etc + // expand, compact, frame, fromRDF, toRDF, normalize, etc type, input, options, @@ -602,7 +602,7 @@ describe('events', () => { if(eventCounts || eventLog || eventCodeLog) { opts.eventHandler = eventHandler; } - if(!['expand', 'fromRDF'].includes(type)) { + if(!['expand', 'fromRDF', 'toRDF', 'canonize'].includes(type)) { throw new Error(`Unknown test type: "${type}"`); } try { @@ -612,6 +612,16 @@ describe('events', () => { if(type === 'fromRDF') { result = await jsonld.fromRDF(input, opts); } + if(type === 'toRDF') { + result = await jsonld.toRDF(input, { + // default to n-quads + format: 'application/n-quads', + ...opts + }); + } + if(type === 'canonize') { + result = await jsonld.canonize(input, opts); + } } catch(e) { error = e; } @@ -1123,6 +1133,9 @@ describe('events', () => { } ] ; + const nq = `\ + "v" . +`; await _test({ type: 'expand', @@ -1143,6 +1156,15 @@ describe('events', () => { ], testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); it('should handle reserved context @id values [2]', async () => { @@ -1168,6 +1190,9 @@ describe('events', () => { } ] ; + const nq = `\ + "v" . +`; await _test({ type: 'expand', @@ -1191,6 +1216,15 @@ describe('events', () => { }, testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); it('should handle reserved content @id values', async () => { @@ -1212,6 +1246,9 @@ describe('events', () => { } ] ; + const nq = `\ +_:b0 "v" . +`; await _test({ type: 'expand', @@ -1226,6 +1263,15 @@ describe('events', () => { }, testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); it('should handle reserved content id values [1]', async () => { @@ -1254,6 +1300,9 @@ describe('events', () => { } ] ; + const nq = `\ +_:b0 "v" . +`; await _test({ type: 'expand', @@ -1268,6 +1317,15 @@ describe('events', () => { }, testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); it('should handle reserved content id values [2]', async () => { @@ -1292,6 +1350,9 @@ describe('events', () => { } ] ; + const nq = `\ +_:b0 "v" . +`; await _test({ type: 'expand', @@ -1306,6 +1367,15 @@ describe('events', () => { }, testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); it('should handle reserved content id values [3]', async () => { @@ -1334,6 +1404,9 @@ describe('events', () => { } ] ; + const nq = `\ +_:b0 "v" . +`; await _test({ type: 'expand', @@ -1348,6 +1421,15 @@ describe('events', () => { }, testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); it('should handle reserved context terms', async () => { @@ -1371,6 +1453,9 @@ describe('events', () => { } ] ; + const nq = `\ +_:b0 "v" . +`; await _test({ type: 'expand', @@ -1391,6 +1476,15 @@ describe('events', () => { }, testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); it('should handle reserved content terms', async () => { @@ -1411,6 +1505,9 @@ describe('events', () => { } ] ; + const nq = `\ +_:b0 "v" . +`; await _test({ type: 'expand', @@ -1430,6 +1527,15 @@ describe('events', () => { }, testNotSafe: true }); + + await _test({ + type: 'toRDF', + input: expected, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); }); }); @@ -3499,4 +3605,208 @@ describe('events', () => { }); }); }); + + describe('toRDF', () => { + it('should handle relative graph reference', async () => { + const input = +[ + { + "@id": "rel", + "@graph": [ + { + "@id": "s:1", + "ex:p": [ + { + "@value": "v1" + } + ] + } + ] + } +] +; + const nq = `\ +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [ + 'relative graph reference' + ], + testNotSafe: true + }); + }); + + it('should handle relative subject reference', async () => { + const input = +[ + { + "@id": "rel", + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + const nq = `\ +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [ + 'relative subject reference' + ], + testNotSafe: true + }); + }); + + it('should handle relative property reference', async () => { + const input = +[ + { + "rel": [ + { + "@value": "v" + } + ] + } +] +; + const nq = `\ +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [ + 'relative property reference' + ], + testNotSafe: true + }); + }); + + it('should handle relative property reference', async () => { + const input = +[ + { + "@type": [ + "rel" + ], + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + const nq = `\ +_:b0 "v" . +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [ + 'relative type reference' + ], + testNotSafe: true + }); + }); + + it('should handle blank node predicates', async () => { + const input = +[ + { + "_:p": [ + { + "@value": "v" + } + ] + } +] +; + const nq = `\ +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [ + 'blank node predicate' + ], + testNotSafe: true + }); + }); + + it('should handle generlized RDf blank node predicates', async () => { + const input = +[ + { + "_:p": [ + { + "@value": "v" + } + ] + } +] +; + const nq = `\ +_:b0 <_:b1> "v" . +`; + + await _test({ + type: 'toRDF', + input, + options: { + skipExpansion: true, + produceGeneralizedRdf: true + }, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + + it.skip('should handle null @id', async () => { + const input = +[ + { + "@id": null, + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + const nq = `\ +_:b0 "v" . +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + }); }); From b47cc24cfd295e12af54ff379bdb1a1c12c649cc Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 5 Aug 2022 00:31:39 -0400 Subject: [PATCH 093/181] Remove eventHandler docs. - The `eventHandler` design and implementation has not been finalized for external use yet. --- CHANGELOG.md | 3 - README.md | 154 -------------------------------------------------- lib/jsonld.js | 11 ---- 3 files changed, 168 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c050f8a0..d8d2f1c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,6 @@ - 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. ## 6.0.0 - 2022-06-06 diff --git a/README.md b/README.md index 0dafc939..ca71d8e0 100644 --- a/README.md +++ b/README.md @@ -346,160 +346,6 @@ It is recommended to set a default `user-agent` header for Node.js applications. The default for the default Node.js document loader is `jsonld.js`. -### Events - -**WARNING**: This feature is **experimental** and the API, events, codes, -levels, and messages may change. - -Various events may occur during processing. The event handler system allows -callers to handle events as appropriate. Use cases can be as simple as logging -warnings, to displaying helpful UI hints, to failing on specific conditions. - -**Note**: By default no event handler is used. This is due to general -performance considerations and the impossibility of providing a default handler -that would work for all use cases. Event construction and the handling system -are avoided by default providing the best performance for use cases where data -quality is known events are unnecessary. - -#### Event Structure - -Events are JSON objects with the following properties: - -- **`type`**: ['JsonLdEvent'] and optionally an array with others. -- **`code`**: A basic string code, similar to existing JSON-LD error codes. -- **`level`**: The severity level. Currently only `warning` is emitted. -- **`message`**: A human readable message describing the event. -- **`details`**: A JSON object with event specific details. - -#### Event Handlers - -Event handlers are chainable functions, arrays of handlers, objects mapping -codes to handlers, or any mix of these structures. Each function is passed an -object with two properties: - -- **`event`**: The event data. -- **`next`**: A function to call to an `event` and a `next`. - -The event handling system will process the handler structure, calling all -handlers, and continuing onto the next handler if `next()` is called. To stop -processing, throw an error, or return without calling `next()`. - -This design allows for composable handler structures, for instance to handle -some conditions with a custom handler, and default to generic "unknown event" -or logging handler. - -**Note**: Handlers are currently synchronous due to possible performance -issues. This may change to an `async`/`await` design in the future. - -```js -// expand a document with a logging event handler -const expanded = await jsonld.expand(data, { - // simple logging handler - eventHandler: function({event, next}) { - console.log('event', {event}); - } -}); -``` - -```js -function logEventHandler({event, next}) { - console.log('event', {event}); - next(); -} - -function noWarningsEventHandler({event, next}) { - if(event.level === 'warning') { - throw new Error('No warnings!', {event}); - } - next(); -} - -function unknownEventHandler({event, next}) { - throw new Error('Unknown event', {event}); -} - -// expand a document with an array of event handlers -const expanded = await jsonld.expand(data, { - // array of handlers - eventHandler: [ - logEventHandler, - noWarningsEventHandler, - unknownEventHandler - ]} -}); -``` - -```js -const handler = { - 'a mild event code': function({event}) { - console.log('the thing happened', {event}); - }, - 'a serious event code': function({event}) { - throw new Error('the specific thing happened', {event}); - } -}; -// expand a document with a code map event handler -const expanded = await jsonld.expand(data, {eventHandler}); -``` - -#### Safe Validation - -A common use case is to avoid JSON-LD constructs that will result in lossy -behavior. The JSON-LD specifications have notes about when data is dropped. -This can be especially important when calling [`canonize`][] in order to -digitally sign data. The event system can be used to detect and avoid these -situations. A special "safe mode" is available that will inject an initial -event handler that fails on conditions that would result in data loss. More -benign events may fall back to the passed event handler, if any. - -**Note**: This mode is designed to be the common way that digital signing and -similar applications use this library. - -The `safe` options flag set to `true` enables this behavior: - -```js -// expand a document in safe mode -const expanded = await jsonld.expand(data, {safe: true}); -``` - -```js -// expand a document in safe mode, with fallback handler -const expanded = await jsonld.expand(data, { - safe: true - eventHandler: function({event}) { /* ... */ } -}); -``` - -#### Available Handlers - -Some predefined event handlers are available to use alone or as part of a more -complex handler: - -- **safeEventHandler**: The handler used when `safe` is `true`. -- **logEventHandler**: A debugging handler that outputs to the console. -- **logWarningHandler**: A debugging handler that outputs `warning` level - events to the console. -- **unhandledEventHandler**: Throws on all events not yet handled. - -#### Default Event Handler - -A default event handler can be set. It will be the only handler when not in -safe mode, and the second handler when in safe mode. - -```js -// fail on unknown events -jsonld.setDefaultEventHandler(jsonld.unhandledEventHandler); -// will use unhandled event handler by default -const expanded = await jsonld.expand(data); -``` - -```js -// always use safe mode event handler, ignore other events -jsonld.setDefaultEventHandler(jsonld.safeEventHandler); -// will use safe mode handler, like `{safe: true}` -const expanded = await jsonld.expand(data); -``` - Related Modules --------------- diff --git a/lib/jsonld.js b/lib/jsonld.js index ebb51d01..28c672ac 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -130,7 +130,6 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); * if this function returns `undefined` then the default behavior * will be used. * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the compacted output. @@ -270,7 +269,6 @@ jsonld.compact = async function(input, ctx, options) { * if this function returns `undefined` then the default behavior * will be used. * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the expanded output. @@ -368,7 +366,6 @@ jsonld.expand = async function(input, options) { * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the flattened output. @@ -425,7 +422,6 @@ jsonld.flatten = async function(input, ctx, options) { * [omitDefault] default @omitDefault flag (default: false). * [documentLoader(url, options)] the document loader. * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the framed output. @@ -525,7 +521,6 @@ jsonld.frame = async function(input, frame, options) { * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the linked output. @@ -562,7 +557,6 @@ jsonld.link = async function(input, ctx, options) { * [documentLoader(url, options)] the document loader. * [useNative] true to use a native canonize algorithm * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the normalized output. @@ -620,7 +614,6 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { * [rdfDirection] 'i18n-datatype' to support RDF transformation of * @direction (default: null). * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * * @return a Promise that resolves to the JSON-LD document. */ @@ -671,7 +664,6 @@ jsonld.fromRDF = async function(dataset, options) { * to produce only standard RDF (default: false). * [documentLoader(url, options)] the document loader. * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the RDF dataset. @@ -725,7 +717,6 @@ 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. - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged node map. @@ -766,7 +757,6 @@ jsonld.createNodeMap = async function(input, options) { * (default: true). * [documentLoader(url, options)] the document loader. * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged output. @@ -930,7 +920,6 @@ jsonld.get = async function(url, options) { * @param [options] the options to use: * [documentLoader(url, options)] the document loader. * [safe] true to use safe mode. (default: false) - * [eventHandler] handler for events. * [contextResolver] internal use only. * * @return a Promise that resolves to the new active context. From a8fc351debb8556a0e352317919fe19e586bc272 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 5 Aug 2022 00:32:10 -0400 Subject: [PATCH 094/181] Add "safe mode" documentation. --- CHANGELOG.md | 6 ++++++ README.md | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d2f1c8..7280aaf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - Support test environment in EARL output. - Support benchmark output in EARL output. - Benchmark comparison tool. +- Add "safe mode" to all APIs. Enable by adding `{safe: true}` to API options. + This mode causes processing to fail when data constructs are encountered that + result in lossy behavior or other data warnings. This is intended to be the + common way that digital signing and similar applications use this libraray. ## 6.0.0 - 2022-06-06 @@ -34,6 +38,8 @@ ### Removed - Experimental non-standard `protectedMode` option. +- **BREAKING**: Various console warnings were removed. The newly added "safe + mode" can stop processing where these warnings were. ## 5.2.0 - 2021-04-07 diff --git a/README.md b/README.md index ca71d8e0..045325a4 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,24 @@ It is recommended to set a default `user-agent` header for Node.js applications. The default for the default Node.js document loader is `jsonld.js`. +### Safe Mode + +A common use case is to avoid JSON-LD constructs that will result in lossy +behavior. The JSON-LD specifications have notes about when data is dropped. +This can be especially important when calling [`canonize`][] in order to +digitally sign data. A special "safe mode" is available that will detect these +situations and cause processing to fail. + +**Note**: This mode is designed to be the common way that digital signing and +similar applications use this library. + +The `safe` options flag set to `true` enables this behavior: + +```js +// expand a document in safe mode +const expanded = await jsonld.expand(data, {safe: true}); +``` + Related Modules --------------- From 353c1925e72d7d79f738924218c1cb431d13097b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 6 Aug 2022 01:21:24 -0400 Subject: [PATCH 095/181] Remove `compactionMap` and `expansionMap`. - **BREAKING**: Remove `compactionMap` and `expansionMap`. Their known use cases are addressed with "safe mode" and future planned features. - Update docs. - Update tests. - Add partial event log tester to simplify checking only some fields. - Add `prepending` events for `@base` and `@vocab`. Likely to be removed since other events better expose what the real data issues are. May return for a debug mode. - Fix some tests. --- CHANGELOG.md | 2 + lib/compact.js | 38 +- lib/context.js | 106 +++-- lib/expand.js | 347 ++++++---------- lib/jsonld.js | 32 +- tests/misc.js | 1041 +++++++++++++++--------------------------------- 6 files changed, 522 insertions(+), 1044 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7280aaf2..b9ec76de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ - Experimental non-standard `protectedMode` option. - **BREAKING**: Various console warnings were removed. The newly added "safe mode" can stop processing where these warnings were. +- **BREAKING**: Remove `compactionMap` and `expansionMap`. Their known use + cases are addressed with "safe mode" and future planned features. ## 5.2.0 - 2021-04-07 diff --git a/lib/compact.js b/lib/compact.js index 1bcdb4bb..ccccc47f 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -51,7 +51,6 @@ module.exports = api; * to compact, null for none. * @param element the element to compact. * @param options the compaction options. - * @param compactionMap the compaction map to use. * * @return a promise that resolves to the compacted value. */ @@ -59,33 +58,21 @@ api.compact = async ({ activeCtx, activeProperty = null, element, - options = {}, - compactionMap = () => undefined + options = {} }) => { // recursively compact array if(_isArray(element)) { let rval = []; for(let i = 0; i < element.length; ++i) { - // compact, dropping any null values unless custom mapped - let compacted = await api.compact({ + const compacted = await api.compact({ activeCtx, activeProperty, element: element[i], - options, - compactionMap + options }); if(compacted === null) { - compacted = await compactionMap({ - unmappedValue: element[i], - activeCtx, - activeProperty, - parent: element, - index: i, - options - }); - if(compacted === undefined) { - continue; - } + // FIXME: need event? + continue; } rval.push(compacted); } @@ -149,8 +136,7 @@ api.compact = async ({ activeCtx, activeProperty, element: element['@list'], - options, - compactionMap + options }); } } @@ -278,8 +264,7 @@ api.compact = async ({ activeCtx, activeProperty: '@reverse', element: expandedValue, - options, - compactionMap + options }); // handle double-reversed properties @@ -316,8 +301,7 @@ api.compact = async ({ activeCtx, activeProperty, element: expandedValue, - options, - compactionMap + options }); if(!(_isArray(compactedValue) && compactedValue.length === 0)) { @@ -434,8 +418,7 @@ api.compact = async ({ activeCtx, activeProperty: itemActiveProperty, element: (isList || isGraph) ? inner : expandedItem, - options, - compactionMap + options }); // handle @list @@ -630,8 +613,7 @@ api.compact = async ({ activeCtx, activeProperty: itemActiveProperty, element: {'@id': expandedItem['@id']}, - options, - compactionMap + options }); } } diff --git a/lib/context.js b/lib/context.js index 13411c99..b06442c6 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1106,35 +1106,29 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { if(relativeTo.vocab && '@vocab' in activeCtx) { // prepend vocab const prependedResult = activeCtx['@vocab'] + value; - let expansionMapResult = undefined; - if(options && options.expansionMap) { - // if we are about to expand the value by prepending - // @vocab then call the expansion map to inform - // interested callers that this is occurring - - // TODO: use `await` to support async - expansionMapResult = options.expansionMap({ - prependedIri: { - type: '@vocab', - vocab: activeCtx['@vocab'], - value, - result: prependedResult, - typeExpansion, + if(options && options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'prepending @vocab during expansion', + level: 'info', + message: 'Prepending @vocab during expansion.', + details: { + type: '@vocab', + vocab: activeCtx['@vocab'], + value, + result: prependedResult, + typeExpansion + } }, - activeCtx, options }); } - if(expansionMapResult !== undefined) { - value = expansionMapResult; - } else { - // the null case preserves value as potentially relative - value = prependedResult; - } + // the null case preserves value as potentially relative + value = prependedResult; } else if(relativeTo.base) { // prepend base let prependedResult; - let expansionMapResult; let base; if('@base' in activeCtx) { if(activeCtx['@base']) { @@ -1148,50 +1142,50 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { base = options.base; prependedResult = prependBase(options.base, value); } - if(options && options.expansionMap) { - // if we are about to expand the value by pre-pending - // @base then call the expansion map to inform - // interested callers that this is occurring - - // TODO: use `await` to support async - expansionMapResult = options.expansionMap({ - prependedIri: { - type: '@base', - base, - value, - result: prependedResult, - typeExpansion, + if(options && options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'prepending @base during expansion', + level: 'info', + message: 'Prepending @base during expansion.', + details: { + type: '@base', + base, + value, + result: prependedResult, + typeExpansion + } }, - activeCtx, options }); } - if(expansionMapResult !== undefined) { - value = expansionMapResult; - } else { - // the null case preserves value as potentially relative - value = prependedResult; - } + // the null case preserves value as potentially relative + value = prependedResult; } - if(!_isAbsoluteIri(value) && options && options.expansionMap) { - // if the result of the expansion is not an absolute iri then - // call the expansion map to inform interested callers that - // the resulting value is a relative iri, which can result in - // it being dropped when converting to other RDF representations - - // TODO: use `await` to support async - const expandedResult = options.expansionMap({ - relativeIri: value, - activeCtx, - typeExpansion, + // FIXME: duplicate? needed? maybe just enable in a verbose debug mode + /* + if(!_isAbsoluteIri(value) && options && options.eventHandler) { + // emit event indicating a relative IRI was found, which can result in it + // being dropped when converting to other RDF representations + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'relative IRI after expansion', + // FIXME: what level? + level: 'warning', + message: 'Relative IRI after expansion.', + details: { + relativeIri: value, + typeExpansion + } + }, options }); - if(expandedResult !== undefined) { - value = expandedResult; - } // NOTE: relative reference events emitted at calling sites as needed } + */ return value; } diff --git a/lib/expand.js b/lib/expand.js index ff407368..d2d94a20 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -63,10 +63,6 @@ module.exports = api; * @param typeScopedContext an optional type-scoped active context for * expanding values of nodes that were expressed according to * a type-scoped context. - * @param expansionMap(info) a function that can be used to custom map - * unmappable values (or to throw an error when they are detected); - * if this function returns `undefined` then the default behavior - * will be used. * * @return a Promise that resolves to the expanded value. */ @@ -77,13 +73,8 @@ api.expand = async ({ options = {}, insideList = false, insideIndex = false, - typeScopedContext = null, - expansionMap = () => undefined + typeScopedContext = null }) => { - - // add expansion map to the processing options - options = {...options, expansionMap}; - // nothing to expand if(element === null || element === undefined) { return null; @@ -95,36 +86,28 @@ api.expand = async ({ } if(!_isArray(element) && !_isObject(element)) { - // drop free-floating scalars that are not in lists unless custom mapped + // drop free-floating scalars that are not in lists if(!insideList && (activeProperty === null || _expandIri(activeCtx, activeProperty, {vocab: true}, options) === '@graph')) { - const mapped = await expansionMap({ - unmappedValue: element, - activeCtx, - activeProperty, - options, - insideList - }); - if(mapped === undefined) { - // FIXME - if(options.eventHandler) { - _handleEvent({ - event: { - type: ['JsonLdEvent'], - code: 'free-floating scalar', - level: 'warning', - message: 'Dropping free-floating scalar not in a list.', - details: { - value: element - } - }, - options - }); - } - return null; + // FIXME + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'free-floating scalar', + level: 'warning', + message: 'Dropping free-floating scalar not in a list.', + details: { + value: element + //activeProperty + //insideList + } + }, + options + }); } - return mapped; + return null; } // expand element according to value expansion rules @@ -144,7 +127,6 @@ api.expand = async ({ activeProperty, element: element[i], options, - expansionMap, insideIndex, typeScopedContext }); @@ -153,20 +135,16 @@ api.expand = async ({ } if(e === null) { - e = await expansionMap({ - unmappedValue: element[i], - activeCtx, - activeProperty, - parent: element, - index: i, - options, - expandedParent: rval, - insideList - }); - if(e === undefined) { - // NOTE: no-value events emitted at calling sites as needed - continue; - } + // FIXME: add debug event? + //unmappedValue: element[i], + //activeProperty, + //parent: element, + //index: i, + //expandedParent: rval, + //insideList + + // NOTE: no-value events emitted at calling sites as needed + continue; } if(_isArray(e)) { @@ -279,8 +257,7 @@ api.expand = async ({ options, insideList, typeKey, - typeScopedContext, - expansionMap + typeScopedContext }); // get property count on expanded output @@ -318,39 +295,27 @@ api.expand = async ({ const values = rval['@value'] === null ? [] : _asArray(rval['@value']); const types = _getValues(rval, '@type'); - // drop null @values unless custom mapped + // drop null @values if(_processingMode(activeCtx, 1.1) && types.includes('@json') && types.length === 1) { // Any value of @value is okay if @type: @json } else if(values.length === 0) { - const mapped = await expansionMap({ - unmappedValue: rval, - activeCtx, - activeProperty, - element, - options, - insideList - }); - if(mapped !== undefined) { - rval = mapped; - } else { - // FIXME - if(options.eventHandler) { - _handleEvent({ - event: { - type: ['JsonLdEvent'], - code: 'null @value value', - level: 'warning', - message: 'Dropping null @value value.', - details: { - value: rval - } - }, - options - }); - } - rval = null; + // FIXME + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'null @value value', + level: 'warning', + message: 'Dropping null @value value.', + details: { + value: rval + } + }, + options + }); } + rval = null; } else if(!values.every(v => (_isString(v) || _isEmptyObject(v))) && '@language' in rval) { // if @language is present, @value must be a string @@ -385,26 +350,56 @@ api.expand = async ({ count = keys.length; } } else if(count === 1 && '@language' in rval) { - // drop objects with only @language unless custom mapped - const mapped = await expansionMap(rval, { - unmappedValue: rval, - activeCtx, - activeProperty, - element, - options, - insideList - }); - if(mapped !== undefined) { - rval = mapped; - } else { + // drop objects with only @language + // FIXME + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'object with only @language', + level: 'warning', + message: 'Dropping object with only @language.', + details: { + value: rval + } + }, + options + }); + } + rval = null; + } + + // drop certain top-level objects that do not occur in lists + if(_isObject(rval) && + !options.keepFreeFloatingNodes && !insideList && + (activeProperty === null || expandedActiveProperty === '@graph')) { + // drop empty object, top-level @value/@list, or object with only @id + if(count === 0 || '@value' in rval || '@list' in rval || + (count === 1 && '@id' in rval)) { // FIXME if(options.eventHandler) { + // FIXME: one event or diff event for empty, @v/@l, {@id}? + let code; + let message; + if(count === 0) { + code = 'empty object'; + message = 'Dropping empty object.'; + } else if('@value' in rval) { + code = 'object with only @value'; + message = 'Dropping object with only @value.'; + } else if('@list' in rval) { + code = 'object with only @list'; + message = 'Dropping object with only @list.'; + } else if(count === 1 && '@id' in rval) { + code = 'object with only @id'; + message = 'Dropping object with only @id.'; + } _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'object with only @language', + code, level: 'warning', - message: 'Dropping object with only @language.', + message, details: { value: rval } @@ -416,61 +411,6 @@ api.expand = async ({ } } - // drop certain top-level objects that do not occur in lists, unless custom - // mapped - if(_isObject(rval) && - !options.keepFreeFloatingNodes && !insideList && - (activeProperty === null || expandedActiveProperty === '@graph')) { - // drop empty object, top-level @value/@list, or object with only @id - if(count === 0 || '@value' in rval || '@list' in rval || - (count === 1 && '@id' in rval)) { - const mapped = await expansionMap({ - unmappedValue: rval, - activeCtx, - activeProperty, - element, - options, - insideList - }); - if(mapped !== undefined) { - rval = mapped; - } else { - // FIXME - if(options.eventHandler) { - // FIXME: one event or diff event for empty, @v/@l, {@id}? - let code; - let message; - if(count === 0) { - code = 'empty object'; - message = 'Dropping empty object.'; - } else if('@value' in rval) { - code = 'object with only @value'; - message = 'Dropping object with only @value.'; - } else if('@list' in rval) { - code = 'object with only @list'; - message = 'Dropping object with only @list.'; - } else if(count === 1 && '@id' in rval) { - code = 'object with only @id'; - message = 'Dropping object with only @id.'; - } - _handleEvent({ - event: { - type: ['JsonLdEvent'], - code, - level: 'warning', - message, - details: { - value: rval - } - }, - options - }); - } - rval = null; - } - } - } - return rval; }; @@ -486,10 +426,6 @@ api.expand = async ({ * @param insideList true if the element is a list, false if not. * @param typeKey first key found expanding to @type. * @param typeScopedContext the context before reverting. - * @param expansionMap(info) a function that can be used to custom map - * unmappable values (or to throw an error when they are detected); - * if this function returns `undefined` then the default behavior - * will be used. */ async function _expandObject({ activeCtx, @@ -500,16 +436,12 @@ async function _expandObject({ options = {}, insideList, typeKey, - typeScopedContext, - expansionMap + typeScopedContext }) { const keys = Object.keys(element).sort(); const nests = []; let unexpandedValue; - // add expansion map to the processing options - options = {...options, expansionMap}; - // Figure out if this is the type for a JSON literal const isJsonType = element[typeKey] && _expandIri(activeCtx, @@ -529,42 +461,28 @@ async function _expandObject({ } // expand property - let expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options); + const expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options); - // drop non-absolute IRI keys that aren't keywords unless custom mapped + // drop non-absolute IRI keys that aren't keywords if(expandedProperty === null || !(_isAbsoluteIri(expandedProperty) || _isKeyword(expandedProperty))) { - // TODO: use `await` to support async - const _expandedProperty = expandedProperty; - expandedProperty = expansionMap({ - unmappedProperty: key, - activeCtx, - activeProperty, - parent: element, - options, - insideList, - value, - expandedParent - }); - if(expandedProperty === undefined) { - if(options.eventHandler) { - _handleEvent({ - event: { - type: ['JsonLdEvent'], - code: 'invalid property', - level: 'warning', - message: 'Dropping property that did not expand into an ' + - 'absolute IRI or keyword.', - details: { - property: key, - expandedProperty: _expandedProperty - } - }, - options - }); - } - continue; + if(options.eventHandler) { + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'invalid property', + level: 'warning', + message: 'Dropping property that did not expand into an ' + + 'absolute IRI or keyword.', + details: { + property: key, + expandedProperty + } + }, + options + }); } + continue; } if(_isKeyword(expandedProperty)) { @@ -728,8 +646,7 @@ async function _expandObject({ activeCtx, activeProperty, element: value, - options, - expansionMap + options })); // Expanded values must be node objects @@ -861,8 +778,7 @@ async function _expandObject({ activeProperty: '@reverse', element: value, - options, - expansionMap + options }); // properties double-reversed if('@reverse' in expandedValue) { @@ -937,7 +853,6 @@ async function _expandObject({ options, activeProperty: key, value, - expansionMap, asGraph, indexKey, propertyIndex @@ -950,7 +865,6 @@ async function _expandObject({ options, activeProperty: key, value, - expansionMap, asGraph, indexKey: '@id' }); @@ -962,7 +876,6 @@ async function _expandObject({ options, activeProperty: key, value, - expansionMap, asGraph: false, indexKey: '@type' }); @@ -979,8 +892,7 @@ async function _expandObject({ activeProperty: nextActiveProperty, element: value, options, - insideList: isList, - expansionMap + insideList: isList }); } else if( _getContextValue(activeCtx, key, '@type') === '@json') { @@ -995,29 +907,18 @@ async function _expandObject({ activeProperty: key, element: value, options, - insideList: false, - expansionMap + insideList: false }); } } // drop null values if property is not @value if(expandedValue === null && expandedProperty !== '@value') { - // TODO: use `await` to support async - expandedValue = expansionMap({ - unmappedValue: value, - expandedProperty, - activeCtx: termCtx, - activeProperty, - parent: element, - options, - insideList, - key, - expandedParent - }); - if(expandedValue === undefined) { - continue; - } + // FIXME: event? + //unmappedValue: value, + //expandedProperty, + //key, + continue; } // convert expanded value to @list if container specifies it @@ -1099,8 +1000,7 @@ async function _expandObject({ options, insideList, typeScopedContext, - typeKey, - expansionMap + typeKey }); } } @@ -1254,9 +1154,9 @@ function _expandLanguageMap(activeCtx, languageMap, direction, options) { return rval; } -async function _expandIndexMap( - {activeCtx, options, activeProperty, value, expansionMap, asGraph, - indexKey, propertyIndex}) { +async function _expandIndexMap({ + activeCtx, options, activeProperty, value, asGraph, indexKey, propertyIndex +}) { const rval = []; const keys = Object.keys(value).sort(); const isTypeIndex = indexKey === '@type'; @@ -1285,8 +1185,7 @@ async function _expandIndexMap( element: val, options, insideList: false, - insideIndex: true, - expansionMap + insideIndex: true }); // expand for @type, but also for @none diff --git a/lib/jsonld.js b/lib/jsonld.js index 28c672ac..8073de26 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -120,15 +120,7 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. * [documentLoader(url, options)] the document loader. - * [expansionMap(info)] a function that can be used to custom map - * unmappable values (or to throw an error when they are detected); - * if this function returns `undefined` then the default behavior - * will be used. * [framing] true if compaction is occuring during a framing operation. - * [compactionMap(info)] a function that can be used to custom map - * unmappable values (or to throw an error when they are detected); - * if this function returns `undefined` then the default behavior - * will be used. * [safe] true to use safe mode. (default: false) * [contextResolver] internal use only. * @@ -187,8 +179,7 @@ jsonld.compact = async function(input, ctx, options) { let compacted = await _compact({ activeCtx, element: expanded, - options, - compactionMap: options.compactionMap + options }); // perform clean up @@ -264,10 +255,6 @@ jsonld.compact = async function(input, ctx, options) { * [keepFreeFloatingNodes] true to keep free-floating nodes, * false not to, defaults to false. * [documentLoader(url, options)] the document loader. - * [expansionMap(info)] a function that can be used to custom map - * unmappable values (or to throw an error when they are detected); - * if this function returns `undefined` then the default behavior - * will be used. * [safe] true to use safe mode. (default: false) * [contextResolver] internal use only. * @@ -284,9 +271,6 @@ jsonld.expand = async function(input, options) { contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); - if(options.expansionMap === false) { - options.expansionMap = undefined; - } // build set of objects that may have @contexts to resolve const toResolve = {}; @@ -337,8 +321,7 @@ jsonld.expand = async function(input, options) { let expanded = await _expand({ activeCtx, element: toResolve.input, - options, - expansionMap: options.expansionMap + options }); // optimize away @graph with no other properties @@ -1034,6 +1017,17 @@ function _setDefaults(options, { documentLoader = jsonld.documentLoader, ...defaults }) { + // fail if obsolete options present + if(options && 'compactionMap' in options) { + throw new JsonLdError( + '"compactionMap" not supported.', + 'jsonld.OptionsError'); + } + if(options && 'expansionMap' in options) { + throw new JsonLdError( + '"expansionMap" not supported.', + 'jsonld.OptionsError'); + } return Object.assign( {}, {documentLoader}, diff --git a/tests/misc.js b/tests/misc.js index 02a75cdc..3734bd28 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -479,7 +479,7 @@ describe('literal JSON', () => { }); }); -// test both events and expansionMaps +// test events describe('events', () => { // track all the event counts // use simple count object (don't use tricky test keys!) @@ -494,6 +494,11 @@ describe('events', () => { counts.codes[event.code]++; } + // create event structure + function makeEvents() { + return {counts: {}, log: []}; + } + // track event and counts // use simple count object (don't use tricky test keys!) function trackEvent({events, event}) { @@ -509,63 +514,35 @@ describe('events', () => { }); } - // track all the map counts - // use simple count object (don't use tricky test keys!) - function addMapCounts(counts, info) { - // overall call count - counts.expansionMap = counts.expansionMap || 0; - counts.expansionMap++; - - if(info.unmappedProperty) { - const c = counts.unmappedProperty = counts.unmappedProperty || {}; - const k = info.unmappedProperty; - c[k] = c[k] || 0; - c[k]++; - } + function isObject(v) { + return Object.prototype.toString.call(v) === '[object Object]'; + } - if(info.unmappedValue) { - const c = counts.unmappedValue = counts.unmappedValue || {}; - const v = info.unmappedValue; - let k; - if(Object.keys(v).length === 1 && '@id' in v) { - k = v['@id']; - } else { - k = '__unknown__'; + // compare partial event array structures + // for each source, only check fields present in target + // allows easier checking of just a few key fields + function comparePartialEvents(source, target, path = []) { + if(Array.isArray(source)) { + assert(Array.isArray(target), + `target not an array, path: ${JSON.stringify(path)}`); + assert.equal(source.length, target.length, + `event arrays size mismatch: ${JSON.stringify(path)}`); + for(let i = 0; i < source.length; ++i) { + comparePartialEvents(source[i], target[i], [...path, i]); } - c[k] = c[k] || 0; - c[k]++; - } - - if(info.relativeIri) { - const c = counts.relativeIri = counts.relativeIri || {}; - const k = info.relativeIri; - c[k] = c[k] || 0; - c[k]++; - } - - if(info.prependedIri) { - const c = counts.prependedIri = counts.prependedIri || {}; - const k = info.prependedIri.value; - c[k] = c[k] || 0; - c[k]++; + } else if(isObject(target)) { + // check all target keys recursively + for(const key of Object.keys(target)) { + assert(key in source, + `missing expected key: "${key}", path: ${JSON.stringify(path)}`); + comparePartialEvents(source[key], target[key], [...path, key]); + } + } else { + assert.deepStrictEqual(source, target, + `not equal, path: ${JSON.stringify(path)}`); } } - // track map and counts - // use simple count object (don't use tricky test keys!) - function trackMap({maps, info}) { - maps.counts = maps.counts || {}; - maps.log = maps.log || []; - - addMapCounts(maps.counts, info); - // just log useful comparison details - // FIXME - maps.log.push(info); - //maps.log.push({ - // xxx: info.xxx - //}); - } - // test different apis // use appropriate options async function _test({ @@ -575,20 +552,18 @@ describe('events', () => { options, expected, exception, - mapCounts, - mapLog, eventCounts, + // event array eventLog, + // parial event array + eventPartialLog, + // event code array eventCodeLog, testSafe, testNotSafe, verbose }) { - const maps = {counts: {}, log: []}; - const expansionMap = info => { - trackMap({maps, info}); - }; - const events = {counts: {}, log: []}; + const events = makeEvents(); const eventHandler = ({event}) => { trackEvent({events, event}); }; @@ -596,10 +571,7 @@ describe('events', () => { let result; let error; const opts = {...options}; - if(mapCounts || mapLog) { - opts.expansionMap = expansionMap; - } - if(eventCounts || eventLog || eventCodeLog) { + if(eventCounts || eventLog || eventPartialLog || eventCodeLog) { opts.eventHandler = eventHandler; } if(!['expand', 'fromRDF', 'toRDF', 'canonize'].includes(type)) { @@ -633,7 +605,6 @@ describe('events', () => { options, expected, result, - maps, events }, null, 2)); } @@ -647,21 +618,21 @@ describe('events', () => { if(expected !== undefined) { assert.deepStrictEqual(result, expected); } - if(mapCounts) { - assert.deepStrictEqual(maps.counts, mapCounts); - } - if(mapLog) { - assert.deepStrictEqual(maps.log, mapLog); - } if(eventCounts) { assert.deepStrictEqual(events.counts, eventCounts); } if(eventLog) { assert.deepStrictEqual(events.log, eventLog); } + if(eventPartialLog) { + comparePartialEvents(events.log, eventPartialLog); + } if(eventCodeLog) { assert.deepStrictEqual(events.log.map(e => e.code), eventCodeLog); } + if(eventLog) { + assert.deepStrictEqual(events.log, eventLog); + } // test passes with safe=true if(testSafe) { await _test({type, input, options: {...options, safe: true}}); @@ -688,9 +659,9 @@ describe('events', () => { ; const ex = []; - const counts = {}; + const events = makeEvents(); const eventHandler = ({event}) => { - addEventCounts(counts, event); + trackEvent({events, event}); }; jsonld.setDefaultEventHandler({eventHandler}); @@ -698,13 +669,25 @@ describe('events', () => { const e = await jsonld.expand(d); assert.deepStrictEqual(e, ex); - assert.deepStrictEqual(counts, { + assert.deepStrictEqual(events.counts, { codes: { 'empty object': 1, 'invalid property': 1 }, events: 2 }); + comparePartialEvents(events.log, [ + { + code: 'invalid property', + details: { + property: 'relative', + expandedProperty: 'relative' + } + }, + { + code: 'empty object' + } + ]); // reset default jsonld.setDefaultEventHandler(); @@ -1141,18 +1124,20 @@ describe('events', () => { type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - relativeIri: { - resId: 2 + eventPartialLog: [ + { + code: 'reserved @id value', + details: { + id: '@RESERVED' + } }, - unmappedProperty: { - resId: 1 + { + code: 'invalid property', + details: { + property: 'resId', + expandedProperty: 'resId' + } } - }, - eventCodeLog: [ - 'reserved @id value', - 'invalid property' ], testNotSafe: true }); @@ -1198,22 +1183,11 @@ describe('events', () => { type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - relativeIri: { - resId: 2 - }, - unmappedProperty: { - resId: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'reserved @id value': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'reserved @id value', + 'invalid property' + // .. resId + ], testNotSafe: true }); @@ -1254,13 +1228,9 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, - eventCounts: { - codes: { - 'reserved @id value': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'reserved @id value' + ], testNotSafe: true }); @@ -1308,13 +1278,9 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, - eventCounts: { - codes: { - 'reserved @id value': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'reserved @id value' + ], testNotSafe: true }); @@ -1358,10 +1324,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, eventCounts: { codes: { 'reserved @id value': 1 + // .. '@RESERVED' }, events: 1 }, @@ -1412,10 +1378,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, eventCounts: { codes: { 'reserved @id value': 1 + // .. '@RESERVED' }, events: 1 }, @@ -1461,19 +1427,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 1, - unmappedProperty: { - '@RESERVED': 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'reserved term': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'reserved term', + // .. @RESERVED + 'invalid property' + // .. @RESERVED + ], testNotSafe: true }); @@ -1513,15 +1472,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 1, - unmappedProperty: { - '@RESERVED': 1 - } - }, eventCounts: { codes: { 'invalid property': 1, + // .. '@RESERVED' }, events: 1 }, @@ -1548,7 +1502,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, eventCounts: {}, testSafe: true }); @@ -1562,12 +1515,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 1, - unmappedValue: { - '__unknown__': 1 - } - }, eventCounts: { codes: { 'empty object': 1 @@ -1592,12 +1539,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 1, - unmappedValue: { - '__unknown__': 1 - } - }, eventCounts: { codes: { 'empty object': 1 @@ -1636,7 +1577,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, eventCounts: {}, testSafe: true }); @@ -1674,16 +1614,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 4, - unmappedValue: { - '__unknown__': 2, - 'http://example.com/free-floating-node': 2 - } - }, eventCounts: { codes: { 'free-floating scalar': 1, + // .. 'http://example.com/free-floating-node' 'object with only @id': 1 }, events: 2 @@ -1709,12 +1643,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 1, - unmappedValue: { - '__unknown__': 1, - } - }, eventCounts: { codes: { 'object with only @list': 1 @@ -1747,16 +1675,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 5, - unmappedValue: { - '__unknown__': 3, - 'http://example.com/free-floating-node': 2 - } - }, eventCounts: { codes: { 'free-floating scalar': 1, + // .. 'http://example.com/free-floating-node' 'object with only @id': 1, 'object with only @list': 1 }, @@ -1794,7 +1716,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, eventCounts: {}, testSafe: true }); @@ -1838,16 +1759,11 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 8, - unmappedValue: { - '__unknown__': 8 - } - }, eventCounts: { codes: { 'empty object': 1, 'free-floating scalar': 1, + // .. 'free-floating strings in set objects are removed' 'object with only @list': 1, 'object with only @value': 1 }, @@ -1872,12 +1788,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - unmappedValue: { - '__unknown__': 3 - } - }, eventCounts: { codes: { 'empty object': 1, @@ -1903,12 +1813,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - unmappedValue: { - '__unknown__': 2 - } - }, eventCounts: { codes: { 'empty object': 1, @@ -1946,7 +1850,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, eventCounts: { codes: { 'invalid @language value': 1 @@ -1983,13 +1886,9 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, - eventCounts: { - codes: { - 'invalid @language value': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'invalid @language value' + ], testNotSafe: true }); }); @@ -2029,19 +1928,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - relativeIri: { - de: 1, - en_bad: 1 - } - }, - eventCounts: { - codes: { - 'invalid @language value': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'invalid @language value' + // .. en_bad + ], testNotSafe: true }); }); @@ -2068,33 +1958,20 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 4, - relativeIri: { - children: 2 - }, - unmappedProperty: { - children: 1 - }, - unmappedValue: { - 'ex:parent': 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'object with only @id': 1, - 'reserved @reverse value': 1 - }, - events: 3 - }, + eventCodeLog: [ + 'reserved @reverse value', + // .. '@RESERVED' + 'invalid property', + // .. children + 'object with only @id' + ], testNotSafe: true }); }); }); describe('properties', () => { - it('should have zero counts with absolute term', async () => { + it('should have zero events with absolute term', async () => { const input = { "urn:definedTerm": "is defined" @@ -2116,13 +1993,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, - eventCounts: {}, + eventCodeLog: [], testSafe: true }); }); - it('should have zero counts with mapped term', async () => { + it('should have zero events with mapped term', async () => { const input = { "@context": { @@ -2147,8 +2023,7 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: {}, - eventCounts: {}, + eventCodeLog: [], testSafe: true }); }); @@ -2165,25 +2040,6 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 4, - relativeIri: { - testUndefined: 2 - }, - unmappedProperty: { - testUndefined: 1 - }, - unmappedValue: { - '__unknown__': 1 - } - }, - eventCounts: { - codes: { - 'empty object': 1, - 'invalid property': 1 - }, - events: 2 - }, eventLog: [ { code: 'invalid property', @@ -2220,25 +2076,11 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 4, - relativeIri: { - testUndefined: 2 - }, - unmappedProperty: { - testUndefined: 1 - }, - unmappedValue: { - '__unknown__': 1 - } - }, - eventCounts: { - codes: { - 'empty object': 1, - 'invalid property': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'invalid property', + // .. 'testUndefined' + 'empty object' + ], testNotSafe: true }); }); @@ -2265,21 +2107,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - relativeIri: { - testundefined: 2 - }, - unmappedProperty: { - testundefined: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'invalid property' + // .. 'testUndefined' + ], testNotSafe: true }); }); @@ -2299,25 +2130,11 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 4, - relativeIri: { - testUndefined: 2 - }, - unmappedProperty: { - testUndefined: 1 - }, - unmappedValue: { - '__unknown__': 1 - } - }, - eventCounts: { - codes: { - 'empty object': 1, - 'invalid property': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'invalid property', + // .. 'testUndefined' + 'empty object' + ], testNotSafe: true }); }); @@ -2348,21 +2165,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - relativeIri: { - testUndefined: 2 - }, - unmappedProperty: { - testUndefined: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'invalid property' + // .. 'testUndefined' + ], testNotSafe: true }); }); @@ -2392,21 +2198,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - relativeIri: { - testUndefined: 2 - }, - unmappedProperty: { - testUndefined: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'invalid property' + // .. 'testUndefined' + ], testNotSafe: true }); }); @@ -2426,23 +2221,13 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - unmappedProperty: { - '@RESERVED': 1 - }, - unmappedValue: { - '__unknown__': 1 - } - }, - eventCounts: { - codes: { - 'empty object': 1, - 'invalid property': 1, - 'reserved term': 1 - }, - events: 3 - }, + eventCodeLog: [ + 'reserved term', + // .. '@RESERVED' + 'invalid property', + // .. '@RESERVED' + 'empty object' + ], testNotSafe: true }); }); @@ -2462,25 +2247,13 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - relativeiri: 1 - }, - unmappedValue: { - relativeiri: 1 - } - }, - eventCounts: { - codes: { - 'object with only @id': 1, - 'relative @id reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @id reference', + // .. 'relativeiri' + 'object with only @id' + ], testNotSafe: true }); }); @@ -2509,21 +2282,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - relativeiri: 1 - } - }, - eventCounts: { - codes: { - 'relative @id reference': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @id reference' + // .. 'relativeiri' + ], testNotSafe: true }); }); @@ -2555,21 +2319,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - relativeiri: 1 - } - }, - eventCounts: { - codes: { - 'relative @id reference': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @id reference' + // .. 'relativeiri' + ], testNotSafe: true }); }); @@ -2603,21 +2358,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - relativeiri: 1 - } - }, - eventCounts: { - codes: { - 'relative @id reference': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @id reference' + // .. 'relativeiri' + ], testNotSafe: true }); }); @@ -2650,21 +2396,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - relativeiri: 1 - } - }, - eventCounts: { - codes: { - 'relative @id reference': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @id reference' + // .. 'relativeiri' + ], testNotSafe: true }); }); @@ -2699,26 +2436,14 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 - }, - unmappedProperty: { - id: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'relative @type reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @type reference', + // .. 'relativeiri' + 'invalid property' + // .. 'id' + ], testNotSafe: true }); }); @@ -2764,26 +2489,14 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 - }, - unmappedProperty: { - id: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'relative @type reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @type reference', + // .. 'relativeiri' + 'invalid property' + // .. 'id' + ], testNotSafe: true }); }); @@ -2820,28 +2533,18 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 8, - prependedIri: { - anotherRelativeiri: 1, - relativeiri: 1 - }, - relativeIri: { - anotherRelativeiri: 1, - id: 2, - relativeiri: 2 - }, - unmappedProperty: { - id: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'relative @type reference': 2 - }, - events: 3 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @type reference', + // .. 'relativeiri' + 'prepending @base during expansion', + // .. 'anotherRelativeiri' + 'relative @type reference', + // .. 'anotherRelativeiri' + 'invalid property' + // 'id' + ], testNotSafe: true }); }); @@ -2888,28 +2591,18 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 8, - prependedIri: { - anotherRelativeiri: 1, - relativeiri: 1 - }, - relativeIri: { - anotherRelativeiri: 1, - id: 2, - relativeiri: 2 - }, - unmappedProperty: { - id: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'relative @type reference': 2 - }, - events: 3 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @type reference', + // .. 'relativeiri' + 'prepending @base during expansion', + // .. 'anotherRelativeiri' + 'relative @type reference', + // .. 'anotherRelativeiri' + 'invalid property' + // .. 'id' + ], testNotSafe: true }); }); @@ -2946,26 +2639,14 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 - }, - unmappedProperty: { - id: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'relative @type reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @type reference', + // .. 'relativeiri' + 'invalid property' + // .. 'id' + ], testNotSafe: true }); }); @@ -3000,26 +2681,14 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 6, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - id: 2, - relativeiri: 2 - }, - unmappedProperty: { - id: 1 - } - }, - eventCounts: { - codes: { - 'invalid property': 1, - 'relative @type reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'invalid property', + // .. 'relativeiri' + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @type reference' + // .. 'id' + ], testNotSafe: true }); }); @@ -3043,25 +2712,13 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - prependedIri: { - 'relativeiri': 1 - }, - relativeIri: { - 'relativeiri': 1 - }, - unmappedValue: { - 'relativeiri': 1 - } - }, - eventCounts: { - codes: { - 'object with only @id': 1, - 'relative @id reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @id reference', + // .. 'relativeiri' + 'object with only @id' + ], testNotSafe: true }); }); @@ -3085,25 +2742,13 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - '/relativeiri': 1 - }, - unmappedValue: { - '/relativeiri': 1 - } - }, - eventCounts: { - codes: { - 'object with only @id': 1, - 'relative @id reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @id reference', + // .. 'relativeiri' + 'object with only @id' + ], testNotSafe: true }); }); @@ -3132,21 +2777,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 3, - prependedIri: { - relativeiri: 1 - }, - relativeIri: { - 'relativeiri': 2 - } - }, - eventCounts: { - codes: { - 'relative @type reference': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeiri' + 'relative @type reference', + // .. 'relativeiri' + ], testNotSafe: true }); }); @@ -3175,24 +2811,18 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 6, - prependedIri: { - './': 1, - relativeiri: 2 - }, - relativeIri: { - '/': 1, - '/relativeiri': 2 - } - }, - eventCounts: { - codes: { - 'relative @type reference': 1, - 'relative @vocab reference': 1 - }, - events: 2 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. './' + 'relative @vocab reference', + // .. './' + 'prepending @vocab during expansion', + // .. 'relativeiri' + 'prepending @vocab during expansion', + // .. 'relativeiri' + 'relative @type reference' + // .. 'relativeiri' + ], testNotSafe: true }); }); @@ -3224,13 +2854,16 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 4, - prependedIri: { - term: 4 - } - }, - eventCounts: {}, + eventCodeLog: [ + 'prepending @vocab during expansion', + // .. 'term' + 'prepending @vocab during expansion', + // .. 'term' + 'prepending @vocab during expansion', + // .. 'term' + 'prepending @vocab during expansion' + // .. 'term' + ], testSafe: true }); }); @@ -3259,13 +2892,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeIri: 2 - } - }, - eventCounts: {}, + eventCodeLog: [ + 'prepending @vocab during expansion', + // .. 'relativeIri' + 'prepending @vocab during expansion' + // .. 'relativeIri' + ], testSafe: true }); }); @@ -3295,13 +2927,12 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeIri: 2 - } - }, - eventCounts: {}, + eventCodeLog: [ + 'prepending @vocab during expansion', + // .. 'relativeIri' + 'prepending @vocab during expansion' + // .. 'relativeIri' + ], testSafe: true }); }); @@ -3342,7 +2973,18 @@ _:b0 "v" . type: 'expand', input, expected, - eventCounts: {}, + eventCodeLog: [ + 'prepending @vocab during expansion', + // .. 'ta' + 'prepending @vocab during expansion', + // .. 'ta' + 'prepending @vocab during expansion', + // .. 'rel/' + 'prepending @vocab during expansion', + // .. 'tb' + 'prepending @vocab during expansion' + // .. 'tb' + ], testSafe: true }); }); @@ -3366,21 +3008,11 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - unmappedValue: { - 'http://example.com/relativeIri': 1 - } - }, - eventCounts: { - codes: { - 'object with only @id': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeIri' + 'object with only @id' + ], testNotSafe: true }); }); @@ -3405,21 +3037,11 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - unmappedValue: { - 'http://example.com/relativeIri': 1 - } - }, - eventCounts: { - codes: { - 'object with only @id': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'prepending @base during expansion', + // .. 'relativeIri' + 'object with only @id' + ], testNotSafe: true }); }); @@ -3448,16 +3070,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - relativeIri: { - relativeIri: 1 - } - }, - eventCounts: {}, + eventCodeLog: [ + 'prepending @base during expansion' + // .. 'relativeIri' + ], // FIXME testSafe: true }); @@ -3488,17 +3104,10 @@ _:b0 "v" . type: 'expand', input, expected, - mapCounts: { - expansionMap: 2, - prependedIri: { - relativeIri: 1 - }, - relativeIri: { - relativeIri: 1 - } - }, - eventCounts: {}, - eventLog: [], + eventCodeLog: [ + 'prepending @base during expansion' + // .. 'relativeIri' + ], testSafe: true }); }); @@ -3530,13 +3139,10 @@ _:b0 "v" . type: 'fromRDF', input, expected, - mapCounts: {}, - eventCounts: { - codes: { - 'invalid @language value': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'invalid @language value' + // .. 'abcdefghi' + ], testNotSafe: true }); }); @@ -3594,13 +3200,9 @@ _:b0 "v" . rdfDirection: 'i18n-datatype', }, expected, - mapCounts: {}, - eventCounts: { - codes: { - 'invalid @language value': 1 - }, - events: 1 - }, + eventCodeLog: [ + 'invalid @language value' + ], testNotSafe: true }); }); @@ -3635,6 +3237,7 @@ _:b0 "v" . expected: nq, eventCodeLog: [ 'relative graph reference' + // .. 'rel' ], testNotSafe: true }); @@ -3663,6 +3266,7 @@ _:b0 "v" . expected: nq, eventCodeLog: [ 'relative subject reference' + // .. 'rel' ], testNotSafe: true }); @@ -3690,6 +3294,7 @@ _:b0 "v" . expected: nq, eventCodeLog: [ 'relative property reference' + // .. 'rel' ], testNotSafe: true }); @@ -3721,6 +3326,7 @@ _:b0 "v" . expected: nq, eventCodeLog: [ 'relative type reference' + // .. 'rel' ], testNotSafe: true }); @@ -3748,12 +3354,13 @@ _:b0 "v" . expected: nq, eventCodeLog: [ 'blank node predicate' + // .. '_:p' ], testNotSafe: true }); }); - it('should handle generlized RDf blank node predicates', async () => { + it('should handle generlized RDF blank node predicates', async () => { const input = [ { @@ -3782,7 +3389,7 @@ _:b0 <_:b1> "v" . }); }); - it.skip('should handle null @id', async () => { + it('should handle null @id', async () => { const input = [ { From 754c551d99756fa32dd8c9fc0d84c6c1e0b5bdbd Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 6 Aug 2022 01:29:16 -0400 Subject: [PATCH 096/181] Remove prepending `@base` and `@vocab` events. - More appropriate events are already emitted for the real data issues. - This may be useful in the future for a debug mode. --- lib/context.js | 6 +++ tests/misc.js | 144 ++++++++++++++++++++++++------------------------- 2 files changed, 78 insertions(+), 72 deletions(-) diff --git a/lib/context.js b/lib/context.js index b06442c6..26cfb27d 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1106,6 +1106,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { if(relativeTo.vocab && '@vocab' in activeCtx) { // prepend vocab const prependedResult = activeCtx['@vocab'] + value; + // FIXME: needed? may be better as debug event. + /* if(options && options.eventHandler) { _handleEvent({ event: { @@ -1124,6 +1126,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { options }); } + */ // the null case preserves value as potentially relative value = prependedResult; } else if(relativeTo.base) { @@ -1142,6 +1145,8 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { base = options.base; prependedResult = prependBase(options.base, value); } + // FIXME: needed? may be better as debug event. + /* if(options && options.eventHandler) { _handleEvent({ event: { @@ -1160,6 +1165,7 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { options }); } + */ // the null case preserves value as potentially relative value = prependedResult; } diff --git a/tests/misc.js b/tests/misc.js index 3734bd28..b5761900 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -2248,8 +2248,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @id reference', // .. 'relativeiri' 'object with only @id' @@ -2283,8 +2283,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + ////'prepending @base during expansion', + //// .. 'relativeiri' 'relative @id reference' // .. 'relativeiri' ], @@ -2320,8 +2320,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @id reference' // .. 'relativeiri' ], @@ -2359,8 +2359,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @id reference' // .. 'relativeiri' ], @@ -2397,8 +2397,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @id reference' // .. 'relativeiri' ], @@ -2437,8 +2437,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @type reference', // .. 'relativeiri' 'invalid property' @@ -2490,8 +2490,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @type reference', // .. 'relativeiri' 'invalid property' @@ -2534,12 +2534,12 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @type reference', // .. 'relativeiri' - 'prepending @base during expansion', - // .. 'anotherRelativeiri' + //'prepending @base during expansion', + //// .. 'anotherRelativeiri' 'relative @type reference', // .. 'anotherRelativeiri' 'invalid property' @@ -2592,12 +2592,12 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @type reference', // .. 'relativeiri' - 'prepending @base during expansion', - // .. 'anotherRelativeiri' + //'prepending @base during expansion', + //// .. 'anotherRelativeiri' 'relative @type reference', // .. 'anotherRelativeiri' 'invalid property' @@ -2640,8 +2640,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @type reference', // .. 'relativeiri' 'invalid property' @@ -2684,8 +2684,8 @@ _:b0 "v" . eventCodeLog: [ 'invalid property', // .. 'relativeiri' - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @type reference' // .. 'id' ], @@ -2713,8 +2713,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @id reference', // .. 'relativeiri' 'object with only @id' @@ -2743,8 +2743,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @id reference', // .. 'relativeiri' 'object with only @id' @@ -2778,8 +2778,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeiri' + //'prepending @base during expansion', + //// .. 'relativeiri' 'relative @type reference', // .. 'relativeiri' ], @@ -2812,14 +2812,14 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. './' + //'prepending @base during expansion', + //// .. './' 'relative @vocab reference', // .. './' - 'prepending @vocab during expansion', - // .. 'relativeiri' - 'prepending @vocab during expansion', - // .. 'relativeiri' + //'prepending @vocab during expansion', + //// .. 'relativeiri' + //'prepending @vocab during expansion', + //// .. 'relativeiri' 'relative @type reference' // .. 'relativeiri' ], @@ -2855,14 +2855,14 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @vocab during expansion', - // .. 'term' - 'prepending @vocab during expansion', - // .. 'term' - 'prepending @vocab during expansion', - // .. 'term' - 'prepending @vocab during expansion' - // .. 'term' + //'prepending @vocab during expansion', + //// .. 'term' + //'prepending @vocab during expansion', + //// .. 'term' + //'prepending @vocab during expansion', + //// .. 'term' + //'prepending @vocab during expansion' + //// .. 'term' ], testSafe: true }); @@ -2893,10 +2893,10 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @vocab during expansion', - // .. 'relativeIri' - 'prepending @vocab during expansion' - // .. 'relativeIri' + //'prepending @vocab during expansion', + //// .. 'relativeIri' + //'prepending @vocab during expansion' + //// .. 'relativeIri' ], testSafe: true }); @@ -2928,10 +2928,10 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @vocab during expansion', - // .. 'relativeIri' - 'prepending @vocab during expansion' - // .. 'relativeIri' + //'prepending @vocab during expansion', + //// .. 'relativeIri' + //'prepending @vocab during expansion' + //// .. 'relativeIri' ], testSafe: true }); @@ -2974,16 +2974,16 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @vocab during expansion', - // .. 'ta' - 'prepending @vocab during expansion', - // .. 'ta' - 'prepending @vocab during expansion', - // .. 'rel/' - 'prepending @vocab during expansion', - // .. 'tb' - 'prepending @vocab during expansion' - // .. 'tb' + //'prepending @vocab during expansion', + //// .. 'ta' + //'prepending @vocab during expansion', + //// .. 'ta' + //'prepending @vocab during expansion', + //// .. 'rel/' + //'prepending @vocab during expansion', + //// .. 'tb' + //'prepending @vocab during expansion' + //// .. 'tb' ], testSafe: true }); @@ -3009,8 +3009,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeIri' + //'prepending @base during expansion', + //// .. 'relativeIri' 'object with only @id' ], testNotSafe: true @@ -3038,8 +3038,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion', - // .. 'relativeIri' + //'prepending @base during expansion', + //// .. 'relativeIri' 'object with only @id' ], testNotSafe: true @@ -3071,8 +3071,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion' - // .. 'relativeIri' + //'prepending @base during expansion' + //// .. 'relativeIri' ], // FIXME testSafe: true @@ -3105,8 +3105,8 @@ _:b0 "v" . input, expected, eventCodeLog: [ - 'prepending @base during expansion' - // .. 'relativeIri' + //'prepending @base during expansion' + //// .. 'relativeIri' ], testSafe: true }); From fa19df32d313198cdb144c1cc2382ae163ece7b0 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 6 Aug 2022 02:05:21 -0400 Subject: [PATCH 097/181] Fix lint issue. --- lib/context.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/context.js b/lib/context.js index 26cfb27d..175a6775 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1097,11 +1097,11 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { // A flag that captures whether the iri being expanded is // the value for an @type - let typeExpansion = false; + //let typeExpansion = false; - if(options !== undefined && options.typeExpansion !== undefined) { - typeExpansion = options.typeExpansion; - } + //if(options !== undefined && options.typeExpansion !== undefined) { + // typeExpansion = options.typeExpansion; + //} if(relativeTo.vocab && '@vocab' in activeCtx) { // prepend vocab From 7edd8aa2c65dd8b450f4cbc6537d9d911a90ee52 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 10 Aug 2022 10:34:23 -0400 Subject: [PATCH 098/181] Fix typo. Co-authored-by: Dave Longley --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ec76de..17a6cb39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Add "safe mode" to all APIs. Enable by adding `{safe: true}` to API options. This mode causes processing to fail when data constructs are encountered that result in lossy behavior or other data warnings. This is intended to be the - common way that digital signing and similar applications use this libraray. + common way that digital signing and similar applications use this library. ## 6.0.0 - 2022-06-06 From ed4918abba9bb3f63bde336d066de9f4cc5732f7 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 10 Aug 2022 10:57:15 -0400 Subject: [PATCH 099/181] Fix changelog. Fix rebase issues. --- CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a6cb39..08f310ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ result in lossy behavior or other data warnings. This is intended to be the common way that digital signing and similar applications use this library. +### Removed +- Experimental non-standard `protectedMode` option. +- **BREAKING**: Various console warnings were removed. The newly added "safe + mode" can stop processing where these warnings occurred. +- **BREAKING**: Remove `compactionMap` and `expansionMap`. Their known use + cases are addressed with "safe mode" and future planned features. + ## 6.0.0 - 2022-06-06 ### Changed @@ -36,13 +43,6 @@ and provide other improvements. - Use global `URL` interface to handle relative redirects. -### Removed -- Experimental non-standard `protectedMode` option. -- **BREAKING**: Various console warnings were removed. The newly added "safe - mode" can stop processing where these warnings were. -- **BREAKING**: Remove `compactionMap` and `expansionMap`. Their known use - cases are addressed with "safe mode" and future planned features. - ## 5.2.0 - 2021-04-07 ### Changed From 96144f7314d86a2559b90c70fa70207f5b49d059 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 2 Apr 2022 00:47:28 -0400 Subject: [PATCH 100/181] Fixed compact t0111 test. --- CHANGELOG.md | 3 +++ lib/compact.js | 4 +++- tests/test-common.js | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08f310ea..01d2b2b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # jsonld ChangeLog +### Fixed +- compact t0111 test: "Keyword-like relative IRIs" + ### Changed - Change EARL Assertor to Digital Bazaar, Inc. - Update eslint dependencies. diff --git a/lib/compact.js b/lib/compact.js index ccccc47f..e6bf0d13 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -34,6 +34,7 @@ const { } = require('./url'); const { + REGEX_KEYWORD, addValue: _addValue, asArray: _asArray, compareShortestLeast: _compareShortestLeast @@ -927,7 +928,8 @@ api.compactIri = ({ // The None case preserves rval as potentially relative return iri; } else { - return _removeBase(_prependBase(base, activeCtx['@base']), iri); + const _iri = _removeBase(_prependBase(base, activeCtx['@base']), iri); + return REGEX_KEYWORD.test(_iri) ? `./${_iri}` : _iri; } } else { return _removeBase(base, iri); diff --git a/tests/test-common.js b/tests/test-common.js index 093b2355..d415c47c 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -35,7 +35,6 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ - /compact-manifest#t0111$/, /compact-manifest#t0112$/, /compact-manifest#t0113$/, // html From 839e7b63f4422ee878d36a19b3c7396ff27c8f31 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Tue, 16 Aug 2022 13:43:59 -0400 Subject: [PATCH 101/181] Update changelog. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01d2b2b0..7efab7ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # jsonld ChangeLog +## 7.0.0 - 2022-08-16 + ### Fixed - compact t0111 test: "Keyword-like relative IRIs" From eb980939765c5407e2579b2b77b9aa613a6ce218 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Tue, 16 Aug 2022 13:43:59 -0400 Subject: [PATCH 102/181] Release 7.0.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba950d6e..748ac39b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "6.0.1-0", + "version": "7.0.0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 6f9c6fe76bb98a0e424e0a92ddddd1de476ef5a1 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Tue, 16 Aug 2022 13:44:36 -0400 Subject: [PATCH 103/181] Start 7.0.1-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 748ac39b..50d93da6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "7.0.0", + "version": "7.0.1-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 0229176701924696891e60bd0faed9a863344308 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Fri, 19 Aug 2022 15:14:40 -0400 Subject: [PATCH 104/181] Use safe mode and `null` base by default in `canonize`. --- CHANGELOG.md | 6 ++++++ lib/jsonld.js | 13 ++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7efab7ab..17afae62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # jsonld ChangeLog +## 8.0.0 - 2022-08-xx + +### Changed +- **BREAKING**: By default, set safe mode to `true` and `base` to + `null` in `canonize`. + ## 7.0.0 - 2022-08-16 ### Fixed diff --git a/lib/jsonld.js b/lib/jsonld.js index 8073de26..ce9b868c 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -524,12 +524,18 @@ jsonld.link = async function(input, ctx, options) { * unless the 'inputFormat' option is used. The output is an RDF dataset * unless the 'format' option is used. * + * Note: Canonicalization sets `safe` to `true` and `base` to `null` by + * default in order to produce safe outputs and "fail closed" by default. This + * is different from the other API transformations in this version which + * allow unsafe defaults (for cryptographic usage) in order to comply with the + * JSON-LD 1.1 specification. + * * @param input the input to normalize as JSON-LD or as a format specified by * the 'inputFormat' option. * @param [options] the options to use: * [algorithm] the normalization algorithm to use, `URDNA2015` or * `URGNA2012` (default: `URDNA2015`). - * [base] the base IRI to use. + * [base] the base IRI to use (default: `null`). * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. @@ -539,7 +545,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 - * [safe] true to use safe mode. (default: false) + * [safe] true to use safe mode. (default: true). * [contextResolver] internal use only. * * @return a Promise that resolves to the normalized output. @@ -551,9 +557,10 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { // set default options options = _setDefaults(options, { - base: _isString(input) ? input : '', + base: _isString(input) ? input : null, algorithm: 'URDNA2015', skipExpansion: false, + safe: true, contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); From 16a040b8f184e324a46c4dcc3fcfd1ddf2e4e328 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 22 Aug 2022 20:22:49 -0400 Subject: [PATCH 105/181] Add safe canonize mode tests. --- tests/misc.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/misc.js b/tests/misc.js index b5761900..683086be 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -3417,3 +3417,53 @@ _:b0 "v" . }); }); }); + +describe('safe canonize defaults', () => { + it('does not throw on safe input', async () => { + const input = +{ + "@id": "ex:id", + "ex:p": "v" +} +; + const expected = +' "v" .\n' +; + const result = await jsonld.canonize(input); + assert.deepStrictEqual(result, expected); + }); + + it('throws on unsafe input', async () => { + const input = +{ + "@id": "ex:id", + "ex:p": "v", + "unknown": "error" +} +; + let error; + try { + await jsonld.canonize(input); + } catch(e) { + error = e; + } + assert(error, 'missing safe validation error'); + }); + + it('allows override of safe mode', async () => { + const input = +{ + "@id": "ex:id", + "ex:p": "v", + "unknown": "error" +} +; + const expected = +' "v" .\n' +; + const result = await jsonld.canonize(input, { + safe: false + }); + assert.deepStrictEqual(result, expected); + }); +}); From b896cb33cb1e5f6bbcb2215391f08ce912c3a0fb Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Tue, 23 Aug 2022 11:25:46 -0400 Subject: [PATCH 106/181] Update changelog. --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17afae62..1bf78a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,12 @@ ### Changed - **BREAKING**: By default, set safe mode to `true` and `base` to - `null` in `canonize`. + `null` in `canonize`. Applications that were previously + canonizing data may see new errors if their data did not fully + define terms or used relative URLs that would be dropped when + converting to canonized RDF. Now these situations are caught + via `safe` mode by default, informing the developer that they + need to fix their data. ## 7.0.0 - 2022-08-16 From 89bfd4e1d6816979a2b4e6a6f7e64404341bdfe2 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Tue, 23 Aug 2022 11:26:26 -0400 Subject: [PATCH 107/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf78a4b..b64f4242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.0.0 - 2022-08-xx +## 8.0.0 - 2022-08-23 ### Changed - **BREAKING**: By default, set safe mode to `true` and `base` to From e4b66c8393a2dae8859b5035dcc3bd005cf9958b Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Tue, 23 Aug 2022 11:26:27 -0400 Subject: [PATCH 108/181] Release 8.0.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 50d93da6..1c6c2746 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "7.0.1-0", + "version": "8.0.0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 04cdf49b2ed02b4b787a3bdff63dd60118cf2c6c Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Tue, 23 Aug 2022 11:27:29 -0400 Subject: [PATCH 109/181] Start 8.0.1-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c6c2746..a96beed4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.0.0", + "version": "8.0.1-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From f4055adbf3b08a0e61566cf0baeb19191db560d0 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 26 Aug 2022 22:19:10 -0400 Subject: [PATCH 110/181] Fix toRDF event names. - `relative property reference` event renamed to `relative predicate reference`. - `relative type reference` event renamed to `relative object reference`. --- CHANGELOG.md | 7 +++++++ lib/events.js | 6 +++--- lib/toRdf.js | 12 ++++++------ tests/misc.js | 8 ++++---- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b64f4242..05bec1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # jsonld ChangeLog +## 8.1.0 - 2022-xx-xx + +### Fixed +- `relative property reference` event renamed to `relative predicate + reference`. +- `relative type reference` event renamed to `relative object reference`. + ## 8.0.0 - 2022-08-23 ### Changed diff --git a/lib/events.js b/lib/events.js index 939885a1..3dbd6046 100644 --- a/lib/events.js +++ b/lib/events.js @@ -121,9 +121,9 @@ const _notSafeEventCodes = new Set([ // toRDF 'blank node predicate', 'relative graph reference', - 'relative property reference', - 'relative subject reference', - 'relative type reference' + 'relative object reference', + 'relative predicate reference', + 'relative subject reference' ]); // safe handler that rejects unsafe warning conditions diff --git a/lib/toRdf.js b/lib/toRdf.js index eac6bad3..d823a97e 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -154,11 +154,11 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'relative property reference', + code: 'relative predicate reference', level: 'warning', - message: 'Relative property reference found.', + message: 'Relative predicate reference found.', details: { - property + predicate: property } }, options @@ -345,11 +345,11 @@ function _objectToRDF( _handleEvent({ event: { type: ['JsonLdEvent'], - code: 'relative type reference', + code: 'relative object reference', level: 'warning', - message: 'Relative type reference found.', + message: 'Relative object reference found.', details: { - type: object.value + object: object.value } }, options diff --git a/tests/misc.js b/tests/misc.js index 683086be..3e5a74e3 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -3272,7 +3272,7 @@ _:b0 "v" . }); }); - it('should handle relative property reference', async () => { + it('should handle relative predicate reference', async () => { const input = [ { @@ -3293,14 +3293,14 @@ _:b0 "v" . options: {skipExpansion: true}, expected: nq, eventCodeLog: [ - 'relative property reference' + 'relative predicate reference' // .. 'rel' ], testNotSafe: true }); }); - it('should handle relative property reference', async () => { + it('should handle relative object reference', async () => { const input = [ { @@ -3325,7 +3325,7 @@ _:b0 "v" . options: {skipExpansion: true}, expected: nq, eventCodeLog: [ - 'relative type reference' + 'relative object reference' // .. 'rel' ], testNotSafe: true From 4b1845b10a3bef8b08d0f87813871cd6f6b28493 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 29 Aug 2022 14:22:12 -0400 Subject: [PATCH 111/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05bec1be..9f3128b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.1.0 - 2022-xx-xx +## 8.1.0 - 2022-08-29 ### Fixed - `relative property reference` event renamed to `relative predicate From c28146fefb2bc41c4afed0fe6bbcf686d3e88914 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 29 Aug 2022 14:22:15 -0400 Subject: [PATCH 112/181] Release 8.1.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a96beed4..6bcaa521 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.0.1-0", + "version": "8.1.0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 96ce33b8819f5c6260d1c24d4ba9085f67e730a4 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 29 Aug 2022 14:23:17 -0400 Subject: [PATCH 113/181] Start 8.1.1-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6bcaa521..590f7f24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.1.0", + "version": "8.1.1-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 916d70160b4bc50e184a9e1501daba60713e5df8 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 29 Aug 2022 16:35:39 -0400 Subject: [PATCH 114/181] Add expand and toRDF for non-IRI test. --- tests/misc.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/misc.js b/tests/misc.js index 3e5a74e3..cd36459a 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -3416,6 +3416,61 @@ _:b0 "v" . }); }); }); + + describe('various', () => { + it('expand and toRDF for non-IRI', async () => { + const input = +{ + "@context": { + "ex": "urn:ex#", + "ex:prop": { + "@type": "@id" + } + }, + "@id": "urn:id", + "@type": "ex:type", + "ex:prop": "value" +} +; + const expanded = +[ + { + "@id": "urn:id", + "@type": [ + "urn:ex#type" + ], + "urn:ex#prop": [ + { + "@id": "value" + } + ] + } +] +; + const nq = `\ + . +`; + + await _test({ + type: 'expand', + input, + expected: expanded, + eventCodeLog: [], + testSafe: true + }); + await _test({ + type: 'toRDF', + input: expanded, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [ + 'relative object reference' + // .. 'value' + ], + testNotSafe: true + }); + }); + }); }); describe('safe canonize defaults', () => { From a37827ca20f0eae6c7c404e5bb00084ae07067d4 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 25 Feb 2023 19:21:47 -0500 Subject: [PATCH 115/181] Fix safe mode with `@json` value object. --- CHANGELOG.md | 5 +++++ lib/expand.js | 2 +- tests/misc.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3128b6..0664085d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # jsonld ChangeLog +## 8.1.1 - 2023-02-xx + +### Fixed +- Don't fail in safe mode for a value object with `"@type": "@json"`. + ## 8.1.0 - 2022-08-29 ### Fixed diff --git a/lib/expand.js b/lib/expand.js index d2d94a20..2695a415 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -614,7 +614,7 @@ async function _expandObject({ const ve = _expandIri(typeScopedContext, v, {base: true, vocab: true}, {...options, typeExpansion: true}); - if(!_isAbsoluteIri(ve)) { + if(ve !== '@json' && !_isAbsoluteIri(ve)) { if(options.eventHandler) { _handleEvent({ event: { diff --git a/tests/misc.js b/tests/misc.js index cd36459a..9ca9ac7a 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1507,6 +1507,48 @@ _:b0 "v" . }); }); + it('should have zero counts with @json value', async () => { + const input = +{ + "ex:p": { + "@type": "@json", + "@value": [null] + } +} +; + const expected = +[ + { + "ex:p": [ + { + "@type": "@json", + "@value": [null] + } + ] + } +] +; + const nq = `\ +_:b0 "[null]"^^ . +` +; + + await _test({ + type: 'expand', + input, + expected, + eventCounts: {}, + testSafe: true + }); + await _test({ + type: 'toRDF', + input, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + it('should count empty top-level object', async () => { const input = {}; const expected = []; From ffb62fcc3aed1d3949f96918ed32b05a85a48a9d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 25 Feb 2023 20:42:13 -0500 Subject: [PATCH 116/181] Temporarily ignore `@json` framing test. --- tests/test-common.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test-common.js b/tests/test-common.js index d415c47c..2e440aa5 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -144,6 +144,7 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ + /frame-manifest#t0069$/, ] }, fn: 'frame', From e5e437def92c274598da8e329c5bdd01ee9ebf7e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 25 Feb 2023 20:53:00 -0500 Subject: [PATCH 117/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0664085d..e0862603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.1.1 - 2023-02-xx +## 8.1.1 - 2023-02-25 ### Fixed - Don't fail in safe mode for a value object with `"@type": "@json"`. From a9e890610e7664ff33dd38d9f690b4b56ec1557f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 25 Feb 2023 20:53:01 -0500 Subject: [PATCH 118/181] Release 8.1.1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 590f7f24..f089434d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.1.1-0", + "version": "8.1.1", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From d75b90530a1f96f284db31a2d23f38672e8c841a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 25 Feb 2023 20:54:04 -0500 Subject: [PATCH 119/181] Start 8.1.2-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f089434d..d1ad2508 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.1.1", + "version": "8.1.2-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 9287be43873ac78c81472c679e38b8f246f367f6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 16:56:54 -0500 Subject: [PATCH 120/181] Update for latest rdf-canon changes. Update for latest W3C RDF Dataset Canonicalization "rdf-canon" changes: - test suite location - README - links - identifiers --- CHANGELOG.md | 7 +++++++ README.md | 8 ++++---- package.json | 4 ++-- tests/test-common.js | 8 ++++---- tests/test-karma.js | 6 +++--- tests/test.js | 10 +++++----- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0862603..7d3c02b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # jsonld ChangeLog +## 8.1.2 - 2023-03-xx + +### Changed +- Update for latest [rdf-canon][] changes: test suite location, README, + links, and identifiers. + ## 8.1.1 - 2023-02-25 ### Fixed @@ -831,5 +837,6 @@ [jsonld-cli]: https://github.com/digitalbazaar/jsonld-cli [jsonld-request]: https://github.com/digitalbazaar/jsonld-request +[rdf-canon]: https://w3c.github.io/rdf-canon/ [rdf-canonize]: https://github.com/digitalbazaar/rdf-canonize [rdf-canonize-native]: https://github.com/digitalbazaar/rdf-canonize-native diff --git a/README.md b/README.md index 045325a4..b75b5d66 100644 --- a/README.md +++ b/README.md @@ -259,11 +259,11 @@ const framed = await jsonld.frame(doc, frame); // output transformed into a particular tree structure per the given frame ``` -### [canonize](http://json-ld.github.io/normalization/spec/) (normalize) +### [canonize](https://w3c.github.io/rdf-canon/spec/) (normalize) ```js -// canonize (normalize) a document using the RDF Dataset Normalization Algorithm -// (URDNA2015), see: +// canonize (normalize) a document using the RDF Dataset Canonicalization Algorithm +// (URDNA2015): const canonized = await jsonld.canonize(doc, { algorithm: 'URDNA2015', format: 'application/n-quads' @@ -398,7 +398,7 @@ the following: https://github.com/w3c/json-ld-api https://github.com/w3c/json-ld-framing https://github.com/json-ld/json-ld.org - https://github.com/json-ld/normalization + https://github.com/w3c/rdf-canon They should be sibling directories of the jsonld.js directory or in a `test-suites` dir. To clone shallow copies into the `test-suites` dir you can diff --git a/package.json b/package.json index d1ad2508..acad97e8 100644 --- a/package.json +++ b/package.json @@ -92,12 +92,12 @@ "prepack": "npm run build", "build": "npm run build-webpack", "build-webpack": "webpack", - "fetch-test-suites": "npm run fetch-json-ld-wg-test-suite && npm run fetch-json-ld-org-test-suite && npm run fetch-normalization-test-suite", + "fetch-test-suites": "npm run fetch-json-ld-wg-test-suite && npm run fetch-json-ld-org-test-suite && npm run fetch-rdf-canon-test-suite", "fetch-json-ld-wg-test-suite": "npm run fetch-json-ld-api-test-suite && npm run fetch-json-ld-framing-test-suite", "fetch-json-ld-api-test-suite": "if [ ! -e test-suites/json-wg-api ]; then git clone --depth 1 https://github.com/w3c/json-ld-api.git test-suites/json-ld-api; fi", "fetch-json-ld-framing-test-suite": "if [ ! -e test-suites/json-wg-framing ]; then git clone --depth 1 https://github.com/w3c/json-ld-framing.git test-suites/json-ld-framing; fi", "fetch-json-ld-org-test-suite": "if [ ! -e test-suites/json-ld.org ]; then git clone --depth 1 https://github.com/json-ld/json-ld.org.git test-suites/json-ld.org; fi", - "fetch-normalization-test-suite": "if [ ! -e test-suites/normalization ]; then git clone --depth 1 https://github.com/json-ld/normalization.git test-suites/normalization; fi", + "fetch-rdf-canon-test-suite": "if [ ! -e test-suites/rdf-canon ]; then git clone --depth 1 https://github.com/w3c/rdf-canon.git test-suites/rdf-canon; fi", "test": "npm run test-node", "test-node": "cross-env NODE_ENV=test mocha --delay -t 30000 -A -R ${REPORTER:-spec} tests/test.js", "test-karma": "cross-env NODE_ENV=test karma start", diff --git a/tests/test-common.js b/tests/test-common.js index 2e440aa5..3e3ac796 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -246,7 +246,7 @@ const TEST_TYPES = { ], compare: compareCanonizedExpectedNQuads }, - 'rdfn:Urgna2012EvalTest': { + 'rdfc:Urgna2012EvalTest': { fn: 'normalize', params: [ readTestNQuads('action'), @@ -258,7 +258,7 @@ const TEST_TYPES = { ], compare: compareExpectedNQuads }, - 'rdfn:Urdna2015EvalTest': { + 'rdfc:Urdna2015EvalTest': { fn: 'normalize', params: [ readTestNQuads('action'), @@ -593,8 +593,8 @@ function addTest(manifest, test, tests) { if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { await compareExpectedError(test, err); } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || - isJsonLdType(test, 'rdfn:Urgna2012EvalTest') || - isJsonLdType(test, 'rdfn:Urdna2015EvalTest')) { + isJsonLdType(test, 'rdfc:Urgna2012EvalTest') || + isJsonLdType(test, 'rdfc:Urdna2015EvalTest')) { if(err) { throw err; } diff --git a/tests/test-karma.js b/tests/test-karma.js index b9f71c8f..33910732 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -88,10 +88,10 @@ if(process.env.JSONLD_TESTS) { _top, '../json-ld.org/test-suite/tests/frame-manifests.jsonld')); */ - // json-ld.org normalization test suite + // W3C RDF Dataset Canonicalization "rdf-canon" test suite // FIXME: add path detection - entries.push(join(_top, 'test-suites/normalization/tests')); - entries.push(join(_top, '../normalization/tests')); + entries.push(join(_top, 'test-suites/rdf-canon/tests')); + entries.push(join(_top, '../rdf-canon/tests')); // other tests entries.push(join(_top, 'tests/misc.js')); diff --git a/tests/test.js b/tests/test.js index d5f482f5..074161d2 100644 --- a/tests/test.js +++ b/tests/test.js @@ -89,13 +89,13 @@ if(process.env.JSONLD_TESTS) { } */ - // json-ld.org normalization test suite - const normPath = path.resolve(_top, 'test-suites/normalization/tests'); - if(fs.existsSync(normPath)) { - entries.push(normPath); + // W3C RDF Dataset Canonicalization "rdf-canon" test suite + const rdfCanonPath = path.resolve(_top, 'test-suites/rdf-canon/tests'); + if(fs.existsSync(rdfCanonPath)) { + entries.push(rdfCanonPath); } else { // default up to sibling dir - entries.push(path.resolve(_top, '../normalization/tests')); + entries.push(path.resolve(_top, '../rdf-canon/tests')); } // other tests From 71f4d20bd07afcddfaf971e34739403f87984a05 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 16:57:37 -0500 Subject: [PATCH 121/181] Fix formatting. --- lib/compact.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compact.js b/lib/compact.js index e6bf0d13..4aa11316 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -547,8 +547,8 @@ api.compact = async ({ let key; if(container.includes('@language')) { - // if container is a language map, simplify compacted value to - // a simple string + // if container is a language map, simplify compacted value to + // a simple string if(_isValue(compactedItem)) { compactedItem = compactedItem['@value']; } From c5f9a6a8ba607d57c61523dc71cf369bdca09339 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 17:00:40 -0500 Subject: [PATCH 122/181] Change some URLs to https. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b75b5d66..2fe09597 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ const context = { }; ``` -### [compact](http://json-ld.org/spec/latest/json-ld/#compacted-document-form) +### [compact](https://json-ld.org/spec/latest/json-ld/#compacted-document-form) ```js // compact a document according to a particular context @@ -226,7 +226,7 @@ const compacted = await jsonld.compact( 'http://example.org/doc', 'http://example.org/context', ...); ``` -### [expand](http://json-ld.org/spec/latest/json-ld/#expanded-document-form) +### [expand](https://json-ld.org/spec/latest/json-ld/#expanded-document-form) ```js // expand a document, removing its context @@ -243,7 +243,7 @@ const expanded = await jsonld.expand(compacted); const expanded = await jsonld.expand('http://example.org/doc', ...); ``` -### [flatten](http://json-ld.org/spec/latest/json-ld/#flattened-document-form) +### [flatten](https://json-ld.org/spec/latest/json-ld/#flattened-document-form) ```js // flatten a document @@ -251,7 +251,7 @@ const flattened = await jsonld.flatten(doc); // output has all deep-level trees flattened to the top-level ``` -### [frame](http://json-ld.org/spec/latest/json-ld-framing/#introduction) +### [frame](https://json-ld.org/spec/latest/json-ld-framing/#introduction) ```js // frame a document @@ -384,7 +384,7 @@ Source The source code for the JavaScript implementation of the JSON-LD API is available at: -http://github.com/digitalbazaar/jsonld.js +https://github.com/digitalbazaar/jsonld.js Tests ----- From 64ef2b346018cf9b1405ef3444d76cf386f61a96 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 17:43:56 -0500 Subject: [PATCH 123/181] Remove obsolete badge. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 2fe09597..23fe61e3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ jsonld.js [![Build status](https://img.shields.io/github/workflow/status/digitalbazaar/jsonld.js/Node.js%20CI)](https://github.com/digitalbazaar/jsonld.js/actions?query=workflow%3A%22Node.js+CI%22) [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/jsonld.js)](https://codecov.io/gh/digitalbazaar/jsonld.js) -[![Dependency Status](https://img.shields.io/david/digitalbazaar/jsonld.js.svg)](https://david-dm.org/digitalbazaar/jsonld.js) Introduction ------------ From cd7192705486ad20c3fae55b6ddb7c4177043b5b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 17:44:09 -0500 Subject: [PATCH 124/181] Add npm badge. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 23fe61e3..48fa5b63 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ jsonld.js [![Build status](https://img.shields.io/github/workflow/status/digitalbazaar/jsonld.js/Node.js%20CI)](https://github.com/digitalbazaar/jsonld.js/actions?query=workflow%3A%22Node.js+CI%22) [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/jsonld.js)](https://codecov.io/gh/digitalbazaar/jsonld.js) +[![npm](https://img.shields.io/npm/v/jsonld)](https://npm.im/jsonld) Introduction ------------ From fcf352f2040bd8861d37c0bd9b4d1659926a8760 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 17:49:29 -0500 Subject: [PATCH 125/181] Fix build badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48fa5b63..ef0cd6e9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ jsonld.js ========= -[![Build status](https://img.shields.io/github/workflow/status/digitalbazaar/jsonld.js/Node.js%20CI)](https://github.com/digitalbazaar/jsonld.js/actions?query=workflow%3A%22Node.js+CI%22) +[![Build status](https://img.shields.io/github/actions/workflow/status/digitalbazaar/jsonld.js/main.yml)](https://github.com/digitalbazaar/jsonld.js/actions/workflows/main.yml) [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/jsonld.js)](https://codecov.io/gh/digitalbazaar/jsonld.js) [![npm](https://img.shields.io/npm/v/jsonld)](https://npm.im/jsonld) From bd50f1f8c1d471714b5a5f50ef8622064fcf835d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 17:52:18 -0500 Subject: [PATCH 126/181] Reorder sections. --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ef0cd6e9..b14d810e 100644 --- a/README.md +++ b/README.md @@ -364,28 +364,6 @@ The `safe` options flag set to `true` enables this behavior: const expanded = await jsonld.expand(data, {safe: true}); ``` -Related Modules ---------------- - -* [jsonld-cli][]: A command line interface tool called `jsonld` that exposes - most of the basic jsonld.js API. -* [jsonld-request][]: A module that can read data from stdin, URLs, and files - and in various formats and return JSON-LD. - -Commercial Support ------------------- - -Commercial support for this library is available upon request from -[Digital Bazaar][]: support@digitalbazaar.com - -Source ------- - -The source code for the JavaScript implementation of the JSON-LD API -is available at: - -https://github.com/digitalbazaar/jsonld.js - Tests ----- @@ -483,6 +461,28 @@ See `tests/test.js` for more `TEST_ENV` control and options. These reports can be compared with the `benchmarks/compare/` tool and at the [JSON-LD Benchmarks][] site. +Related Modules +--------------- + +* [jsonld-cli][]: A command line interface tool called `jsonld` that exposes + most of the basic jsonld.js API. +* [jsonld-request][]: A module that can read data from stdin, URLs, and files + and in various formats and return JSON-LD. + +Source +------ + +The source code for the JavaScript implementation of the JSON-LD API +is available at: + +https://github.com/digitalbazaar/jsonld.js + +Commercial Support +------------------ + +Commercial support for this library is available upon request from +[Digital Bazaar][]: support@digitalbazaar.com + [Digital Bazaar]: https://digitalbazaar.com/ [JSON-LD 1.0 API]: http://www.w3.org/TR/2014/REC-json-ld-api-20140116/ From a85e9ea9ef74ed27284d7d746e26df26c5d2e01e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 17:59:13 -0500 Subject: [PATCH 127/181] Fix link. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b14d810e..6d6db7c6 100644 --- a/README.md +++ b/README.md @@ -518,6 +518,7 @@ Commercial support for this library is available upon request from [errata]: http://www.w3.org/2014/json-ld-errata [jsonld-cli]: https://github.com/digitalbazaar/jsonld-cli [jsonld-request]: https://github.com/digitalbazaar/jsonld-request +[rdf]: https://rubygems.org/gems/rdf [rdf-canonize-native]: https://github.com/digitalbazaar/rdf-canonize-native [test runner]: https://github.com/digitalbazaar/jsonld.js/blob/master/tests/test-common.js [test suite]: https://github.com/json-ld/json-ld.org/tree/master/test-suite From b4eb7550235e9ebfc9c947c2ad69f255db6d21d7 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 27 Apr 2023 18:24:19 -0400 Subject: [PATCH 128/181] Skip rdf-canon test with U escapes. Will enable when rdf-canonize dependency is updated. --- CHANGELOG.md | 6 ++++-- tests/test-common.js | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d3c02b5..54d23fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ ## 8.1.2 - 2023-03-xx ### Changed -- Update for latest [rdf-canon][] changes: test suite location, README, - links, and identifiers. +- Update for latest [rdf-canon][] changes: test suite location, README, links, + and identifiers. + - Skip test with 'U' escapes. Will enable when [rdf-canonize][] dependency is + updated. ## 8.1.1 - 2023-02-25 diff --git a/tests/test-common.js b/tests/test-common.js index 3e3ac796..616098fe 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -259,6 +259,14 @@ const TEST_TYPES = { compare: compareExpectedNQuads }, 'rdfc:Urdna2015EvalTest': { + skip: { + // NOTE: idRegex format: + //manifest-urdna2015#testNNN$/, + idRegex: [ + // Unsupported U escape + /manifest-urdna2015#test060/ + ] + }, fn: 'normalize', params: [ readTestNQuads('action'), From 6e69ada223d1184823816ab739efee5bc586e42e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 20 Apr 2023 00:50:43 -0400 Subject: [PATCH 129/181] Small cleanups. - Ensure a param is a boolean for clearer debugging. - Formatting. --- lib/expand.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/expand.js b/lib/expand.js index 2695a415..559079e9 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -634,7 +634,7 @@ async function _expandObject({ } return v; }), - {propertyIsArray: options.isFrame}); + {propertyIsArray: !!options.isFrame}); continue; } @@ -775,8 +775,7 @@ async function _expandObject({ expandedValue = await api.expand({ activeCtx, - activeProperty: - '@reverse', + activeProperty: '@reverse', element: value, options }); @@ -881,7 +880,7 @@ async function _expandObject({ }); } else { // recurse into @list or @set - const isList = (expandedProperty === '@list'); + const isList = expandedProperty === '@list'; if(isList || expandedProperty === '@set') { let nextActiveProperty = activeProperty; if(isList && expandedActiveProperty === '@graph') { From 6bbc3e4edc9a8c9ac7f27034a9001cd4a761ca3c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 27 Apr 2023 18:31:45 -0400 Subject: [PATCH 130/181] Update GitHub actions versions. --- .github/workflows/main.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e533b87c..689056b9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,9 +10,9 @@ jobs: matrix: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install @@ -25,9 +25,9 @@ jobs: matrix: node-version: [14.x, 16.x, 18.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install @@ -44,9 +44,9 @@ jobs: node-version: [16.x] bundler: [webpack, browserify] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install @@ -64,9 +64,9 @@ jobs: matrix: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install @@ -80,9 +80,9 @@ jobs: matrix: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install From f83ab32dc195cdec015d329dcd2f4d0822bf6aaa Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 27 Apr 2023 18:32:31 -0400 Subject: [PATCH 131/181] Test on Node.js 20.x. --- .github/workflows/main.yml | 2 +- CHANGELOG.md | 1 + package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 689056b9..15981680 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [14.x, 16.x, 18.x, 20.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d23fe0..4e66ae78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and identifiers. - Skip test with 'U' escapes. Will enable when [rdf-canonize][] dependency is updated. +- Test on Node.js 20.x. ## 8.1.1 - 2023-02-25 diff --git a/package.json b/package.json index acad97e8..a2d0cea9 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "lib/**/*.js" ], "dependencies": { - "@digitalbazaar/http-client": "^3.2.0", + "@digitalbazaar/http-client": "^3.4.1", "canonicalize": "^1.0.1", "lru-cache": "^6.0.0", "rdf-canonize": "^3.0.0" From 60d8484240c0ac18ad0e6cc06891d6e08d14dcda Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 27 Apr 2023 18:33:10 -0400 Subject: [PATCH 132/181] Use Node.js 18.x for GitHub actions. --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 15981680..c16c8e3e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} @@ -41,7 +41,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [18.x] bundler: [webpack, browserify] steps: - uses: actions/checkout@v3 @@ -62,7 +62,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} @@ -78,7 +78,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} From b5df97db866b0e2b601fb25ddad5d6d87d0aaa6d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 28 Apr 2023 20:28:05 -0400 Subject: [PATCH 133/181] Use Node.js 16.x for webpack related actions. - Node.js 18.x needs a newer webpack and related tools. Will return to 18.x when the tooling is updated. --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c16c8e3e..08332857 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [18.x] + node-version: [16.x] bundler: [webpack, browserify] steps: - uses: actions/checkout@v3 @@ -62,7 +62,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [18.x] + node-version: [16.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} From ca09db4ebd33f5b7b7e1b30d80a602e62b65c688 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 20 Apr 2023 00:40:18 -0400 Subject: [PATCH 134/181] Improve safe mode for `@graph` use cases. - Better handling for `@graph` top-level empty objects, objects with only ids, relative references, etc. --- CHANGELOG.md | 3 + lib/expand.js | 102 +++++++++++++-------- tests/misc.js | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e66ae78..c039a057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ updated. - Test on Node.js 20.x. +### Fixed +- Improve safe mode for `@graph` use cases. + ## 8.1.1 - 2023-02-25 ### Fixed diff --git a/lib/expand.js b/lib/expand.js index 559079e9..280d67db 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -372,48 +372,64 @@ api.expand = async ({ // drop certain top-level objects that do not occur in lists if(_isObject(rval) && !options.keepFreeFloatingNodes && !insideList && - (activeProperty === null || expandedActiveProperty === '@graph')) { + (activeProperty === null || + expandedActiveProperty === '@graph' || + (_getContextValue(activeCtx, activeProperty, '@container') || []) + .includes('@graph') + )) { // drop empty object, top-level @value/@list, or object with only @id - if(count === 0 || '@value' in rval || '@list' in rval || - (count === 1 && '@id' in rval)) { - // FIXME - if(options.eventHandler) { - // FIXME: one event or diff event for empty, @v/@l, {@id}? - let code; - let message; - if(count === 0) { - code = 'empty object'; - message = 'Dropping empty object.'; - } else if('@value' in rval) { - code = 'object with only @value'; - message = 'Dropping object with only @value.'; - } else if('@list' in rval) { - code = 'object with only @list'; - message = 'Dropping object with only @list.'; - } else if(count === 1 && '@id' in rval) { - code = 'object with only @id'; - message = 'Dropping object with only @id.'; - } - _handleEvent({ - event: { - type: ['JsonLdEvent'], - code, - level: 'warning', - message, - details: { - value: rval - } - }, - options - }); - } - rval = null; - } + rval = _dropUnsafeObject({value: rval, count, options}); } return rval; }; +/** + * Drop empty object, top-level @value/@list, or object with only @id + */ +function _dropUnsafeObject({ + value, + count, + options +}) { + if(count === 0 || '@value' in value || '@list' in value || + (count === 1 && '@id' in value)) { + // FIXME + if(options.eventHandler) { + // FIXME: one event or diff event for empty, @v/@l, {@id}? + let code; + let message; + if(count === 0) { + code = 'empty object'; + message = 'Dropping empty object.'; + } else if('@value' in value) { + code = 'object with only @value'; + message = 'Dropping object with only @value.'; + } else if('@list' in value) { + code = 'object with only @list'; + message = 'Dropping object with only @list.'; + } else if(count === 1 && '@id' in value) { + code = 'object with only @id'; + message = 'Dropping object with only @id.'; + } + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code, + level: 'warning', + message, + details: { + value + } + }, + options + }); + } + return null; + } + return value; +} + /** * Expand each key and value of element adding to result * @@ -933,8 +949,18 @@ async function _expandObject({ if(container.includes('@graph') && !container.some(key => key === '@id' || key === '@index')) { // ensure expanded values are arrays - expandedValue = _asArray(expandedValue) - .map(v => ({'@graph': _asArray(v)})); + // ensure an array + expandedValue = _asArray(expandedValue); + // check if needs to be dropped + const count = Object.keys(expandedValue[0]).length; + if(!options.isFrame && _dropUnsafeObject({ + value: expandedValue[0], count, options + }) === null) { + // skip adding and continue + continue; + } + // convert to graph + expandedValue = expandedValue.map(v => ({'@graph': _asArray(v)})); } // FIXME: can this be merged with code above to simplify? diff --git a/tests/misc.js b/tests/misc.js index 9ca9ac7a..f6b5b3f4 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1815,6 +1815,247 @@ _:b0 "[null]"^^ . }); }); + it('should emit for @graph with empty object (1)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + } + }, + "@id": "urn:id", + "p": {} +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'empty object', + 'object with only @id' + ], + testNotSafe: true + }); + }); + + it('should emit for ok @graph with empty object (2)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + }, + "urn:t": { + "@type": "@id" + } + }, + "@id": "urn:id", + "urn:t": "urn:id", + "p": {} +} +; + const expected = +[ + { + "@id": "urn:id", + "urn:t": [ + { + "@id": "urn:id" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'empty object' + ], + testNotSafe: true + }); + }); + + it('should emit for @graph with relative @id (1)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + } + }, + "@id": "urn:id", + "p": ["rel"] +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'object with only @id', + 'object with only @id' + ], + testNotSafe: true + }); + }); + + it('should emit for @graph with relative @id (2)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + }, + "urn:t": { + "@type": "@id" + } + }, + "@id": "urn:id", + "urn:t": "urn:id", + "p": ["rel"] +} +; + const expected = +[ + { + "@id": "urn:id", + "urn:t": [ + { + "@id": "urn:id" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'object with only @id', + ], + testNotSafe: true + }); + }); + + it('should emit for @graph with relative @id (3)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + }, + "urn:t": { + "@type": "@id" + } + }, + "@id": "urn:id", + "urn:t": "urn:id", + "p": "rel" +} +; + const expected = +[ + { + "@id": "urn:id", + "urn:t": [ + { + "@id": "urn:id" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'object with only @id', + ], + testNotSafe: true + }); + }); + + it('should emit for @graph with relative @id (4)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + }, + "urn:t": { + "@type": "@id" + } + }, + "@id": "urn:id", + "urn:t": "urn:id", + "p": { + "@id": "rel", + "urn:t": "urn:id2" + } +} +; + const expected = +[ + { + "@id": "urn:id", + "urn:t": [ + { + "@id": "urn:id" + } + ], + "urn:p": [ + { + "@graph": [ + { + "@id": "rel", + "urn:t": [ + { + "@id": "urn:id2" + } + ] + } + ] + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'relative @id reference', + ], + testNotSafe: true + }); + }); + it('should emit for null @value', async () => { const input = { From 8f5e47fb52f0efbafa89972f0c4b30642cddf688 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 1 Mar 2023 20:15:46 -0500 Subject: [PATCH 135/181] Update testing framework. - Refactor and align closer with rdf-canonize testing. - Update EARL reporting. --- package.json | 1 + tests/earl-report.js | 269 +++++++++++++++--------------- tests/test-common.js | 387 +++++++++++++++++++++++-------------------- 3 files changed, 340 insertions(+), 317 deletions(-) diff --git a/package.json b/package.json index a2d0cea9..1a7b531e 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-tap-reporter": "0.0.6", "karma-webpack": "^4.0.2", + "klona": "^2.0.5", "mocha": "^8.3.2", "mocha-lcov-reporter": "^1.3.0", "nyc": "^15.1.0", diff --git a/tests/earl-report.js b/tests/earl-report.js index e5545acd..d969e0d8 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -7,114 +7,152 @@ */ /** - * Create an EARL Reporter. - * - * @param options {Object} reporter options - * id: {String} report id - * env: {Object} environment description + * EARL Reporter */ -function EarlReport(options) { - let today = new Date(); - today = today.getFullYear() + '-' + - (today.getMonth() < 9 ? - '0' + (today.getMonth() + 1) : today.getMonth() + 1) + '-' + - (today.getDate() < 10 ? '0' + today.getDate() : today.getDate()); - // one date for tests with no subsecond resolution - this.now = new Date(); - this.now.setMilliseconds(0); - this.id = options.id; - this.env = options.env; - // test environment - this._environment = null; - /* eslint-disable quote-props */ - this._report = { - '@context': { - 'doap': 'http://usefulinc.com/ns/doap#', - 'foaf': 'http://xmlns.com/foaf/0.1/', - 'dc': 'http://purl.org/dc/terms/', - 'earl': 'http://www.w3.org/ns/earl#', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - 'jsonld': 'http://www.w3.org/ns/json-ld#', - 'doap:homepage': {'@type': '@id'}, - 'doap:license': {'@type': '@id'}, - 'dc:creator': {'@type': '@id'}, - 'foaf:homepage': {'@type': '@id'}, - 'subjectOf': {'@reverse': 'earl:subject'}, - 'earl:assertedBy': {'@type': '@id'}, - 'earl:mode': {'@type': '@id'}, - 'earl:test': {'@type': '@id'}, - 'earl:outcome': {'@type': '@id'}, - 'dc:date': {'@type': 'xsd:date'}, - 'doap:created': {'@type': 'xsd:date'} - }, - '@id': 'https://github.com/digitalbazaar/jsonld.js', - '@type': [ - 'doap:Project', - 'earl:TestSubject', - 'earl:Software' - ], - 'doap:name': 'jsonld.js', - 'dc:title': 'jsonld.js', - 'doap:homepage': 'https://github.com/digitalbazaar/jsonld.js', - 'doap:license': - 'https://github.com/digitalbazaar/jsonld.js/blob/master/LICENSE', - 'doap:description': 'A JSON-LD processor for JavaScript', - 'doap:programming-language': 'JavaScript', - 'dc:creator': 'https://digitalbazaar.com/', - 'doap:developer': { - '@id': 'https://digitalbazaar.com/', +class EarlReport { + /** + * Create an EARL Reporter. + * + * @param options {Object} reporter options + * env: {Object} environment description + */ + constructor(options) { + let today = new Date(); + today = today.getFullYear() + '-' + + (today.getMonth() < 9 ? + '0' + (today.getMonth() + 1) : today.getMonth() + 1) + '-' + + (today.getDate() < 10 ? '0' + today.getDate() : today.getDate()); + // one date for tests with no subsecond resolution + this.now = new Date(); + this.now.setMilliseconds(0); + this.env = options.env; + // test environment + this._environment = null; + /* eslint-disable quote-props */ + this._report = { + '@context': { + 'doap': 'http://usefulinc.com/ns/doap#', + 'foaf': 'http://xmlns.com/foaf/0.1/', + 'dc': 'http://purl.org/dc/terms/', + 'earl': 'http://www.w3.org/ns/earl#', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + 'jsonld': 'http://www.w3.org/ns/json-ld#', + 'doap:homepage': {'@type': '@id'}, + 'doap:license': {'@type': '@id'}, + 'dc:creator': {'@type': '@id'}, + 'foaf:homepage': {'@type': '@id'}, + 'subjectOf': {'@reverse': 'earl:subject'}, + 'earl:assertedBy': {'@type': '@id'}, + 'earl:mode': {'@type': '@id'}, + 'earl:test': {'@type': '@id'}, + 'earl:outcome': {'@type': '@id'}, + 'dc:date': {'@type': 'xsd:date'}, + 'doap:created': {'@type': 'xsd:date'} + }, + '@id': 'https://github.com/digitalbazaar/jsonld.js', '@type': [ - 'foaf:Organization', - 'earl:Assertor' + 'doap:Project', + 'earl:TestSubject', + 'earl:Software' ], - 'foaf:name': 'Digital Bazaar, Inc.', - 'foaf:homepage': 'https://digitalbazaar.com/' - }, - 'doap:release': { - 'doap:revision': '', - 'doap:created': today - }, - 'subjectOf': [] - }; - /* eslint-enable quote-props */ - if(this.env && this.env.version) { - this._report['doap:release']['doap:revision'] = this.env.version; + 'doap:name': 'jsonld.js', + 'dc:title': 'jsonld.js', + 'doap:homepage': 'https://github.com/digitalbazaar/jsonld.js', + 'doap:license': + 'https://github.com/digitalbazaar/jsonld.js/blob/master/LICENSE', + 'doap:description': 'A JSON-LD processor for JavaScript', + 'doap:programming-language': 'JavaScript', + 'dc:creator': 'https://digitalbazaar.com/', + 'doap:developer': { + '@id': 'https://digitalbazaar.com/', + '@type': [ + 'foaf:Organization', + 'earl:Assertor' + ], + 'foaf:name': 'Digital Bazaar, Inc.', + 'foaf:homepage': 'https://digitalbazaar.com/' + }, + 'doap:release': { + 'doap:revision': '', + 'doap:created': today + }, + 'subjectOf': [] + }; + /* eslint-enable quote-props */ + if(this.env && this.env.version) { + this._report['doap:release']['doap:revision'] = this.env.version; + } } -} -EarlReport.prototype.addAssertion = function(test, pass, options) { - options = options || {}; - const assertion = { - '@type': 'earl:Assertion', - 'earl:assertedBy': this._report['doap:developer']['@id'], - 'earl:mode': 'earl:automatic', - 'earl:test': test['@id'], - 'earl:result': { - '@type': 'earl:TestResult', - 'dc:date': this.now.toISOString(), - 'earl:outcome': pass ? 'earl:passed' : 'earl:failed' - } - }; - if(options.benchmarkResult) { - const result = { - ...options.benchmarkResult + addAssertion(test, pass, options) { + options = options || {}; + const assertion = { + '@type': 'earl:Assertion', + 'earl:assertedBy': this._report['doap:developer']['@id'], + 'earl:mode': 'earl:automatic', + 'earl:test': test['@id'], + 'earl:result': { + '@type': 'earl:TestResult', + 'dc:date': this.now.toISOString(), + 'earl:outcome': pass ? 'earl:passed' : 'earl:failed' + } }; - if(this._environment) { - result['jldb:environment'] = this._environment['@id']; + if(options.benchmarkResult) { + const result = { + ...options.benchmarkResult + }; + if(this._environment) { + result['jldb:environment'] = this._environment['@id']; + } + assertion['jldb:result'] = result; } - assertion['jldb:result'] = result; + this._report.subjectOf.push(assertion); + return this; } - this._report.subjectOf.push(assertion); - return this; -}; -EarlReport.prototype.report = function() { - return this._report; -}; + report() { + return this._report; + } -EarlReport.prototype.reportJson = function() { - return JSON.stringify(this._report, null, 2); -}; + reportJson() { + return JSON.stringify(this._report, null, 2); + } + + // setup @context and environment to handle benchmark data + setupForBenchmarks(options) { + // add context if needed + if(!Array.isArray(this._report['@context'])) { + this._report['@context'] = [this._report['@context']]; + } + if(!this._report['@context'].some(c => c === _benchmarkContext)) { + this._report['@context'].push(_benchmarkContext); + } + if(options.testEnv) { + // add report environment + const fields = [ + ['label', 'jldb:label'], + ['arch', 'jldb:arch'], + ['cpu', 'jldb:cpu'], + ['cpuCount', 'jldb:cpuCount'], + ['platform', 'jldb:platform'], + ['runtime', 'jldb:runtime'], + ['runtimeVersion', 'jldb:runtimeVersion'], + ['comment', 'jldb:comment'] + ]; + const _env = { + '@id': '_:environment:0' + }; + for(const [field, property] of fields) { + if(options.testEnv[field]) { + _env[property] = options.testEnv[field]; + } + } + this._environment = _env; + this._report['@included'] = this._report['@included'] || []; + this._report['@included'].push(_env); + } + } +} /* eslint-disable quote-props */ const _benchmarkContext = { @@ -162,39 +200,4 @@ const _benchmarkContext = { }; /* eslint-enable quote-props */ -// setup @context and environment to handle benchmark data -EarlReport.prototype.setupForBenchmarks = function(options) { - // add context if needed - if(!Array.isArray(this._report['@context'])) { - this._report['@context'] = [this._report['@context']]; - } - if(!this._report['@context'].some(c => c === _benchmarkContext)) { - this._report['@context'].push(_benchmarkContext); - } - if(options.testEnv) { - // add report environment - const fields = [ - ['label', 'jldb:label'], - ['arch', 'jldb:arch'], - ['cpu', 'jldb:cpu'], - ['cpuCount', 'jldb:cpuCount'], - ['platform', 'jldb:platform'], - ['runtime', 'jldb:runtime'], - ['runtimeVersion', 'jldb:runtimeVersion'], - ['comment', 'jldb:comment'] - ]; - const _env = { - '@id': '_:environment:0' - }; - for(const [field, property] of fields) { - if(options.testEnv[field]) { - _env[property] = options.testEnv[field]; - } - } - this._environment = _env; - this._report['@included'] = this._report['@included'] || []; - this._report['@included'].push(_env); - } -}; - module.exports = EarlReport; diff --git a/tests/test-common.js b/tests/test-common.js index 616098fe..500fe9ed 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -6,6 +6,7 @@ const EarlReport = require('./earl-report'); const join = require('join-path-js'); const rdfCanonize = require('rdf-canonize'); const {prependBase} = require('../lib/url'); +const {klona} = require('klona'); module.exports = function(options) { @@ -285,7 +286,6 @@ const SKIP_TESTS = []; // create earl report if(options.earl && options.earl.filename) { options.earl.report = new EarlReport({ - id: options.earl.id, env: options.testEnv }); if(options.benchmarkOptions) { @@ -429,16 +429,16 @@ function addManifest(manifest, parent) { * Adds a test. * * @param manifest {Object} the manifest. - * @param parent {Object} the test. + * @param test {Object} the test. * @param tests {Array} the list of tests to add to. * @return {Promise} */ -function addTest(manifest, test, tests) { +async function addTest(manifest, test, tests) { // expand @id and input base const test_id = test['@id'] || test.id; //var number = test_id.substr(2); test['@id'] = - manifest.baseIri + + (manifest.baseIri || '') + basename(manifest.filename).replace('.jsonld', '') + test_id; test.base = manifest.baseIri + test.input; @@ -447,223 +447,242 @@ function addTest(manifest, test, tests) { const _test = { title: description, - f: makeFn() + f: makeFn({ + test, + run: ({test, testInfo, params}) => { + return jsonld[testInfo.fn](...params); + } + }) }; - // only based on test manifest - // skip handled via skip() + // 'only' based on test manifest + // 'skip' handled via skip() if('only' in test) { _test.only = test.only; } tests.push(_test); +} - function makeFn() { - return async function() { - const self = this; - self.timeout(5000); - const testInfo = TEST_TYPES[getJsonLdTestType(test)]; - - // skip based on test manifest - if('skip' in test && test.skip) { - if(options.verboseSkip) { - console.log('Skipping test due to manifest:', - {id: test['@id'], name: test.name}); - } - self.skip(); +function makeFn({ + test, + adjustParams = p => p, + run, + ignoreResult = false +}) { + return async function() { + const self = this; + self.timeout(5000); + const testInfo = TEST_TYPES[getJsonLdTestType(test)]; + + // skip based on test manifest + if('skip' in test && test.skip) { + if(options.verboseSkip) { + console.log('Skipping test due to manifest:', + {id: test['@id'], name: test.name}); } + self.skip(); + } - // skip based on unknown test type - const testTypes = Object.keys(TEST_TYPES); - if(!isJsonLdType(test, testTypes)) { - if(options.verboseSkip) { - const type = [].concat( - getJsonLdValues(test, '@type'), - getJsonLdValues(test, 'type') - ); - console.log('Skipping test due to unknown type:', - {id: test['@id'], name: test.name, type}); - } - self.skip(); + // skip based on unknown test type + const testTypes = Object.keys(TEST_TYPES); + if(!isJsonLdType(test, testTypes)) { + if(options.verboseSkip) { + const type = [].concat( + getJsonLdValues(test, '@type'), + getJsonLdValues(test, 'type') + ); + console.log('Skipping test due to unknown type:', + {id: test['@id'], name: test.name, type}); } + self.skip(); + } - // skip based on test type - if(isJsonLdType(test, SKIP_TESTS)) { - if(options.verboseSkip) { - const type = [].concat( - getJsonLdValues(test, '@type'), - getJsonLdValues(test, 'type') - ); - console.log('Skipping test due to test type:', - {id: test['@id'], name: test.name, type}); - } - self.skip(); + // skip based on test type + if(isJsonLdType(test, SKIP_TESTS)) { + if(options.verboseSkip) { + const type = [].concat( + getJsonLdValues(test, '@type'), + getJsonLdValues(test, 'type') + ); + console.log('Skipping test due to test type:', + {id: test['@id'], name: test.name, type}); } + self.skip(); + } - // skip based on type info - if(testInfo.skip && testInfo.skip.type) { - if(options.verboseSkip) { - console.log('Skipping test due to type info:', - {id: test['@id'], name: test.name}); - } - self.skip(); + // skip based on type info + if(testInfo.skip && testInfo.skip.type) { + if(options.verboseSkip) { + console.log('Skipping test due to type info:', + {id: test['@id'], name: test.name}); } + self.skip(); + } - // skip based on id regex - if(testInfo.skip && testInfo.skip.idRegex) { - testInfo.skip.idRegex.forEach(function(re) { - if(re.test(test['@id'])) { - if(options.verboseSkip) { - console.log('Skipping test due to id:', - {id: test['@id']}); - } - self.skip(); + // skip based on id regex + if(testInfo.skip && testInfo.skip.idRegex) { + testInfo.skip.idRegex.forEach(function(re) { + if(re.test(test['@id'])) { + if(options.verboseSkip) { + console.log('Skipping test due to id:', + {id: test['@id']}); } - }); - } + self.skip(); + } + }); + } - // skip based on description regex - if(testInfo.skip && testInfo.skip.descriptionRegex) { - testInfo.skip.descriptionRegex.forEach(function(re) { - if(re.test(description)) { - if(options.verboseSkip) { - console.log('Skipping test due to description:', - {id: test['@id'], name: test.name, description}); - } - self.skip(); + // skip based on description regex + if(testInfo.skip && testInfo.skip.descriptionRegex) { + testInfo.skip.descriptionRegex.forEach(function(re) { + if(re.test(description)) { + if(options.verboseSkip) { + console.log('Skipping test due to description:', + {id: test['@id'], name: test.name, description}); } - }); - } + self.skip(); + } + }); + } - // Make expandContext absolute to the manifest - if(test.hasOwnProperty('option') && test.option.expandContext) { - test.option.expandContext = - prependBase(test.manifest.baseIri, test.option.expandContext); - } + // Make expandContext absolute to the manifest + if(test.hasOwnProperty('option') && test.option.expandContext) { + test.option.expandContext = + prependBase(test.manifest.baseIri, test.option.expandContext); + } - const testOptions = getJsonLdValues(test, 'option'); - // allow special handling in case of normative test failures - let normativeTest = true; + const testOptions = getJsonLdValues(test, 'option'); + // allow special handling in case of normative test failures + let normativeTest = true; - testOptions.forEach(function(opt) { - const processingModes = getJsonLdValues(opt, 'processingMode'); - processingModes.forEach(function(pm) { - let skipModes = []; - if(testInfo.skip && testInfo.skip.processingMode) { - skipModes = testInfo.skip.processingMode; - } - if(skipModes.indexOf(pm) !== -1) { - if(options.verboseSkip) { - console.log('Skipping test due to processingMode:', - {id: test['@id'], name: test.name, processingMode: pm}); - } - self.skip(); + testOptions.forEach(function(opt) { + const processingModes = getJsonLdValues(opt, 'processingMode'); + processingModes.forEach(function(pm) { + let skipModes = []; + if(testInfo.skip && testInfo.skip.processingMode) { + skipModes = testInfo.skip.processingMode; + } + if(skipModes.indexOf(pm) !== -1) { + if(options.verboseSkip) { + console.log('Skipping test due to processingMode:', + {id: test['@id'], name: test.name, processingMode: pm}); } - }); + self.skip(); + } }); + }); - testOptions.forEach(function(opt) { - const specVersions = getJsonLdValues(opt, 'specVersion'); - specVersions.forEach(function(sv) { - let skipVersions = []; - if(testInfo.skip && testInfo.skip.specVersion) { - skipVersions = testInfo.skip.specVersion; - } - if(skipVersions.indexOf(sv) !== -1) { - if(options.verboseSkip) { - console.log('Skipping test due to specVersion:', - {id: test['@id'], name: test.name, specVersion: sv}); - } - self.skip(); + testOptions.forEach(function(opt) { + const specVersions = getJsonLdValues(opt, 'specVersion'); + specVersions.forEach(function(sv) { + let skipVersions = []; + if(testInfo.skip && testInfo.skip.specVersion) { + skipVersions = testInfo.skip.specVersion; + } + if(skipVersions.indexOf(sv) !== -1) { + if(options.verboseSkip) { + console.log('Skipping test due to specVersion:', + {id: test['@id'], name: test.name, specVersion: sv}); } - }); + self.skip(); + } }); + }); - testOptions.forEach(function(opt) { - const normative = getJsonLdValues(opt, 'normative'); - normative.forEach(function(n) { - normativeTest = normativeTest && n; - }); + testOptions.forEach(function(opt) { + const normative = getJsonLdValues(opt, 'normative'); + normative.forEach(function(n) { + normativeTest = normativeTest && n; }); + }); - const fn = testInfo.fn; - const params = testInfo.params.map(param => param(test)); - // resolve test data - const values = await Promise.all(params); - let err; - let result; - // run and capture errors and results - try { - result = await jsonld[fn].apply(null, values); - } catch(e) { - err = e; - } + const params = adjustParams(testInfo.params.map(param => param(test))); + // resolve test data + const values = await Promise.all(params); + // copy used to check inputs do not change + const valuesOrig = klona(values); + let err; + let result; + // run and capture errors and results + try { + result = await run({test, testInfo, params: values}); + // check input not changed + assert.deepStrictEqual(valuesOrig, values); + } catch(e) { + err = e; + } - try { - if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { + try { + if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { + if(!ignoreResult) { await compareExpectedError(test, err); - } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || - isJsonLdType(test, 'rdfc:Urgna2012EvalTest') || - isJsonLdType(test, 'rdfc:Urdna2015EvalTest')) { - if(err) { - throw err; - } + } + } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || + isJsonLdType(test, 'rdfc:Urgna2012EvalTest') || + isJsonLdType(test, 'rdfc:Urdna2015EvalTest')) { + if(err) { + throw err; + } + if(!ignoreResult) { await testInfo.compare(test, result); - } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) { - // no checks - } else { - throw Error('Unknown test type: ' + test.type); } + } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) { + // no checks + } else { + throw Error('Unknown test type: ' + test.type); + } - let benchmarkResult = null; - if(options.benchmarkOptions) { - const result = await runBenchmark({ - test, - fn, - params: testInfo.params.map(param => param(test, { - // pre-load params to avoid doc loader and parser timing - load: true - })), - mochaTest: self - }); - benchmarkResult = { - '@type': 'jldb:BenchmarkResult', - 'jldb:hz': result.target.hz, - 'jldb:rme': result.target.stats.rme - }; - } + let benchmarkResult = null; + if(options.benchmarkOptions) { + const result = await runBenchmark({ + test, + testInfo, + run, + params: testInfo.params.map(param => param(test, { + // pre-load params to avoid doc loader and parser timing + load: true + })), + mochaTest: self + }); + benchmarkResult = { + // FIXME use generic prefix + '@type': 'jldb:BenchmarkResult', + 'jldb:hz': result.target.hz, + 'jldb:rme': result.target.stats.rme + }; + } - if(options.earl.report) { - options.earl.report.addAssertion(test, true, { - benchmarkResult - }); - } - } catch(err) { - // FIXME: improve handling of non-normative errors - // FIXME: for now, explicitly disabling tests. - //if(!normativeTest) { - // // failure ok - // if(options.verboseSkip) { - // console.log('Skipping non-normative test due to failure:', - // {id: test['@id'], name: test.name}); - // } - // self.skip(); - //} - if(options.bailOnError) { - if(err.name !== 'AssertionError') { - console.error('\nError: ', JSON.stringify(err, null, 2)); - } - options.exit(); - } - if(options.earl.report) { - options.earl.report.addAssertion(test, false); + if(options.earl.report) { + options.earl.report.addAssertion(test, true, { + benchmarkResult + }); + } + } catch(err) { + // FIXME: improve handling of non-normative errors + // FIXME: for now, explicitly disabling tests. + //if(!normativeTest) { + // // failure ok + // if(options.verboseSkip) { + // console.log('Skipping non-normative test due to failure:', + // {id: test['@id'], name: test.name}); + // } + // self.skip(); + //} + if(options.bailOnError) { + if(err.name !== 'AssertionError') { + console.error('\nError: ', JSON.stringify(err, null, 2)); } - console.error('Error: ', JSON.stringify(err, null, 2)); - throw err; + options.exit(); } - }; - } + if(options.earl.report) { + options.earl.report.addAssertion(test, false); + } + console.error('Error: ', JSON.stringify(err, null, 2)); + throw err; + } + }; } -async function runBenchmark({test, fn, params, mochaTest}) { +async function runBenchmark({test, testInfo, params, run, mochaTest}) { const values = await Promise.all(params); return new Promise((resolve, reject) => { @@ -672,7 +691,7 @@ async function runBenchmark({test, fn, params, mochaTest}) { name: test.name, defer: true, fn: deferred => { - jsonld[fn].apply(null, values).then(() => { + run({test, testInfo, params: values}).then(() => { deferred.resolve(); }); } From 80701d8861c12c086e1395af10e7c82ddd8e94e6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 13 Apr 2023 21:39:58 -0400 Subject: [PATCH 136/181] Rename test files. - Rename test.js to test-node.js. Aligns with test-karma.js. - Rename test-common.js to test.js. --- package.json | 2 +- tests/test-common.js | 1113 ------------------------------------- tests/test-karma.js | 2 +- tests/test-node.js | 213 +++++++ tests/test.js | 1266 ++++++++++++++++++++++++++++++++++++------ 5 files changed, 1298 insertions(+), 1298 deletions(-) delete mode 100644 tests/test-common.js create mode 100644 tests/test-node.js diff --git a/package.json b/package.json index 1a7b531e..7a0bd8ee 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "fetch-json-ld-org-test-suite": "if [ ! -e test-suites/json-ld.org ]; then git clone --depth 1 https://github.com/json-ld/json-ld.org.git test-suites/json-ld.org; fi", "fetch-rdf-canon-test-suite": "if [ ! -e test-suites/rdf-canon ]; then git clone --depth 1 https://github.com/w3c/rdf-canon.git test-suites/rdf-canon; fi", "test": "npm run test-node", - "test-node": "cross-env NODE_ENV=test mocha --delay -t 30000 -A -R ${REPORTER:-spec} tests/test.js", + "test-node": "cross-env NODE_ENV=test mocha --delay -t 30000 -A -R ${REPORTER:-spec} tests/test-node.js", "test-karma": "cross-env NODE_ENV=test karma start", "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text-summary npm test", "coverage-ci": "cross-env NODE_ENV=test nyc --reporter=lcovonly npm run test", diff --git a/tests/test-common.js b/tests/test-common.js deleted file mode 100644 index 500fe9ed..00000000 --- a/tests/test-common.js +++ /dev/null @@ -1,1113 +0,0 @@ -/** - * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. - */ -/* eslint-disable indent */ -const EarlReport = require('./earl-report'); -const join = require('join-path-js'); -const rdfCanonize = require('rdf-canonize'); -const {prependBase} = require('../lib/url'); -const {klona} = require('klona'); - -module.exports = function(options) { - -'use strict'; - -const assert = options.assert; -const benchmark = options.benchmark; -const jsonld = options.jsonld; - -const manifest = options.manifest || { - '@context': 'https://json-ld.org/test-suite/context.jsonld', - '@id': '', - '@type': 'mf:Manifest', - description: 'Top level jsonld.js manifest', - name: 'jsonld.js', - sequence: options.entries || [], - filename: '/' -}; - -const TEST_TYPES = { - 'jld:CompactTest': { - skip: { - // skip tests where behavior changed for a 1.1 processor - // see JSON-LD 1.0 Errata - specVersion: ['json-ld-1.0'], - // FIXME - // NOTE: idRegex format: - //MMM-manifest#tNNN$/, - idRegex: [ - /compact-manifest#t0112$/, - /compact-manifest#t0113$/, - // html - /html-manifest#tc001$/, - /html-manifest#tc002$/, - /html-manifest#tc003$/, - /html-manifest#tc004$/, - ] - }, - fn: 'compact', - params: [ - readTestUrl('input'), - readTestJson('context'), - createTestOptions() - ], - compare: compareExpectedJson - }, - 'jld:ExpandTest': { - skip: { - // skip tests where behavior changed for a 1.1 processor - // see JSON-LD 1.0 Errata - specVersion: ['json-ld-1.0'], - // FIXME - // NOTE: idRegex format: - //MMM-manifest#tNNN$/, - idRegex: [ - // spec issues - // Unclear how to handle {"@id": null} edge case - // See https://github.com/w3c/json-ld-api/issues/480 - // non-normative test, also see toRdf-manifest#te122 - ///expand-manifest#t0122$/, - - // misc - /expand-manifest#tc037$/, - /expand-manifest#tc038$/, - /expand-manifest#ter54$/, - - // html - /html-manifest#te001$/, - /html-manifest#te002$/, - /html-manifest#te003$/, - /html-manifest#te004$/, - /html-manifest#te005$/, - /html-manifest#te006$/, - /html-manifest#te007$/, - /html-manifest#te010$/, - /html-manifest#te011$/, - /html-manifest#te012$/, - /html-manifest#te013$/, - /html-manifest#te014$/, - /html-manifest#te015$/, - /html-manifest#te016$/, - /html-manifest#te017$/, - /html-manifest#te018$/, - /html-manifest#te019$/, - /html-manifest#te020$/, - /html-manifest#te021$/, - /html-manifest#te022$/, - /html-manifest#tex01$/, - // HTML extraction - /expand-manifest#thc01$/, - /expand-manifest#thc02$/, - /expand-manifest#thc03$/, - /expand-manifest#thc04$/, - /expand-manifest#thc05$/, - // remote - /remote-doc-manifest#t0013$/, // HTML - ] - }, - fn: 'expand', - params: [ - readTestUrl('input'), - createTestOptions() - ], - compare: compareExpectedJson - }, - 'jld:FlattenTest': { - skip: { - // skip tests where behavior changed for a 1.1 processor - // see JSON-LD 1.0 Errata - specVersion: ['json-ld-1.0'], - // FIXME - // NOTE: idRegex format: - //MMM-manifest#tNNN$/, - idRegex: [ - // html - /html-manifest#tf001$/, - /html-manifest#tf002$/, - /html-manifest#tf003$/, - /html-manifest#tf004$/, - ] - }, - fn: 'flatten', - params: [ - readTestUrl('input'), - readTestJson('context'), - createTestOptions() - ], - compare: compareExpectedJson - }, - 'jld:FrameTest': { - skip: { - // skip tests where behavior changed for a 1.1 processor - // see JSON-LD 1.0 Errata - specVersion: ['json-ld-1.0'], - // FIXME - // NOTE: idRegex format: - //MMM-manifest#tNNN$/, - idRegex: [ - /frame-manifest#t0069$/, - ] - }, - fn: 'frame', - params: [ - readTestUrl('input'), - readTestJson('frame'), - createTestOptions() - ], - compare: compareExpectedJson - }, - 'jld:FromRDFTest': { - skip: { - // skip tests where behavior changed for a 1.1 processor - // see JSON-LD 1.0 Errata - specVersion: ['json-ld-1.0'], - // FIXME - // NOTE: idRegex format: - //MMM-manifest#tNNN$/, - idRegex: [ - // direction (compound-literal) - /fromRdf-manifest#tdi11$/, - /fromRdf-manifest#tdi12$/, - ] - }, - fn: 'fromRDF', - params: [ - readTestNQuads('input'), - createTestOptions({format: 'application/n-quads'}) - ], - compare: compareExpectedJson - }, - 'jld:NormalizeTest': { - fn: 'normalize', - params: [ - readTestUrl('input'), - createTestOptions({format: 'application/n-quads'}) - ], - compare: compareExpectedNQuads - }, - 'jld:ToRDFTest': { - skip: { - // skip tests where behavior changed for a 1.1 processor - // see JSON-LD 1.0 Errata - specVersion: ['json-ld-1.0'], - // FIXME - // NOTE: idRegex format: - //MMM-manifest#tNNN$/, - idRegex: [ - // spec issues - // Unclear how to handle {"@id": null} edge case - // See https://github.com/w3c/json-ld-api/issues/480 - // normative test, also see expand-manifest#t0122 - ///toRdf-manifest#te122$/, - - // misc - /toRdf-manifest#tc037$/, - /toRdf-manifest#tc038$/, - /toRdf-manifest#ter54$/, - /toRdf-manifest#tli12$/, - /toRdf-manifest#tli14$/, - - // well formed - /toRdf-manifest#twf05$/, - - // html - /html-manifest#tr001$/, - /html-manifest#tr002$/, - /html-manifest#tr003$/, - /html-manifest#tr004$/, - /html-manifest#tr005$/, - /html-manifest#tr006$/, - /html-manifest#tr007$/, - /html-manifest#tr010$/, - /html-manifest#tr011$/, - /html-manifest#tr012$/, - /html-manifest#tr013$/, - /html-manifest#tr014$/, - /html-manifest#tr015$/, - /html-manifest#tr016$/, - /html-manifest#tr017$/, - /html-manifest#tr018$/, - /html-manifest#tr019$/, - /html-manifest#tr020$/, - /html-manifest#tr021$/, - /html-manifest#tr022$/, - // Invalid Statement - /toRdf-manifest#te075$/, - /toRdf-manifest#te111$/, - /toRdf-manifest#te112$/, - // direction (compound-literal) - /toRdf-manifest#tdi11$/, - /toRdf-manifest#tdi12$/, - ] - }, - fn: 'toRDF', - params: [ - readTestUrl('input'), - createTestOptions({format: 'application/n-quads'}) - ], - compare: compareCanonizedExpectedNQuads - }, - 'rdfc:Urgna2012EvalTest': { - fn: 'normalize', - params: [ - readTestNQuads('action'), - createTestOptions({ - algorithm: 'URGNA2012', - inputFormat: 'application/n-quads', - format: 'application/n-quads' - }) - ], - compare: compareExpectedNQuads - }, - 'rdfc:Urdna2015EvalTest': { - skip: { - // NOTE: idRegex format: - //manifest-urdna2015#testNNN$/, - idRegex: [ - // Unsupported U escape - /manifest-urdna2015#test060/ - ] - }, - fn: 'normalize', - params: [ - readTestNQuads('action'), - createTestOptions({ - algorithm: 'URDNA2015', - inputFormat: 'application/n-quads', - format: 'application/n-quads' - }) - ], - compare: compareExpectedNQuads - } -}; - -const SKIP_TESTS = []; - -// create earl report -if(options.earl && options.earl.filename) { - options.earl.report = new EarlReport({ - env: options.testEnv - }); - if(options.benchmarkOptions) { - options.earl.report.setupForBenchmarks({testEnv: options.testEnv}); - } -} - -return new Promise(resolve => { - -// async generated tests -// _tests => [{suite}, ...] -// suite => { -// title: ..., -// tests: [test, ...], -// suites: [suite, ...] -// } -const _tests = []; - -return addManifest(manifest, _tests) - .then(() => { - return _testsToMocha(_tests); - }).then(result => { - if(options.earl.report) { - describe('Writing EARL report to: ' + options.earl.filename, function() { - // print out EARL even if .only was used - const _it = result.hadOnly ? it.only : it; - _it('should print the earl report', function() { - return options.writeFile( - options.earl.filename, options.earl.report.reportJson()); - }); - }); - } - }).then(() => resolve()); - -// build mocha tests from local test structure -function _testsToMocha(tests) { - let hadOnly = false; - tests.forEach(suite => { - if(suite.skip) { - describe.skip(suite.title); - return; - } - describe(suite.title, () => { - suite.tests.forEach(test => { - if(test.only) { - hadOnly = true; - it.only(test.title, test.f); - return; - } - it(test.title, test.f); - }); - const {hadOnly: _hadOnly} = _testsToMocha(suite.suites); - hadOnly = hadOnly || _hadOnly; - }); - suite.imports.forEach(f => { - options.import(f); - }); - }); - return { - hadOnly - }; -} - -}); - -/** - * Adds the tests for all entries in the given manifest. - * - * @param manifest {Object} the manifest. - * @param parent {Object} the parent test structure - * @return {Promise} - */ -function addManifest(manifest, parent) { - return new Promise((resolve, reject) => { - // create test structure - const suite = { - title: manifest.name || manifest.label, - tests: [], - suites: [], - imports: [] - }; - parent.push(suite); - - // get entries and sequence (alias for entries) - const entries = [].concat( - getJsonLdValues(manifest, 'entries'), - getJsonLdValues(manifest, 'sequence') - ); - - const includes = getJsonLdValues(manifest, 'include'); - // add includes to sequence as jsonld files - for(let i = 0; i < includes.length; ++i) { - entries.push(includes[i] + '.jsonld'); - } - - // resolve all entry promises and process - Promise.all(entries).then(entries => { - let p = Promise.resolve(); - entries.forEach(entry => { - if(typeof entry === 'string' && entry.endsWith('js')) { - // process later as a plain JavaScript file - suite.imports.push(entry); - return; - } else if(typeof entry === 'function') { - // process as a function that returns a promise - p = p.then(() => { - return entry(options); - }).then(childSuite => { - if(suite) { - suite.suites.push(childSuite); - } - }); - return; - } - p = p.then(() => { - return readManifestEntry(manifest, entry); - }).then(entry => { - if(isJsonLdType(entry, '__SKIP__')) { - // special local skip logic - suite.tests.push(entry); - } else if(isJsonLdType(entry, 'mf:Manifest')) { - // entry is another manifest - return addManifest(entry, suite.suites); - } else { - // assume entry is a test - return addTest(manifest, entry, suite.tests); - } - }); - }); - return p; - }).then(() => { - resolve(); - }).catch(err => { - console.error(err); - reject(err); - }); - }); -} - -/** - * Adds a test. - * - * @param manifest {Object} the manifest. - * @param test {Object} the test. - * @param tests {Array} the list of tests to add to. - * @return {Promise} - */ -async function addTest(manifest, test, tests) { - // expand @id and input base - const test_id = test['@id'] || test.id; - //var number = test_id.substr(2); - test['@id'] = - (manifest.baseIri || '') + - basename(manifest.filename).replace('.jsonld', '') + - test_id; - test.base = manifest.baseIri + test.input; - test.manifest = manifest; - const description = test_id + ' ' + (test.purpose || test.name); - - const _test = { - title: description, - f: makeFn({ - test, - run: ({test, testInfo, params}) => { - return jsonld[testInfo.fn](...params); - } - }) - }; - // 'only' based on test manifest - // 'skip' handled via skip() - if('only' in test) { - _test.only = test.only; - } - tests.push(_test); -} - -function makeFn({ - test, - adjustParams = p => p, - run, - ignoreResult = false -}) { - return async function() { - const self = this; - self.timeout(5000); - const testInfo = TEST_TYPES[getJsonLdTestType(test)]; - - // skip based on test manifest - if('skip' in test && test.skip) { - if(options.verboseSkip) { - console.log('Skipping test due to manifest:', - {id: test['@id'], name: test.name}); - } - self.skip(); - } - - // skip based on unknown test type - const testTypes = Object.keys(TEST_TYPES); - if(!isJsonLdType(test, testTypes)) { - if(options.verboseSkip) { - const type = [].concat( - getJsonLdValues(test, '@type'), - getJsonLdValues(test, 'type') - ); - console.log('Skipping test due to unknown type:', - {id: test['@id'], name: test.name, type}); - } - self.skip(); - } - - // skip based on test type - if(isJsonLdType(test, SKIP_TESTS)) { - if(options.verboseSkip) { - const type = [].concat( - getJsonLdValues(test, '@type'), - getJsonLdValues(test, 'type') - ); - console.log('Skipping test due to test type:', - {id: test['@id'], name: test.name, type}); - } - self.skip(); - } - - // skip based on type info - if(testInfo.skip && testInfo.skip.type) { - if(options.verboseSkip) { - console.log('Skipping test due to type info:', - {id: test['@id'], name: test.name}); - } - self.skip(); - } - - // skip based on id regex - if(testInfo.skip && testInfo.skip.idRegex) { - testInfo.skip.idRegex.forEach(function(re) { - if(re.test(test['@id'])) { - if(options.verboseSkip) { - console.log('Skipping test due to id:', - {id: test['@id']}); - } - self.skip(); - } - }); - } - - // skip based on description regex - if(testInfo.skip && testInfo.skip.descriptionRegex) { - testInfo.skip.descriptionRegex.forEach(function(re) { - if(re.test(description)) { - if(options.verboseSkip) { - console.log('Skipping test due to description:', - {id: test['@id'], name: test.name, description}); - } - self.skip(); - } - }); - } - - // Make expandContext absolute to the manifest - if(test.hasOwnProperty('option') && test.option.expandContext) { - test.option.expandContext = - prependBase(test.manifest.baseIri, test.option.expandContext); - } - - const testOptions = getJsonLdValues(test, 'option'); - // allow special handling in case of normative test failures - let normativeTest = true; - - testOptions.forEach(function(opt) { - const processingModes = getJsonLdValues(opt, 'processingMode'); - processingModes.forEach(function(pm) { - let skipModes = []; - if(testInfo.skip && testInfo.skip.processingMode) { - skipModes = testInfo.skip.processingMode; - } - if(skipModes.indexOf(pm) !== -1) { - if(options.verboseSkip) { - console.log('Skipping test due to processingMode:', - {id: test['@id'], name: test.name, processingMode: pm}); - } - self.skip(); - } - }); - }); - - testOptions.forEach(function(opt) { - const specVersions = getJsonLdValues(opt, 'specVersion'); - specVersions.forEach(function(sv) { - let skipVersions = []; - if(testInfo.skip && testInfo.skip.specVersion) { - skipVersions = testInfo.skip.specVersion; - } - if(skipVersions.indexOf(sv) !== -1) { - if(options.verboseSkip) { - console.log('Skipping test due to specVersion:', - {id: test['@id'], name: test.name, specVersion: sv}); - } - self.skip(); - } - }); - }); - - testOptions.forEach(function(opt) { - const normative = getJsonLdValues(opt, 'normative'); - normative.forEach(function(n) { - normativeTest = normativeTest && n; - }); - }); - - const params = adjustParams(testInfo.params.map(param => param(test))); - // resolve test data - const values = await Promise.all(params); - // copy used to check inputs do not change - const valuesOrig = klona(values); - let err; - let result; - // run and capture errors and results - try { - result = await run({test, testInfo, params: values}); - // check input not changed - assert.deepStrictEqual(valuesOrig, values); - } catch(e) { - err = e; - } - - try { - if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { - if(!ignoreResult) { - await compareExpectedError(test, err); - } - } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || - isJsonLdType(test, 'rdfc:Urgna2012EvalTest') || - isJsonLdType(test, 'rdfc:Urdna2015EvalTest')) { - if(err) { - throw err; - } - if(!ignoreResult) { - await testInfo.compare(test, result); - } - } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) { - // no checks - } else { - throw Error('Unknown test type: ' + test.type); - } - - let benchmarkResult = null; - if(options.benchmarkOptions) { - const result = await runBenchmark({ - test, - testInfo, - run, - params: testInfo.params.map(param => param(test, { - // pre-load params to avoid doc loader and parser timing - load: true - })), - mochaTest: self - }); - benchmarkResult = { - // FIXME use generic prefix - '@type': 'jldb:BenchmarkResult', - 'jldb:hz': result.target.hz, - 'jldb:rme': result.target.stats.rme - }; - } - - if(options.earl.report) { - options.earl.report.addAssertion(test, true, { - benchmarkResult - }); - } - } catch(err) { - // FIXME: improve handling of non-normative errors - // FIXME: for now, explicitly disabling tests. - //if(!normativeTest) { - // // failure ok - // if(options.verboseSkip) { - // console.log('Skipping non-normative test due to failure:', - // {id: test['@id'], name: test.name}); - // } - // self.skip(); - //} - if(options.bailOnError) { - if(err.name !== 'AssertionError') { - console.error('\nError: ', JSON.stringify(err, null, 2)); - } - options.exit(); - } - if(options.earl.report) { - options.earl.report.addAssertion(test, false); - } - console.error('Error: ', JSON.stringify(err, null, 2)); - throw err; - } - }; -} - -async function runBenchmark({test, testInfo, params, run, mochaTest}) { - const values = await Promise.all(params); - - return new Promise((resolve, reject) => { - const suite = new benchmark.Suite(); - suite.add({ - name: test.name, - defer: true, - fn: deferred => { - run({test, testInfo, params: values}).then(() => { - deferred.resolve(); - }); - } - }); - suite - .on('start', e => { - // set timeout to a bit more than max benchmark time - mochaTest.timeout((e.target.maxTime + 2) * 1000); - }) - .on('cycle', e => { - console.log(String(e.target)); - }) - .on('error', err => { - reject(new Error(err)); - }) - .on('complete', e => { - resolve(e); - }) - .run({async: true}); - }); -} - -function getJsonLdTestType(test) { - const types = Object.keys(TEST_TYPES); - for(let i = 0; i < types.length; ++i) { - if(isJsonLdType(test, types[i])) { - return types[i]; - } - } - return null; -} - -function readManifestEntry(manifest, entry) { - let p = Promise.resolve(); - let _entry = entry; - if(typeof entry === 'string') { - let _filename; - p = p.then(() => { - if(entry.endsWith('json') || entry.endsWith('jsonld')) { - // load as file - return entry; - } - // load as dir with manifest.jsonld - return joinPath(entry, 'manifest.jsonld'); - }).then(entry => { - const dir = dirname(manifest.filename); - return joinPath(dir, entry); - }).then(filename => { - _filename = filename; - return readJson(filename); - }).then(entry => { - _entry = entry; - _entry.filename = _filename; - return _entry; - }).catch(err => { - if(err.code === 'ENOENT') { - //console.log('File does not exist, skipping: ' + _filename); - // return a "skip" entry - _entry = { - type: '__SKIP__', - title: 'Not found, skipping: ' + _filename, - filename: _filename, - skip: true - }; - return; - } - throw err; - }); - } - return p.then(() => { - _entry.dirname = dirname(_entry.filename || manifest.filename); - return _entry; - }); -} - -function readTestUrl(property) { - return async function(test, options) { - if(!test[property]) { - return null; - } - if(options && options.load) { - // always load - const filename = await joinPath(test.dirname, test[property]); - return readJson(filename); - } - return test.manifest.baseIri + test[property]; - }; -} - -function readTestJson(property) { - return async function(test) { - if(!test[property]) { - return null; - } - const filename = await joinPath(test.dirname, test[property]); - return readJson(filename); - }; -} - -function readTestNQuads(property) { - return async function(test) { - if(!test[property]) { - return null; - } - const filename = await joinPath(test.dirname, test[property]); - return readFile(filename); - }; -} - -function createTestOptions(opts) { - return function(test) { - const options = { - documentLoader: createDocumentLoader(test) - }; - const httpOptions = ['contentType', 'httpLink', 'httpStatus', 'redirectTo']; - const testOptions = test.option || {}; - for(const key in testOptions) { - if(httpOptions.indexOf(key) === -1) { - options[key] = testOptions[key]; - } - } - if(opts) { - // extend options - for(const key in opts) { - options[key] = opts[key]; - } - } - return options; - }; -} - -// find the expected output property or throw error -function _getExpectProperty(test) { - if('expectErrorCode' in test) { - return 'expectErrorCode'; - } else if('expect' in test) { - return 'expect'; - } else if('result' in test) { - return 'result'; - } else { - throw Error('No expected output property found'); - } -} - -async function compareExpectedJson(test, result) { - let expect; - try { - expect = await readTestJson(_getExpectProperty(test))(test); - assert.deepStrictEqual(result, expect); - } catch(err) { - if(options.bailOnError) { - console.log('\nTEST FAILED\n'); - console.log('EXPECTED: ' + JSON.stringify(expect, null, 2)); - console.log('ACTUAL: ' + JSON.stringify(result, null, 2)); - } - throw err; - } -} - -async function compareExpectedNQuads(test, result) { - let expect; - try { - expect = await readTestNQuads(_getExpectProperty(test))(test); - assert.strictEqual(result, expect); - } catch(ex) { - if(options.bailOnError) { - console.log('\nTEST FAILED\n'); - console.log('EXPECTED:\n' + expect); - console.log('ACTUAL:\n' + result); - } - throw ex; - } -} - -async function compareCanonizedExpectedNQuads(test, result) { - let expect; - try { - expect = await readTestNQuads(_getExpectProperty(test))(test); - const opts = {algorithm: 'URDNA2015'}; - const expectDataset = rdfCanonize.NQuads.parse(expect); - const expectCmp = await rdfCanonize.canonize(expectDataset, opts); - const resultDataset = rdfCanonize.NQuads.parse(result); - const resultCmp = await rdfCanonize.canonize(resultDataset, opts); - assert.strictEqual(resultCmp, expectCmp); - } catch(err) { - if(options.bailOnError) { - console.log('\nTEST FAILED\n'); - console.log('EXPECTED:\n' + expect); - console.log('ACTUAL:\n' + result); - } - throw err; - } -} - -async function compareExpectedError(test, err) { - let expect; - let result; - try { - expect = test[_getExpectProperty(test)]; - result = getJsonLdErrorCode(err); - assert.ok(err, 'no error present'); - assert.strictEqual(result, expect); - } catch(_err) { - if(options.bailOnError) { - console.log('\nTEST FAILED\n'); - console.log('EXPECTED: ' + expect); - console.log('ACTUAL: ' + result); - } - // log the unexpected error to help with debugging - console.log('Unexpected error:', err); - throw _err; - } -} - -function isJsonLdType(node, type) { - const nodeType = [].concat( - getJsonLdValues(node, '@type'), - getJsonLdValues(node, 'type') - ); - type = Array.isArray(type) ? type : [type]; - for(let i = 0; i < type.length; ++i) { - if(nodeType.indexOf(type[i]) !== -1) { - return true; - } - } - return false; -} - -function getJsonLdValues(node, property) { - let rval = []; - if(property in node) { - rval = node[property]; - if(!Array.isArray(rval)) { - rval = [rval]; - } - } - return rval; -} - -function getJsonLdErrorCode(err) { - if(!err) { - return null; - } - if(err.details) { - if(err.details.code) { - return err.details.code; - } - if(err.details.cause) { - return getJsonLdErrorCode(err.details.cause); - } - } - return err.name; -} - -async function readJson(filename) { - const data = await readFile(filename); - return JSON.parse(data); -} - -async function readFile(filename) { - return options.readFile(filename); -} - -async function joinPath() { - return join.apply(null, Array.prototype.slice.call(arguments)); -} - -function dirname(filename) { - if(options.nodejs) { - return options.nodejs.path.dirname(filename); - } - const idx = filename.lastIndexOf('/'); - if(idx === -1) { - return filename; - } - return filename.substr(0, idx); -} - -function basename(filename) { - if(options.nodejs) { - return options.nodejs.path.basename(filename); - } - const idx = filename.lastIndexOf('/'); - if(idx === -1) { - return filename; - } - return filename.substr(idx + 1); -} - -// check test.option.loader.rewrite map for url, -// if no test rewrite, check manifest, -// else no rewrite -function rewrite(test, url) { - if(test.option && - test.option.loader && - test.option.loader.rewrite && - url in test.option.loader.rewrite) { - return test.option.loader.rewrite[url]; - } - const manifest = test.manifest; - if(manifest.option && - manifest.option.loader && - manifest.option.loader.rewrite && - url in manifest.option.loader.rewrite) { - return manifest.option.loader.rewrite[url]; - } - return url; -} - -/** - * Creates a test remote document loader. - * - * @param test the test to use the document loader for. - * - * @return the document loader. - */ -function createDocumentLoader(test) { - const localBases = [ - 'http://json-ld.org/test-suite', - 'https://json-ld.org/test-suite', - 'https://json-ld.org/benchmarks', - 'https://w3c.github.io/json-ld-api/tests', - 'https://w3c.github.io/json-ld-framing/tests' - ]; - - const localLoader = function(url) { - // always load remote-doc tests remotely in node - // NOTE: disabled due to github pages issues. - //if(options.nodejs && test.manifest.name === 'Remote document') { - // return jsonld.documentLoader(url); - //} - - // handle loader rewrite options for test or manifest - url = rewrite(test, url); - - // FIXME: this check only works for main test suite and will not work if: - // - running other tests and main test suite not installed - // - use other absolute URIs but want to load local files - const isTestSuite = localBases.some(function(base) { - return url.startsWith(base); - }); - // TODO: improve this check - const isRelative = url.indexOf(':') === -1; - if(isTestSuite || isRelative) { - // attempt to load official test-suite files or relative URLs locally - return loadLocally(url); - } - - // load remotely - return jsonld.documentLoader(url); - }; - - return localLoader; - - function loadLocally(url) { - const doc = {contextUrl: null, documentUrl: url, document: null}; - const options = test.option; - if(options && url === test.base) { - if('redirectTo' in options && parseInt(options.httpStatus, 10) >= 300) { - doc.documentUrl = test.manifest.baseIri + options.redirectTo; - } else if('httpLink' in options) { - let contentType = options.contentType || null; - if(!contentType && url.indexOf('.jsonld', url.length - 7) !== -1) { - contentType = 'application/ld+json'; - } - if(!contentType && url.indexOf('.json', url.length - 5) !== -1) { - contentType = 'application/json'; - } - let linkHeader = options.httpLink; - if(Array.isArray(linkHeader)) { - linkHeader = linkHeader.join(','); - } - const linkHeaders = jsonld.parseLinkHeader(linkHeader); - const linkedContext = - linkHeaders['http://www.w3.org/ns/json-ld#context']; - if(linkedContext && contentType !== 'application/ld+json') { - if(Array.isArray(linkedContext)) { - throw {name: 'multiple context link headers'}; - } - doc.contextUrl = linkedContext.target; - } - - // If not JSON-LD, alternate may point there - if(linkHeaders.alternate && - linkHeaders.alternate.type == 'application/ld+json' && - !(contentType || '').match(/^application\/(\w*\+)?json$/)) { - doc.documentUrl = prependBase(url, linkHeaders.alternate.target); - } - } - } - - let p = Promise.resolve(); - if(doc.documentUrl.indexOf(':') === -1) { - p = p.then(() => { - return joinPath(test.manifest.dirname, doc.documentUrl); - }).then(filename => { - doc.documentUrl = 'file://' + filename; - return filename; - }); - } else { - p = p.then(() => { - return joinPath( - test.manifest.dirname, - doc.documentUrl.substr(test.manifest.baseIri.length)); - }).then(fn => { - return fn; - }); - } - - return p.then(readJson).then(json => { - doc.document = json; - return doc; - }).catch(() => { - throw {name: 'loading document failed', url}; - }); - } -} - -}; diff --git a/tests/test-karma.js b/tests/test-karma.js index 33910732..5d04e8ec 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -46,7 +46,7 @@ mocha.setup({delay: true, ui: 'bdd'}); const assert = require('chai').assert; -const common = require('./test-common'); +const common = require('./test'); const jsonld = require('..'); const server = require('karma-server-side'); const webidl = require('./test-webidl'); diff --git a/tests/test-node.js b/tests/test-node.js new file mode 100644 index 00000000..c9001aa8 --- /dev/null +++ b/tests/test-node.js @@ -0,0 +1,213 @@ +/** + * Node.js test runner for jsonld.js. + * + * Use environment vars to control: + * + * Set dirs, manifests, or js to run: + * JSONLD_TESTS="r1 r2 ..." + * Output an EARL report: + * EARL=filename + * Test environment details for EARL report: + * This is useful for benchmark comparison. + * By default no details are added for privacy reasons. + * Automatic details can be added for all fields with '1', 'true', or 'auto': + * TEST_ENV=1 + * To include only certain fields, set them, or use 'auto': + * TEST_ENV=cpu='Intel i7-4790K @ 4.00GHz',runtime='Node.js',... + * TEST_ENV=cpu=auto # only cpu + * TEST_ENV=cpu,runtime # only cpu and runtime + * TEST_ENV=auto,comment='special test' # all auto with override + * Available fields: + * - label - ex: 'Setup 1' (short label for reports) + * - arch - ex: 'x64' + * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' + * - cpuCount - ex: 8 + * - platform - ex: 'linux' + * - runtime - ex: 'Node.js' + * - runtimeVersion - ex: 'v14.19.0' + * - comment: any text + * - version: jsonld.js version + * Bail with tests fail: + * BAIL=true + * Verbose skip reasons: + * VERBOSE_SKIP=true + * Benchmark mode: + * Basic: + * JSONLD_BENCHMARK=1 + * With options: + * JSONLD_BENCHMARK=key1=value1,key2=value2,... + * + * @author Dave Longley + * @author David I. Lehn + * + * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. + */ +const assert = require('chai').assert; +const benchmark = require('benchmark'); +const common = require('./test'); +const fs = require('fs-extra'); +const jsonld = require('..'); +const os = require('os'); +const path = require('path'); + +const entries = []; + +if(process.env.JSONLD_TESTS) { + entries.push(...process.env.JSONLD_TESTS.split(' ')); +} else { + const _top = path.resolve(__dirname, '..'); + + // json-ld-api main test suite + const apiPath = path.resolve(_top, 'test-suites/json-ld-api/tests'); + if(fs.existsSync(apiPath)) { + entries.push(apiPath); + } else { + // default to sibling dir + entries.push(path.resolve(_top, '../json-ld-api/tests')); + } + + // json-ld-framing main test suite + const framingPath = path.resolve(_top, 'test-suites/json-ld-framing/tests'); + if(fs.existsSync(framingPath)) { + entries.push(framingPath); + } else { + // default to sibling dir + entries.push(path.resolve(_top, '../json-ld-framing/tests')); + } + + /* + // TODO: use json-ld-framing once tests are moved + // json-ld.org framing test suite + const framingPath = path.resolve( + _top, 'test-suites/json-ld.org/test-suite/tests/frame-manifest.jsonld'); + if(fs.existsSync(framingPath)) { + entries.push(framingPath); + } else { + // default to sibling dir + entries.push(path.resolve( + _top, '../json-ld.org/test-suite/tests/frame-manifest.jsonld')); + } + */ + + // W3C RDF Dataset Canonicalization "rdf-canon" test suite + const rdfCanonPath = path.resolve(_top, 'test-suites/rdf-canon/tests'); + if(fs.existsSync(rdfCanonPath)) { + entries.push(rdfCanonPath); + } else { + // default up to sibling dir + entries.push(path.resolve(_top, '../rdf-canon/tests')); + } + + // other tests + entries.push(path.resolve(_top, 'tests/misc.js')); + entries.push(path.resolve(_top, 'tests/graph-container.js')); + entries.push(path.resolve(_top, 'tests/node-document-loader-tests.js')); +} + +// test environment +let testEnv = null; +if(process.env.TEST_ENV) { + let _test_env = process.env.TEST_ENV; + if(!(['0', 'false'].includes(_test_env))) { + testEnv = {}; + if(['1', 'true', 'auto'].includes(_test_env)) { + _test_env = 'auto'; + } + _test_env.split(',').forEach(pair => { + if(pair === 'auto') { + testEnv.name = 'auto'; + testEnv.arch = 'auto'; + testEnv.cpu = 'auto'; + testEnv.cpuCount = 'auto'; + testEnv.platform = 'auto'; + testEnv.runtime = 'auto'; + testEnv.runtimeVersion = 'auto'; + testEnv.comment = 'auto'; + testEnv.version = 'auto'; + } else { + const kv = pair.split('='); + if(kv.length === 1) { + testEnv[kv[0]] = 'auto'; + } else { + testEnv[kv[0]] = kv.slice(1).join('='); + } + } + }); + if(testEnv.label === 'auto') { + testEnv.label = ''; + } + if(testEnv.arch === 'auto') { + testEnv.arch = process.arch; + } + if(testEnv.cpu === 'auto') { + testEnv.cpu = os.cpus()[0].model; + } + if(testEnv.cpuCount === 'auto') { + testEnv.cpuCount = os.cpus().length; + } + if(testEnv.platform === 'auto') { + testEnv.platform = process.platform; + } + if(testEnv.runtime === 'auto') { + testEnv.runtime = 'Node.js'; + } + if(testEnv.runtimeVersion === 'auto') { + testEnv.runtimeVersion = process.version; + } + if(testEnv.comment === 'auto') { + testEnv.comment = ''; + } + if(testEnv.version === 'auto') { + testEnv.version = require('../package.json').version; + } + } +} + +let benchmarkOptions = null; +if(process.env.JSONLD_BENCHMARK) { + if(!(['0', 'false'].includes(process.env.JSONLD_BENCHMARK))) { + benchmarkOptions = {}; + if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { + process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { + const kv = pair.split('='); + benchmarkOptions[kv[0]] = kv[1]; + }); + } + } +} + +const options = { + nodejs: { + path + }, + assert, + benchmark, + jsonld, + exit: code => process.exit(code), + earl: { + filename: process.env.EARL + }, + verboseSkip: process.env.VERBOSE_SKIP === 'true', + bailOnError: process.env.BAIL === 'true', + entries, + testEnv, + benchmarkOptions, + readFile: filename => { + return fs.readFile(filename, 'utf8'); + }, + writeFile: (filename, data) => { + return fs.outputFile(filename, data); + }, + import: f => require(f) +}; + +// wait for setup of all tests then run mocha +common(options).then(() => { + run(); +}).catch(err => { + console.error(err); +}); + +process.on('unhandledRejection', (reason, p) => { + console.error('Unhandled Rejection at:', p, 'reason:', reason); +}); diff --git a/tests/test.js b/tests/test.js index 074161d2..500fe9ed 100644 --- a/tests/test.js +++ b/tests/test.js @@ -1,213 +1,1113 @@ /** - * Node.js test runner for jsonld.js. - * - * Use environment vars to control: - * - * Set dirs, manifests, or js to run: - * JSONLD_TESTS="r1 r2 ..." - * Output an EARL report: - * EARL=filename - * Test environment details for EARL report: - * This is useful for benchmark comparison. - * By default no details are added for privacy reasons. - * Automatic details can be added for all fields with '1', 'true', or 'auto': - * TEST_ENV=1 - * To include only certain fields, set them, or use 'auto': - * TEST_ENV=cpu='Intel i7-4790K @ 4.00GHz',runtime='Node.js',... - * TEST_ENV=cpu=auto # only cpu - * TEST_ENV=cpu,runtime # only cpu and runtime - * TEST_ENV=auto,comment='special test' # all auto with override - * Available fields: - * - label - ex: 'Setup 1' (short label for reports) - * - arch - ex: 'x64' - * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' - * - cpuCount - ex: 8 - * - platform - ex: 'linux' - * - runtime - ex: 'Node.js' - * - runtimeVersion - ex: 'v14.19.0' - * - comment: any text - * - version: jsonld.js version - * Bail with tests fail: - * BAIL=true - * Verbose skip reasons: - * VERBOSE_SKIP=true - * Benchmark mode: - * Basic: - * JSONLD_BENCHMARK=1 - * With options: - * JSONLD_BENCHMARK=key1=value1,key2=value2,... - * - * @author Dave Longley - * @author David I. Lehn - * - * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. */ -const assert = require('chai').assert; -const benchmark = require('benchmark'); -const common = require('./test-common'); -const fs = require('fs-extra'); -const jsonld = require('..'); -const os = require('os'); -const path = require('path'); - -const entries = []; - -if(process.env.JSONLD_TESTS) { - entries.push(...process.env.JSONLD_TESTS.split(' ')); -} else { - const _top = path.resolve(__dirname, '..'); - - // json-ld-api main test suite - const apiPath = path.resolve(_top, 'test-suites/json-ld-api/tests'); - if(fs.existsSync(apiPath)) { - entries.push(apiPath); - } else { - // default to sibling dir - entries.push(path.resolve(_top, '../json-ld-api/tests')); +/* eslint-disable indent */ +const EarlReport = require('./earl-report'); +const join = require('join-path-js'); +const rdfCanonize = require('rdf-canonize'); +const {prependBase} = require('../lib/url'); +const {klona} = require('klona'); + +module.exports = function(options) { + +'use strict'; + +const assert = options.assert; +const benchmark = options.benchmark; +const jsonld = options.jsonld; + +const manifest = options.manifest || { + '@context': 'https://json-ld.org/test-suite/context.jsonld', + '@id': '', + '@type': 'mf:Manifest', + description: 'Top level jsonld.js manifest', + name: 'jsonld.js', + sequence: options.entries || [], + filename: '/' +}; + +const TEST_TYPES = { + 'jld:CompactTest': { + skip: { + // skip tests where behavior changed for a 1.1 processor + // see JSON-LD 1.0 Errata + specVersion: ['json-ld-1.0'], + // FIXME + // NOTE: idRegex format: + //MMM-manifest#tNNN$/, + idRegex: [ + /compact-manifest#t0112$/, + /compact-manifest#t0113$/, + // html + /html-manifest#tc001$/, + /html-manifest#tc002$/, + /html-manifest#tc003$/, + /html-manifest#tc004$/, + ] + }, + fn: 'compact', + params: [ + readTestUrl('input'), + readTestJson('context'), + createTestOptions() + ], + compare: compareExpectedJson + }, + 'jld:ExpandTest': { + skip: { + // skip tests where behavior changed for a 1.1 processor + // see JSON-LD 1.0 Errata + specVersion: ['json-ld-1.0'], + // FIXME + // NOTE: idRegex format: + //MMM-manifest#tNNN$/, + idRegex: [ + // spec issues + // Unclear how to handle {"@id": null} edge case + // See https://github.com/w3c/json-ld-api/issues/480 + // non-normative test, also see toRdf-manifest#te122 + ///expand-manifest#t0122$/, + + // misc + /expand-manifest#tc037$/, + /expand-manifest#tc038$/, + /expand-manifest#ter54$/, + + // html + /html-manifest#te001$/, + /html-manifest#te002$/, + /html-manifest#te003$/, + /html-manifest#te004$/, + /html-manifest#te005$/, + /html-manifest#te006$/, + /html-manifest#te007$/, + /html-manifest#te010$/, + /html-manifest#te011$/, + /html-manifest#te012$/, + /html-manifest#te013$/, + /html-manifest#te014$/, + /html-manifest#te015$/, + /html-manifest#te016$/, + /html-manifest#te017$/, + /html-manifest#te018$/, + /html-manifest#te019$/, + /html-manifest#te020$/, + /html-manifest#te021$/, + /html-manifest#te022$/, + /html-manifest#tex01$/, + // HTML extraction + /expand-manifest#thc01$/, + /expand-manifest#thc02$/, + /expand-manifest#thc03$/, + /expand-manifest#thc04$/, + /expand-manifest#thc05$/, + // remote + /remote-doc-manifest#t0013$/, // HTML + ] + }, + fn: 'expand', + params: [ + readTestUrl('input'), + createTestOptions() + ], + compare: compareExpectedJson + }, + 'jld:FlattenTest': { + skip: { + // skip tests where behavior changed for a 1.1 processor + // see JSON-LD 1.0 Errata + specVersion: ['json-ld-1.0'], + // FIXME + // NOTE: idRegex format: + //MMM-manifest#tNNN$/, + idRegex: [ + // html + /html-manifest#tf001$/, + /html-manifest#tf002$/, + /html-manifest#tf003$/, + /html-manifest#tf004$/, + ] + }, + fn: 'flatten', + params: [ + readTestUrl('input'), + readTestJson('context'), + createTestOptions() + ], + compare: compareExpectedJson + }, + 'jld:FrameTest': { + skip: { + // skip tests where behavior changed for a 1.1 processor + // see JSON-LD 1.0 Errata + specVersion: ['json-ld-1.0'], + // FIXME + // NOTE: idRegex format: + //MMM-manifest#tNNN$/, + idRegex: [ + /frame-manifest#t0069$/, + ] + }, + fn: 'frame', + params: [ + readTestUrl('input'), + readTestJson('frame'), + createTestOptions() + ], + compare: compareExpectedJson + }, + 'jld:FromRDFTest': { + skip: { + // skip tests where behavior changed for a 1.1 processor + // see JSON-LD 1.0 Errata + specVersion: ['json-ld-1.0'], + // FIXME + // NOTE: idRegex format: + //MMM-manifest#tNNN$/, + idRegex: [ + // direction (compound-literal) + /fromRdf-manifest#tdi11$/, + /fromRdf-manifest#tdi12$/, + ] + }, + fn: 'fromRDF', + params: [ + readTestNQuads('input'), + createTestOptions({format: 'application/n-quads'}) + ], + compare: compareExpectedJson + }, + 'jld:NormalizeTest': { + fn: 'normalize', + params: [ + readTestUrl('input'), + createTestOptions({format: 'application/n-quads'}) + ], + compare: compareExpectedNQuads + }, + 'jld:ToRDFTest': { + skip: { + // skip tests where behavior changed for a 1.1 processor + // see JSON-LD 1.0 Errata + specVersion: ['json-ld-1.0'], + // FIXME + // NOTE: idRegex format: + //MMM-manifest#tNNN$/, + idRegex: [ + // spec issues + // Unclear how to handle {"@id": null} edge case + // See https://github.com/w3c/json-ld-api/issues/480 + // normative test, also see expand-manifest#t0122 + ///toRdf-manifest#te122$/, + + // misc + /toRdf-manifest#tc037$/, + /toRdf-manifest#tc038$/, + /toRdf-manifest#ter54$/, + /toRdf-manifest#tli12$/, + /toRdf-manifest#tli14$/, + + // well formed + /toRdf-manifest#twf05$/, + + // html + /html-manifest#tr001$/, + /html-manifest#tr002$/, + /html-manifest#tr003$/, + /html-manifest#tr004$/, + /html-manifest#tr005$/, + /html-manifest#tr006$/, + /html-manifest#tr007$/, + /html-manifest#tr010$/, + /html-manifest#tr011$/, + /html-manifest#tr012$/, + /html-manifest#tr013$/, + /html-manifest#tr014$/, + /html-manifest#tr015$/, + /html-manifest#tr016$/, + /html-manifest#tr017$/, + /html-manifest#tr018$/, + /html-manifest#tr019$/, + /html-manifest#tr020$/, + /html-manifest#tr021$/, + /html-manifest#tr022$/, + // Invalid Statement + /toRdf-manifest#te075$/, + /toRdf-manifest#te111$/, + /toRdf-manifest#te112$/, + // direction (compound-literal) + /toRdf-manifest#tdi11$/, + /toRdf-manifest#tdi12$/, + ] + }, + fn: 'toRDF', + params: [ + readTestUrl('input'), + createTestOptions({format: 'application/n-quads'}) + ], + compare: compareCanonizedExpectedNQuads + }, + 'rdfc:Urgna2012EvalTest': { + fn: 'normalize', + params: [ + readTestNQuads('action'), + createTestOptions({ + algorithm: 'URGNA2012', + inputFormat: 'application/n-quads', + format: 'application/n-quads' + }) + ], + compare: compareExpectedNQuads + }, + 'rdfc:Urdna2015EvalTest': { + skip: { + // NOTE: idRegex format: + //manifest-urdna2015#testNNN$/, + idRegex: [ + // Unsupported U escape + /manifest-urdna2015#test060/ + ] + }, + fn: 'normalize', + params: [ + readTestNQuads('action'), + createTestOptions({ + algorithm: 'URDNA2015', + inputFormat: 'application/n-quads', + format: 'application/n-quads' + }) + ], + compare: compareExpectedNQuads } +}; - // json-ld-framing main test suite - const framingPath = path.resolve(_top, 'test-suites/json-ld-framing/tests'); - if(fs.existsSync(framingPath)) { - entries.push(framingPath); - } else { - // default to sibling dir - entries.push(path.resolve(_top, '../json-ld-framing/tests')); +const SKIP_TESTS = []; + +// create earl report +if(options.earl && options.earl.filename) { + options.earl.report = new EarlReport({ + env: options.testEnv + }); + if(options.benchmarkOptions) { + options.earl.report.setupForBenchmarks({testEnv: options.testEnv}); } +} - /* - // TODO: use json-ld-framing once tests are moved - // json-ld.org framing test suite - const framingPath = path.resolve( - _top, 'test-suites/json-ld.org/test-suite/tests/frame-manifest.jsonld'); - if(fs.existsSync(framingPath)) { - entries.push(framingPath); - } else { - // default to sibling dir - entries.push(path.resolve( - _top, '../json-ld.org/test-suite/tests/frame-manifest.jsonld')); +return new Promise(resolve => { + +// async generated tests +// _tests => [{suite}, ...] +// suite => { +// title: ..., +// tests: [test, ...], +// suites: [suite, ...] +// } +const _tests = []; + +return addManifest(manifest, _tests) + .then(() => { + return _testsToMocha(_tests); + }).then(result => { + if(options.earl.report) { + describe('Writing EARL report to: ' + options.earl.filename, function() { + // print out EARL even if .only was used + const _it = result.hadOnly ? it.only : it; + _it('should print the earl report', function() { + return options.writeFile( + options.earl.filename, options.earl.report.reportJson()); + }); + }); + } + }).then(() => resolve()); + +// build mocha tests from local test structure +function _testsToMocha(tests) { + let hadOnly = false; + tests.forEach(suite => { + if(suite.skip) { + describe.skip(suite.title); + return; + } + describe(suite.title, () => { + suite.tests.forEach(test => { + if(test.only) { + hadOnly = true; + it.only(test.title, test.f); + return; + } + it(test.title, test.f); + }); + const {hadOnly: _hadOnly} = _testsToMocha(suite.suites); + hadOnly = hadOnly || _hadOnly; + }); + suite.imports.forEach(f => { + options.import(f); + }); + }); + return { + hadOnly + }; +} + +}); + +/** + * Adds the tests for all entries in the given manifest. + * + * @param manifest {Object} the manifest. + * @param parent {Object} the parent test structure + * @return {Promise} + */ +function addManifest(manifest, parent) { + return new Promise((resolve, reject) => { + // create test structure + const suite = { + title: manifest.name || manifest.label, + tests: [], + suites: [], + imports: [] + }; + parent.push(suite); + + // get entries and sequence (alias for entries) + const entries = [].concat( + getJsonLdValues(manifest, 'entries'), + getJsonLdValues(manifest, 'sequence') + ); + + const includes = getJsonLdValues(manifest, 'include'); + // add includes to sequence as jsonld files + for(let i = 0; i < includes.length; ++i) { + entries.push(includes[i] + '.jsonld'); + } + + // resolve all entry promises and process + Promise.all(entries).then(entries => { + let p = Promise.resolve(); + entries.forEach(entry => { + if(typeof entry === 'string' && entry.endsWith('js')) { + // process later as a plain JavaScript file + suite.imports.push(entry); + return; + } else if(typeof entry === 'function') { + // process as a function that returns a promise + p = p.then(() => { + return entry(options); + }).then(childSuite => { + if(suite) { + suite.suites.push(childSuite); + } + }); + return; + } + p = p.then(() => { + return readManifestEntry(manifest, entry); + }).then(entry => { + if(isJsonLdType(entry, '__SKIP__')) { + // special local skip logic + suite.tests.push(entry); + } else if(isJsonLdType(entry, 'mf:Manifest')) { + // entry is another manifest + return addManifest(entry, suite.suites); + } else { + // assume entry is a test + return addTest(manifest, entry, suite.tests); + } + }); + }); + return p; + }).then(() => { + resolve(); + }).catch(err => { + console.error(err); + reject(err); + }); + }); +} + +/** + * Adds a test. + * + * @param manifest {Object} the manifest. + * @param test {Object} the test. + * @param tests {Array} the list of tests to add to. + * @return {Promise} + */ +async function addTest(manifest, test, tests) { + // expand @id and input base + const test_id = test['@id'] || test.id; + //var number = test_id.substr(2); + test['@id'] = + (manifest.baseIri || '') + + basename(manifest.filename).replace('.jsonld', '') + + test_id; + test.base = manifest.baseIri + test.input; + test.manifest = manifest; + const description = test_id + ' ' + (test.purpose || test.name); + + const _test = { + title: description, + f: makeFn({ + test, + run: ({test, testInfo, params}) => { + return jsonld[testInfo.fn](...params); + } + }) + }; + // 'only' based on test manifest + // 'skip' handled via skip() + if('only' in test) { + _test.only = test.only; } - */ + tests.push(_test); +} - // W3C RDF Dataset Canonicalization "rdf-canon" test suite - const rdfCanonPath = path.resolve(_top, 'test-suites/rdf-canon/tests'); - if(fs.existsSync(rdfCanonPath)) { - entries.push(rdfCanonPath); - } else { - // default up to sibling dir - entries.push(path.resolve(_top, '../rdf-canon/tests')); - } - - // other tests - entries.push(path.resolve(_top, 'tests/misc.js')); - entries.push(path.resolve(_top, 'tests/graph-container.js')); - entries.push(path.resolve(_top, 'tests/node-document-loader-tests.js')); -} - -// test environment -let testEnv = null; -if(process.env.TEST_ENV) { - let _test_env = process.env.TEST_ENV; - if(!(['0', 'false'].includes(_test_env))) { - testEnv = {}; - if(['1', 'true', 'auto'].includes(_test_env)) { - _test_env = 'auto'; - } - _test_env.split(',').forEach(pair => { - if(pair === 'auto') { - testEnv.name = 'auto'; - testEnv.arch = 'auto'; - testEnv.cpu = 'auto'; - testEnv.cpuCount = 'auto'; - testEnv.platform = 'auto'; - testEnv.runtime = 'auto'; - testEnv.runtimeVersion = 'auto'; - testEnv.comment = 'auto'; - testEnv.version = 'auto'; +function makeFn({ + test, + adjustParams = p => p, + run, + ignoreResult = false +}) { + return async function() { + const self = this; + self.timeout(5000); + const testInfo = TEST_TYPES[getJsonLdTestType(test)]; + + // skip based on test manifest + if('skip' in test && test.skip) { + if(options.verboseSkip) { + console.log('Skipping test due to manifest:', + {id: test['@id'], name: test.name}); + } + self.skip(); + } + + // skip based on unknown test type + const testTypes = Object.keys(TEST_TYPES); + if(!isJsonLdType(test, testTypes)) { + if(options.verboseSkip) { + const type = [].concat( + getJsonLdValues(test, '@type'), + getJsonLdValues(test, 'type') + ); + console.log('Skipping test due to unknown type:', + {id: test['@id'], name: test.name, type}); + } + self.skip(); + } + + // skip based on test type + if(isJsonLdType(test, SKIP_TESTS)) { + if(options.verboseSkip) { + const type = [].concat( + getJsonLdValues(test, '@type'), + getJsonLdValues(test, 'type') + ); + console.log('Skipping test due to test type:', + {id: test['@id'], name: test.name, type}); + } + self.skip(); + } + + // skip based on type info + if(testInfo.skip && testInfo.skip.type) { + if(options.verboseSkip) { + console.log('Skipping test due to type info:', + {id: test['@id'], name: test.name}); + } + self.skip(); + } + + // skip based on id regex + if(testInfo.skip && testInfo.skip.idRegex) { + testInfo.skip.idRegex.forEach(function(re) { + if(re.test(test['@id'])) { + if(options.verboseSkip) { + console.log('Skipping test due to id:', + {id: test['@id']}); + } + self.skip(); + } + }); + } + + // skip based on description regex + if(testInfo.skip && testInfo.skip.descriptionRegex) { + testInfo.skip.descriptionRegex.forEach(function(re) { + if(re.test(description)) { + if(options.verboseSkip) { + console.log('Skipping test due to description:', + {id: test['@id'], name: test.name, description}); + } + self.skip(); + } + }); + } + + // Make expandContext absolute to the manifest + if(test.hasOwnProperty('option') && test.option.expandContext) { + test.option.expandContext = + prependBase(test.manifest.baseIri, test.option.expandContext); + } + + const testOptions = getJsonLdValues(test, 'option'); + // allow special handling in case of normative test failures + let normativeTest = true; + + testOptions.forEach(function(opt) { + const processingModes = getJsonLdValues(opt, 'processingMode'); + processingModes.forEach(function(pm) { + let skipModes = []; + if(testInfo.skip && testInfo.skip.processingMode) { + skipModes = testInfo.skip.processingMode; + } + if(skipModes.indexOf(pm) !== -1) { + if(options.verboseSkip) { + console.log('Skipping test due to processingMode:', + {id: test['@id'], name: test.name, processingMode: pm}); + } + self.skip(); + } + }); + }); + + testOptions.forEach(function(opt) { + const specVersions = getJsonLdValues(opt, 'specVersion'); + specVersions.forEach(function(sv) { + let skipVersions = []; + if(testInfo.skip && testInfo.skip.specVersion) { + skipVersions = testInfo.skip.specVersion; + } + if(skipVersions.indexOf(sv) !== -1) { + if(options.verboseSkip) { + console.log('Skipping test due to specVersion:', + {id: test['@id'], name: test.name, specVersion: sv}); + } + self.skip(); + } + }); + }); + + testOptions.forEach(function(opt) { + const normative = getJsonLdValues(opt, 'normative'); + normative.forEach(function(n) { + normativeTest = normativeTest && n; + }); + }); + + const params = adjustParams(testInfo.params.map(param => param(test))); + // resolve test data + const values = await Promise.all(params); + // copy used to check inputs do not change + const valuesOrig = klona(values); + let err; + let result; + // run and capture errors and results + try { + result = await run({test, testInfo, params: values}); + // check input not changed + assert.deepStrictEqual(valuesOrig, values); + } catch(e) { + err = e; + } + + try { + if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { + if(!ignoreResult) { + await compareExpectedError(test, err); + } + } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || + isJsonLdType(test, 'rdfc:Urgna2012EvalTest') || + isJsonLdType(test, 'rdfc:Urdna2015EvalTest')) { + if(err) { + throw err; + } + if(!ignoreResult) { + await testInfo.compare(test, result); + } + } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) { + // no checks } else { - const kv = pair.split('='); - if(kv.length === 1) { - testEnv[kv[0]] = 'auto'; - } else { - testEnv[kv[0]] = kv.slice(1).join('='); + throw Error('Unknown test type: ' + test.type); + } + + let benchmarkResult = null; + if(options.benchmarkOptions) { + const result = await runBenchmark({ + test, + testInfo, + run, + params: testInfo.params.map(param => param(test, { + // pre-load params to avoid doc loader and parser timing + load: true + })), + mochaTest: self + }); + benchmarkResult = { + // FIXME use generic prefix + '@type': 'jldb:BenchmarkResult', + 'jldb:hz': result.target.hz, + 'jldb:rme': result.target.stats.rme + }; + } + + if(options.earl.report) { + options.earl.report.addAssertion(test, true, { + benchmarkResult + }); + } + } catch(err) { + // FIXME: improve handling of non-normative errors + // FIXME: for now, explicitly disabling tests. + //if(!normativeTest) { + // // failure ok + // if(options.verboseSkip) { + // console.log('Skipping non-normative test due to failure:', + // {id: test['@id'], name: test.name}); + // } + // self.skip(); + //} + if(options.bailOnError) { + if(err.name !== 'AssertionError') { + console.error('\nError: ', JSON.stringify(err, null, 2)); } + options.exit(); + } + if(options.earl.report) { + options.earl.report.addAssertion(test, false); + } + console.error('Error: ', JSON.stringify(err, null, 2)); + throw err; + } + }; +} + +async function runBenchmark({test, testInfo, params, run, mochaTest}) { + const values = await Promise.all(params); + + return new Promise((resolve, reject) => { + const suite = new benchmark.Suite(); + suite.add({ + name: test.name, + defer: true, + fn: deferred => { + run({test, testInfo, params: values}).then(() => { + deferred.resolve(); + }); } }); - if(testEnv.label === 'auto') { - testEnv.label = ''; + suite + .on('start', e => { + // set timeout to a bit more than max benchmark time + mochaTest.timeout((e.target.maxTime + 2) * 1000); + }) + .on('cycle', e => { + console.log(String(e.target)); + }) + .on('error', err => { + reject(new Error(err)); + }) + .on('complete', e => { + resolve(e); + }) + .run({async: true}); + }); +} + +function getJsonLdTestType(test) { + const types = Object.keys(TEST_TYPES); + for(let i = 0; i < types.length; ++i) { + if(isJsonLdType(test, types[i])) { + return types[i]; } - if(testEnv.arch === 'auto') { - testEnv.arch = process.arch; + } + return null; +} + +function readManifestEntry(manifest, entry) { + let p = Promise.resolve(); + let _entry = entry; + if(typeof entry === 'string') { + let _filename; + p = p.then(() => { + if(entry.endsWith('json') || entry.endsWith('jsonld')) { + // load as file + return entry; + } + // load as dir with manifest.jsonld + return joinPath(entry, 'manifest.jsonld'); + }).then(entry => { + const dir = dirname(manifest.filename); + return joinPath(dir, entry); + }).then(filename => { + _filename = filename; + return readJson(filename); + }).then(entry => { + _entry = entry; + _entry.filename = _filename; + return _entry; + }).catch(err => { + if(err.code === 'ENOENT') { + //console.log('File does not exist, skipping: ' + _filename); + // return a "skip" entry + _entry = { + type: '__SKIP__', + title: 'Not found, skipping: ' + _filename, + filename: _filename, + skip: true + }; + return; + } + throw err; + }); + } + return p.then(() => { + _entry.dirname = dirname(_entry.filename || manifest.filename); + return _entry; + }); +} + +function readTestUrl(property) { + return async function(test, options) { + if(!test[property]) { + return null; } - if(testEnv.cpu === 'auto') { - testEnv.cpu = os.cpus()[0].model; + if(options && options.load) { + // always load + const filename = await joinPath(test.dirname, test[property]); + return readJson(filename); } - if(testEnv.cpuCount === 'auto') { - testEnv.cpuCount = os.cpus().length; + return test.manifest.baseIri + test[property]; + }; +} + +function readTestJson(property) { + return async function(test) { + if(!test[property]) { + return null; } - if(testEnv.platform === 'auto') { - testEnv.platform = process.platform; + const filename = await joinPath(test.dirname, test[property]); + return readJson(filename); + }; +} + +function readTestNQuads(property) { + return async function(test) { + if(!test[property]) { + return null; + } + const filename = await joinPath(test.dirname, test[property]); + return readFile(filename); + }; +} + +function createTestOptions(opts) { + return function(test) { + const options = { + documentLoader: createDocumentLoader(test) + }; + const httpOptions = ['contentType', 'httpLink', 'httpStatus', 'redirectTo']; + const testOptions = test.option || {}; + for(const key in testOptions) { + if(httpOptions.indexOf(key) === -1) { + options[key] = testOptions[key]; + } } - if(testEnv.runtime === 'auto') { - testEnv.runtime = 'Node.js'; + if(opts) { + // extend options + for(const key in opts) { + options[key] = opts[key]; + } } - if(testEnv.runtimeVersion === 'auto') { - testEnv.runtimeVersion = process.version; + return options; + }; +} + +// find the expected output property or throw error +function _getExpectProperty(test) { + if('expectErrorCode' in test) { + return 'expectErrorCode'; + } else if('expect' in test) { + return 'expect'; + } else if('result' in test) { + return 'result'; + } else { + throw Error('No expected output property found'); + } +} + +async function compareExpectedJson(test, result) { + let expect; + try { + expect = await readTestJson(_getExpectProperty(test))(test); + assert.deepStrictEqual(result, expect); + } catch(err) { + if(options.bailOnError) { + console.log('\nTEST FAILED\n'); + console.log('EXPECTED: ' + JSON.stringify(expect, null, 2)); + console.log('ACTUAL: ' + JSON.stringify(result, null, 2)); } - if(testEnv.comment === 'auto') { - testEnv.comment = ''; + throw err; + } +} + +async function compareExpectedNQuads(test, result) { + let expect; + try { + expect = await readTestNQuads(_getExpectProperty(test))(test); + assert.strictEqual(result, expect); + } catch(ex) { + if(options.bailOnError) { + console.log('\nTEST FAILED\n'); + console.log('EXPECTED:\n' + expect); + console.log('ACTUAL:\n' + result); } - if(testEnv.version === 'auto') { - testEnv.version = require('../package.json').version; + throw ex; + } +} + +async function compareCanonizedExpectedNQuads(test, result) { + let expect; + try { + expect = await readTestNQuads(_getExpectProperty(test))(test); + const opts = {algorithm: 'URDNA2015'}; + const expectDataset = rdfCanonize.NQuads.parse(expect); + const expectCmp = await rdfCanonize.canonize(expectDataset, opts); + const resultDataset = rdfCanonize.NQuads.parse(result); + const resultCmp = await rdfCanonize.canonize(resultDataset, opts); + assert.strictEqual(resultCmp, expectCmp); + } catch(err) { + if(options.bailOnError) { + console.log('\nTEST FAILED\n'); + console.log('EXPECTED:\n' + expect); + console.log('ACTUAL:\n' + result); } + throw err; } } -let benchmarkOptions = null; -if(process.env.JSONLD_BENCHMARK) { - if(!(['0', 'false'].includes(process.env.JSONLD_BENCHMARK))) { - benchmarkOptions = {}; - if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { - process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { - const kv = pair.split('='); - benchmarkOptions[kv[0]] = kv[1]; - }); +async function compareExpectedError(test, err) { + let expect; + let result; + try { + expect = test[_getExpectProperty(test)]; + result = getJsonLdErrorCode(err); + assert.ok(err, 'no error present'); + assert.strictEqual(result, expect); + } catch(_err) { + if(options.bailOnError) { + console.log('\nTEST FAILED\n'); + console.log('EXPECTED: ' + expect); + console.log('ACTUAL: ' + result); } + // log the unexpected error to help with debugging + console.log('Unexpected error:', err); + throw _err; } } -const options = { - nodejs: { - path - }, - assert, - benchmark, - jsonld, - exit: code => process.exit(code), - earl: { - filename: process.env.EARL - }, - verboseSkip: process.env.VERBOSE_SKIP === 'true', - bailOnError: process.env.BAIL === 'true', - entries, - testEnv, - benchmarkOptions, - readFile: filename => { - return fs.readFile(filename, 'utf8'); - }, - writeFile: (filename, data) => { - return fs.outputFile(filename, data); - }, - import: f => require(f) -}; +function isJsonLdType(node, type) { + const nodeType = [].concat( + getJsonLdValues(node, '@type'), + getJsonLdValues(node, 'type') + ); + type = Array.isArray(type) ? type : [type]; + for(let i = 0; i < type.length; ++i) { + if(nodeType.indexOf(type[i]) !== -1) { + return true; + } + } + return false; +} -// wait for setup of all tests then run mocha -common(options).then(() => { - run(); -}).catch(err => { - console.error(err); -}); +function getJsonLdValues(node, property) { + let rval = []; + if(property in node) { + rval = node[property]; + if(!Array.isArray(rval)) { + rval = [rval]; + } + } + return rval; +} -process.on('unhandledRejection', (reason, p) => { - console.error('Unhandled Rejection at:', p, 'reason:', reason); -}); +function getJsonLdErrorCode(err) { + if(!err) { + return null; + } + if(err.details) { + if(err.details.code) { + return err.details.code; + } + if(err.details.cause) { + return getJsonLdErrorCode(err.details.cause); + } + } + return err.name; +} + +async function readJson(filename) { + const data = await readFile(filename); + return JSON.parse(data); +} + +async function readFile(filename) { + return options.readFile(filename); +} + +async function joinPath() { + return join.apply(null, Array.prototype.slice.call(arguments)); +} + +function dirname(filename) { + if(options.nodejs) { + return options.nodejs.path.dirname(filename); + } + const idx = filename.lastIndexOf('/'); + if(idx === -1) { + return filename; + } + return filename.substr(0, idx); +} + +function basename(filename) { + if(options.nodejs) { + return options.nodejs.path.basename(filename); + } + const idx = filename.lastIndexOf('/'); + if(idx === -1) { + return filename; + } + return filename.substr(idx + 1); +} + +// check test.option.loader.rewrite map for url, +// if no test rewrite, check manifest, +// else no rewrite +function rewrite(test, url) { + if(test.option && + test.option.loader && + test.option.loader.rewrite && + url in test.option.loader.rewrite) { + return test.option.loader.rewrite[url]; + } + const manifest = test.manifest; + if(manifest.option && + manifest.option.loader && + manifest.option.loader.rewrite && + url in manifest.option.loader.rewrite) { + return manifest.option.loader.rewrite[url]; + } + return url; +} + +/** + * Creates a test remote document loader. + * + * @param test the test to use the document loader for. + * + * @return the document loader. + */ +function createDocumentLoader(test) { + const localBases = [ + 'http://json-ld.org/test-suite', + 'https://json-ld.org/test-suite', + 'https://json-ld.org/benchmarks', + 'https://w3c.github.io/json-ld-api/tests', + 'https://w3c.github.io/json-ld-framing/tests' + ]; + + const localLoader = function(url) { + // always load remote-doc tests remotely in node + // NOTE: disabled due to github pages issues. + //if(options.nodejs && test.manifest.name === 'Remote document') { + // return jsonld.documentLoader(url); + //} + + // handle loader rewrite options for test or manifest + url = rewrite(test, url); + + // FIXME: this check only works for main test suite and will not work if: + // - running other tests and main test suite not installed + // - use other absolute URIs but want to load local files + const isTestSuite = localBases.some(function(base) { + return url.startsWith(base); + }); + // TODO: improve this check + const isRelative = url.indexOf(':') === -1; + if(isTestSuite || isRelative) { + // attempt to load official test-suite files or relative URLs locally + return loadLocally(url); + } + + // load remotely + return jsonld.documentLoader(url); + }; + + return localLoader; + + function loadLocally(url) { + const doc = {contextUrl: null, documentUrl: url, document: null}; + const options = test.option; + if(options && url === test.base) { + if('redirectTo' in options && parseInt(options.httpStatus, 10) >= 300) { + doc.documentUrl = test.manifest.baseIri + options.redirectTo; + } else if('httpLink' in options) { + let contentType = options.contentType || null; + if(!contentType && url.indexOf('.jsonld', url.length - 7) !== -1) { + contentType = 'application/ld+json'; + } + if(!contentType && url.indexOf('.json', url.length - 5) !== -1) { + contentType = 'application/json'; + } + let linkHeader = options.httpLink; + if(Array.isArray(linkHeader)) { + linkHeader = linkHeader.join(','); + } + const linkHeaders = jsonld.parseLinkHeader(linkHeader); + const linkedContext = + linkHeaders['http://www.w3.org/ns/json-ld#context']; + if(linkedContext && contentType !== 'application/ld+json') { + if(Array.isArray(linkedContext)) { + throw {name: 'multiple context link headers'}; + } + doc.contextUrl = linkedContext.target; + } + + // If not JSON-LD, alternate may point there + if(linkHeaders.alternate && + linkHeaders.alternate.type == 'application/ld+json' && + !(contentType || '').match(/^application\/(\w*\+)?json$/)) { + doc.documentUrl = prependBase(url, linkHeaders.alternate.target); + } + } + } + + let p = Promise.resolve(); + if(doc.documentUrl.indexOf(':') === -1) { + p = p.then(() => { + return joinPath(test.manifest.dirname, doc.documentUrl); + }).then(filename => { + doc.documentUrl = 'file://' + filename; + return filename; + }); + } else { + p = p.then(() => { + return joinPath( + test.manifest.dirname, + doc.documentUrl.substr(test.manifest.baseIri.length)); + }).then(fn => { + return fn; + }); + } + + return p.then(readJson).then(json => { + doc.document = json; + return doc; + }).catch(() => { + throw {name: 'loading document failed', url}; + }); + } +} + +}; From 2b5aa2d3737884301df95f9fd9e1c79bab58b2b8 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 18 Apr 2023 19:53:30 -0400 Subject: [PATCH 137/181] Align test and benchmark code with rdf-canonize. - Align test and benchmark code with rdf-canonize. - **NOTE**: This changes various testing and benchmark runner features and options. - Update env var usage. - Use more common code between Node.js and karma tests. - Conditionally load test suites. - Fix various minor bugs. - Add multiple jobs benchmarking support. --- CHANGELOG.md | 10 +- README.md | 16 +-- karma.conf.js | 20 ++-- tests/test-karma.js | 187 ++++++++++------------------- tests/test-node.js | 142 ++++------------------ tests/test.js | 284 +++++++++++++++++++++++++++++++++++--------- 6 files changed, 345 insertions(+), 314 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c039a057..0947b8cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.1.2 - 2023-03-xx +## 8.2.0 - 2023-03-xx ### Changed - Update for latest [rdf-canon][] changes: test suite location, README, links, @@ -8,6 +8,14 @@ - Skip test with 'U' escapes. Will enable when [rdf-canonize][] dependency is updated. - Test on Node.js 20.x. +- Align test and benchmark code with [rdf-canonize][]. + - **NOTE**: This changes various testing and benchmark runner features and + options. + - Update env var usage. + - Use more common code between Node.js and karma tests. + - Conditionally load test suites. + - Fix various minor bugs. + - Add multiple jobs benchmarking support. ### Fixed - Improve safe mode for `@graph` use cases. diff --git a/README.md b/README.md index 6d6db7c6..98299837 100644 --- a/README.md +++ b/README.md @@ -389,13 +389,13 @@ Node.js tests can be run with a simple command: npm test If you installed the test suites elsewhere, or wish to run other tests, use -the `JSONLD_TESTS` environment var: +the `TESTS` environment var: - JSONLD_TESTS="/tmp/org/test-suites /tmp/norm/tests" npm test + TESTS="/tmp/org/test-suites /tmp/norm/tests" npm test This feature can be used to run the older json-ld.org test suite: - JSONLD_TESTS=/tmp/json-ld.org/test-suite npm test + TESTS=/tmp/json-ld.org/test-suite npm test Browser testing can be done with Karma: @@ -419,7 +419,7 @@ Remote context tests are also available: # run the context server in the background or another terminal node tests/remote-context-server.js - JSONLD_TESTS=`pwd`/tests npm test + TESTS=`pwd`/tests npm test To generate EARL reports: @@ -432,7 +432,7 @@ To generate EARL reports: To generate an EARL report with the `json-ld-api` and `json-ld-framing` tests as used on the official [JSON-LD Processor Conformance][] page - JSONLD_TESTS="`pwd`/../json-ld-api/tests `pwd`/../json-ld-framing/tests" EARL="jsonld-js-earl.jsonld" npm test + TESTS="`pwd`/../json-ld-api/tests `pwd`/../json-ld-framing/tests" EARL="jsonld-js-earl.jsonld" npm test The EARL `.jsonld` output can be converted to `.ttl` using the [rdf][] tool: @@ -449,14 +449,14 @@ Benchmarks Benchmarks can be created from any manifest that the test system supports. Use a command line with a test suite and a benchmark flag: - JSONLD_TESTS=/tmp/benchmark-manifest.jsonld JSONLD_BENCHMARK=1 npm test + TESTS=/tmp/benchmark-manifest.jsonld BENCHMARK=1 npm test EARL reports with benchmark data can be generated with an optional environment details: - JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test + TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test -See `tests/test.js` for more `TEST_ENV` control and options. +See `tests/test.js` for more `TEST_ENV` and `BENCHMARK` control and options. These reports can be compared with the `benchmarks/compare/` tool and at the [JSON-LD Benchmarks][] site. diff --git a/karma.conf.js b/karma.conf.js index bd8d5c4f..81a6ce96 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,17 +1,12 @@ /** - * Karam configuration for jsonld.js. + * Karma configuration for jsonld.js. * - * Set dirs, manifests, or js to run: - * JSONLD_TESTS="f1 f2 ..." - * Output an EARL report: - * EARL=filename - * Bail with tests fail: - * BAIL=true + * See ./test/test.js for env options. * * @author Dave Longley * @author David I. Lehn * - * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2011-2023 Digital Bazaar, Inc. All rights reserved. */ const os = require('os'); const webpack = require('webpack'); @@ -67,11 +62,10 @@ module.exports = function(config) { plugins: [ new webpack.DefinePlugin({ 'process.env.BAIL': JSON.stringify(process.env.BAIL), + 'process.env.BENCHMARK': JSON.stringify(process.env.BENCHMARK), 'process.env.EARL': JSON.stringify(process.env.EARL), + 'process.env.TESTS': JSON.stringify(process.env.TESTS), 'process.env.TEST_ENV': JSON.stringify(process.env.TEST_ENV), - 'process.env.JSONLD_BENCHMARK': - JSON.stringify(process.env.JSONLD_BENCHMARK), - 'process.env.JSONLD_TESTS': JSON.stringify(process.env.JSONLD_TESTS), 'process.env.TEST_ROOT_DIR': JSON.stringify(__dirname), 'process.env.VERBOSE_SKIP': JSON.stringify(process.env.VERBOSE_SKIP), // for 'auto' test env @@ -149,10 +143,10 @@ module.exports = function(config) { [ 'envify', { BAIL: process.env.BAIL, + BENCHMARK: process.env.BENCHMARK, EARL: process.env.EARL, + TESTS: process.env.TESTS, TEST_ENV: process.env.TEST_ENV, - JSONLD_BENCHMARK: process.env.JSONLD_BENCHMARK, - JSONLD_TESTS: process.env.JSONLD_TESTS, TEST_ROOT_DIR: __dirname, VERBOSE_SKIP: process.env.VERBOSE_SKIP, // for 'auto' test env diff --git a/tests/test-karma.js b/tests/test-karma.js index 5d04e8ec..e15cb2a4 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -1,82 +1,66 @@ /** * Karma test runner for jsonld.js. * - * Use environment vars to control, set via karma.conf.js/webpack: - * - * Set dirs, manifests, or js to run: - * JSONLD_TESTS="r1 r2 ..." - * Output an EARL report: - * EARL=filename - * Test environment details for EARL report: - * This is useful for benchmark comparison. - * By default no details are added for privacy reasons. - * Automatic details can be added for all fields with '1', 'true', or 'auto': - * TEST_ENV=1 - * To include only certain fields, set them, or use 'auto': - * TEST_ENV=cpu='Intel i7-4790K @ 4.00GHz',runtime='Node.js',... - * TEST_ENV=cpu=auto # only cpu - * TEST_ENV=cpu,runtime # only cpu and runtime - * TEST_ENV=auto,comment='special test' # all auto with override - * Available fields: - * - arch - ex: 'x64' - * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' - * - cpuCount - ex: 8 - * - platform - ex: 'linux' - * - runtime - ex: 'Node.js' - * - runtimeVersion - ex: 'v14.19.0' - * - comment: any text - * - version: jsonld.js version - * Bail with tests fail: - * BAIL=true - * Verbose skip reasons: - * VERBOSE_SKIP=true - * Benchmark mode: - * Basic: - * JSONLD_BENCHMARK=1 - * With options: - * JSONLD_BENCHMARK=key1=value1,key2=value2,... + * See ./test.js for environment vars options. * * @author Dave Longley * @author David I. Lehn * - * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2011-2023 Digital Bazaar, Inc. All rights reserved. */ /* global serverRequire */ // FIXME: hack to ensure delay is set first mocha.setup({delay: true, ui: 'bdd'}); const assert = require('chai').assert; -const common = require('./test'); -const jsonld = require('..'); +const benchmark = require('benchmark'); +const common = require('./test.js'); const server = require('karma-server-side'); const webidl = require('./test-webidl'); const join = require('join-path-js'); // special benchmark setup const _ = require('lodash'); -//const _process = require('process'); -const benchmark = require('benchmark'); -//const Benchmark = benchmark.runInContext({_, _process}); const Benchmark = benchmark.runInContext({_}); window.Benchmark = Benchmark; const entries = []; -if(process.env.JSONLD_TESTS) { - entries.push(...process.env.JSONLD_TESTS.split(' ')); +if(process.env.TESTS) { + entries.push(...process.env.TESTS.split(' ')); } else { const _top = process.env.TEST_ROOT_DIR; // TODO: support just adding certain entries in EARL mode? // json-ld-api main test suite - // FIXME: add path detection - entries.push(join(_top, 'test-suites/json-ld-api/tests')); - entries.push(join(_top, '../json-ld-api/tests')); + entries.push((async () => { + const testPath = join(_top, 'test-suites/json-ld-api/tests'); + const siblingPath = join(_top, '../json-ld-api/tests'); + return server.run(testPath, siblingPath, function(testPath, siblingPath) { + const fs = serverRequire('fs-extra'); + // use local tests if setup + if(fs.existsSync(testPath)) { + return testPath; + } + // default to sibling dir + return siblingPath; + }); + })()); // json-ld-framing main test suite - // FIXME: add path detection - entries.push(join(_top, 'test-suites/json-ld-framing/tests')); - entries.push(join(_top, '../json-ld-framing/tests')); + entries.push((async () => { + const testPath = join(_top, 'test-suites/json-ld-framing/tests'); + const siblingPath = join(_top, '../json-ld-framing/tests'); + return server.run(testPath, siblingPath, function(testPath, siblingPath) { + const fs = serverRequire('fs-extra'); + // use local tests if setup + if(fs.existsSync(testPath)) { + return testPath; + } + // default to sibling dir + return siblingPath; + }); + })()); /* // TODO: use json-ld-framing once tests are moved @@ -89,9 +73,19 @@ if(process.env.JSONLD_TESTS) { */ // W3C RDF Dataset Canonicalization "rdf-canon" test suite - // FIXME: add path detection - entries.push(join(_top, 'test-suites/rdf-canon/tests')); - entries.push(join(_top, '../rdf-canon/tests')); + entries.push((async () => { + const testPath = join(_top, 'test-suites/rdf-canon/tests'); + const siblingPath = join(_top, '../rdf-canon/tests'); + return server.run(testPath, siblingPath, function(testPath, siblingPath) { + const fs = serverRequire('fs-extra'); + // use local tests if setup + if(fs.existsSync(testPath)) { + return testPath; + } + // default to sibling dir + return siblingPath; + }); + })()); // other tests entries.push(join(_top, 'tests/misc.js')); @@ -102,79 +96,31 @@ if(process.env.JSONLD_TESTS) { entries.push(webidl); } -// test environment -let testEnv = null; -if(process.env.TEST_ENV) { - let _test_env = process.env.TEST_ENV; - if(!(['0', 'false'].includes(_test_env))) { - testEnv = {}; - if(['1', 'true', 'auto'].includes(_test_env)) { - _test_env = 'auto'; - } - _test_env.split(',').forEach(pair => { - if(pair === 'auto') { - testEnv.arch = 'auto'; - testEnv.cpu = 'auto'; - testEnv.cpuCount = 'auto'; - testEnv.platform = 'auto'; - testEnv.runtime = 'auto'; - testEnv.runtimeVersion = 'auto'; - testEnv.comment = 'auto'; - testEnv.version = 'auto'; - } else { - const kv = pair.split('='); - if(kv.length === 1) { - testEnv[kv[0]] = 'auto'; - } else { - testEnv[kv[0]] = kv.slice(1).join('='); - } - } - }); - if(testEnv.arch === 'auto') { - testEnv.arch = process.env._TEST_ENV_ARCH; - } - if(testEnv.cpu === 'auto') { - testEnv.cpu = process.env._TEST_ENV_CPU; - } - if(testEnv.cpuCount === 'auto') { - testEnv.cpuCount = process.env._TEST_ENV_CPU_COUNT; - } - if(testEnv.platform === 'auto') { - testEnv.platform = process.env._TEST_ENV_PLATFORM; - } - if(testEnv.runtime === 'auto') { - testEnv.runtime = 'browser'; - } - if(testEnv.runtimeVersion === 'auto') { - testEnv.runtimeVersion = '(unknown)'; - } - if(testEnv.comment === 'auto') { - testEnv.comment = ''; - } - if(testEnv.version === 'auto') { - testEnv.version = require('../package.json').version; - } - } -} +// test environment defaults +const testEnvDefaults = { + label: '', + arch: process.env._TEST_ENV_ARCH, + cpu: process.env._TEST_ENV_CPU, + cpuCount: process.env._TEST_ENV_CPU_COUNT, + platform: process.env._TEST_ENV_PLATFORM, + runtime: 'browser', + runtimeVersion: '(unknown)', + comment: '', + version: require('../package.json').version +}; -let benchmarkOptions = null; -if(process.env.JSONLD_BENCHMARK) { - if(!(['0', 'false'].includes(process.env.JSONLD_BENCHMARK))) { - benchmarkOptions = {}; - if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { - process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { - const kv = pair.split('='); - benchmarkOptions[kv[0]] = kv[1]; - }); - } - } -} +const env = { + BAIL: process.env.BAIL, + BENCHMARK: process.env.BENCHMARK, + TEST_ENV: process.env.TEST_ENV, + VERBOSE_SKIP: process.env.VERBOSE_SKIP +}; const options = { + env, nodejs: false, assert, benchmark, - jsonld, /* eslint-disable-next-line no-unused-vars */ exit: code => { console.error('exit not implemented'); @@ -183,11 +129,8 @@ const options = { earl: { filename: process.env.EARL }, - verboseSkip: process.env.VERBOSE_SKIP === 'true', - bailOnError: process.env.BAIL === 'true', entries, - testEnv, - benchmarkOptions, + testEnvDefaults, readFile: filename => { return server.run(filename, function(filename) { const fs = serverRequire('fs-extra'); diff --git a/tests/test-node.js b/tests/test-node.js index c9001aa8..09ca54d3 100644 --- a/tests/test-node.js +++ b/tests/test-node.js @@ -1,59 +1,24 @@ /** * Node.js test runner for jsonld.js. * - * Use environment vars to control: - * - * Set dirs, manifests, or js to run: - * JSONLD_TESTS="r1 r2 ..." - * Output an EARL report: - * EARL=filename - * Test environment details for EARL report: - * This is useful for benchmark comparison. - * By default no details are added for privacy reasons. - * Automatic details can be added for all fields with '1', 'true', or 'auto': - * TEST_ENV=1 - * To include only certain fields, set them, or use 'auto': - * TEST_ENV=cpu='Intel i7-4790K @ 4.00GHz',runtime='Node.js',... - * TEST_ENV=cpu=auto # only cpu - * TEST_ENV=cpu,runtime # only cpu and runtime - * TEST_ENV=auto,comment='special test' # all auto with override - * Available fields: - * - label - ex: 'Setup 1' (short label for reports) - * - arch - ex: 'x64' - * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' - * - cpuCount - ex: 8 - * - platform - ex: 'linux' - * - runtime - ex: 'Node.js' - * - runtimeVersion - ex: 'v14.19.0' - * - comment: any text - * - version: jsonld.js version - * Bail with tests fail: - * BAIL=true - * Verbose skip reasons: - * VERBOSE_SKIP=true - * Benchmark mode: - * Basic: - * JSONLD_BENCHMARK=1 - * With options: - * JSONLD_BENCHMARK=key1=value1,key2=value2,... + * See ./test.js for environment vars options. * * @author Dave Longley * @author David I. Lehn * - * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2011-2023 Digital Bazaar, Inc. All rights reserved. */ const assert = require('chai').assert; const benchmark = require('benchmark'); -const common = require('./test'); +const common = require('./test.js'); const fs = require('fs-extra'); -const jsonld = require('..'); const os = require('os'); const path = require('path'); const entries = []; -if(process.env.JSONLD_TESTS) { - entries.push(...process.env.JSONLD_TESTS.split(' ')); +if(process.env.TESTS) { + entries.push(...process.env.TESTS.split(' ')); } else { const _top = path.resolve(__dirname, '..'); @@ -94,7 +59,7 @@ if(process.env.JSONLD_TESTS) { if(fs.existsSync(rdfCanonPath)) { entries.push(rdfCanonPath); } else { - // default up to sibling dir + // default to sibling dir entries.push(path.resolve(_top, '../rdf-canon/tests')); } @@ -104,94 +69,39 @@ if(process.env.JSONLD_TESTS) { entries.push(path.resolve(_top, 'tests/node-document-loader-tests.js')); } -// test environment -let testEnv = null; -if(process.env.TEST_ENV) { - let _test_env = process.env.TEST_ENV; - if(!(['0', 'false'].includes(_test_env))) { - testEnv = {}; - if(['1', 'true', 'auto'].includes(_test_env)) { - _test_env = 'auto'; - } - _test_env.split(',').forEach(pair => { - if(pair === 'auto') { - testEnv.name = 'auto'; - testEnv.arch = 'auto'; - testEnv.cpu = 'auto'; - testEnv.cpuCount = 'auto'; - testEnv.platform = 'auto'; - testEnv.runtime = 'auto'; - testEnv.runtimeVersion = 'auto'; - testEnv.comment = 'auto'; - testEnv.version = 'auto'; - } else { - const kv = pair.split('='); - if(kv.length === 1) { - testEnv[kv[0]] = 'auto'; - } else { - testEnv[kv[0]] = kv.slice(1).join('='); - } - } - }); - if(testEnv.label === 'auto') { - testEnv.label = ''; - } - if(testEnv.arch === 'auto') { - testEnv.arch = process.arch; - } - if(testEnv.cpu === 'auto') { - testEnv.cpu = os.cpus()[0].model; - } - if(testEnv.cpuCount === 'auto') { - testEnv.cpuCount = os.cpus().length; - } - if(testEnv.platform === 'auto') { - testEnv.platform = process.platform; - } - if(testEnv.runtime === 'auto') { - testEnv.runtime = 'Node.js'; - } - if(testEnv.runtimeVersion === 'auto') { - testEnv.runtimeVersion = process.version; - } - if(testEnv.comment === 'auto') { - testEnv.comment = ''; - } - if(testEnv.version === 'auto') { - testEnv.version = require('../package.json').version; - } - } -} +// test environment defaults +const testEnvDefaults = { + label: '', + arch: process.arch, + cpu: os.cpus()[0].model, + cpuCount: os.cpus().length, + platform: process.platform, + runtime: 'Node.js', + runtimeVersion: process.version, + comment: '', + version: require('../package.json').version +}; -let benchmarkOptions = null; -if(process.env.JSONLD_BENCHMARK) { - if(!(['0', 'false'].includes(process.env.JSONLD_BENCHMARK))) { - benchmarkOptions = {}; - if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { - process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { - const kv = pair.split('='); - benchmarkOptions[kv[0]] = kv[1]; - }); - } - } -} +const env = { + BAIL: process.env.BAIL, + BENCHMARK: process.env.BENCHMARK, + TEST_ENV: process.env.TEST_ENV, + VERBOSE_SKIP: process.env.VERBOSE_SKIP +}; const options = { + env, nodejs: { path }, assert, benchmark, - jsonld, exit: code => process.exit(code), earl: { filename: process.env.EARL }, - verboseSkip: process.env.VERBOSE_SKIP === 'true', - bailOnError: process.env.BAIL === 'true', entries, - testEnv, - benchmarkOptions, + testEnvDefaults, readFile: filename => { return fs.readFile(filename, 'utf8'); }, diff --git a/tests/test.js b/tests/test.js index 500fe9ed..0d910bee 100644 --- a/tests/test.js +++ b/tests/test.js @@ -1,20 +1,120 @@ /** - * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. + * Test and benchmark runner for jsonld.js. + * + * Use environment vars to control: + * + * General: + * Boolean env options enabled with case insensitve values: + * 'true', 't', 'yes', 'y', 'on', '1', similar for false + * Set dirs, manifests, or js to run: + * TESTS="r1 r2 ..." + * Output an EARL report: + * EARL=filename + * Test environment details for EARL report: + * This is useful for benchmark comparison. + * By default no details are added for privacy reasons. + * Automatic details can be added for all fields with '1', 'true', or 'auto': + * TEST_ENV=1 + * To include only certain fields, set them, or use 'auto': + * TEST_ENV=cpu='Intel i7-4790K @ 4.00GHz',runtime='Node.js',... + * TEST_ENV=cpu=auto # only cpu + * TEST_ENV=cpu,runtime # only cpu and runtime + * TEST_ENV=auto,comment='special test' # all auto with override + * Available fields: + * - label - ex: 'Setup 1' (short label for reports) + * - arch - ex: 'x64' + * - cpu - ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' + * - cpuCount - ex: 8 + * - platform - ex: 'linux' + * - runtime - ex: 'Node.js' + * - runtimeVersion - ex: 'v14.19.0' + * - comment: any text + * - version: jsonld.js version + * Bail with tests fail: + * BAIL= (default: false) + * Verbose skip reasons: + * VERBOSE_SKIP= (default: false) + * Benchmark mode: + * Basic: + * BENCHMARK=1 + * With options: + * BENCHMARK=key1=value1,key2=value2,... + * Benchmark options: + * jobs=N1[+N2[...]] (default: 1) + * Run each test with jobs size of N1, N2, ... + * Recommend 1+10 to get simple and parallel data. + * Note the N>1 tests use custom reporter to show time per job. + * fast1= (default: false) + * Run single job faster by omitting Promise.all wrapper. + * + * @author Dave Longley + * @author David I. Lehn + * + * Copyright (c) 2011-2023 Digital Bazaar, Inc. All rights reserved. */ /* eslint-disable indent */ const EarlReport = require('./earl-report'); const join = require('join-path-js'); -const rdfCanonize = require('rdf-canonize'); -const {prependBase} = require('../lib/url'); +const jsonld = require('..'); const {klona} = require('klona'); +const {prependBase} = require('../lib/url'); +const rdfCanonize = require('rdf-canonize'); + +// helper functions, inspired by 'boolean' package +function isTrue(value) { + return value && [ + 'true', 't', 'yes', 'y', 'on', '1' + ].includes(value.trim().toLowerCase()); +} -module.exports = function(options) { +function isFalse(value) { + return !value || [ + 'false', 'f', 'no', 'n', 'off', '0' + ].includes(value.trim().toLowerCase()); +} + +module.exports = async function(options) { 'use strict'; const assert = options.assert; const benchmark = options.benchmark; -const jsonld = options.jsonld; + +const bailOnError = isTrue(options.env.BAIL || 'false'); +const verboseSkip = isTrue(options.env.VERBOSE_SKIP || 'false'); + +const benchmarkOptions = { + enabled: false, + jobs: [1], + fast1: false +}; + +if(options.env.BENCHMARK) { + if(!isFalse(options.env.BENCHMARK)) { + benchmarkOptions.enabled = true; + if(!isTrue(options.env.BENCHMARK)) { + options.env.BENCHMARK.split(',').forEach(pair => { + const kv = pair.split('='); + switch(kv[0]) { + case 'jobs': + benchmarkOptions.jobs = kv[1].split('+').map(n => parseInt(n, 10)); + break; + case 'fast1': + benchmarkOptions.fast1 = isTrue(kv[1]); + break; + default: + throw new Error(`Unknown benchmark option: "${pair}"`); + } + }); + } + } +} + +// Only support one job size for EARL output to simplify reporting and avoid +// multi-variable issues. Can compare multiple runs with different job sizes. +if(options.earl.filename && benchmarkOptions.jobs.length > 1) { + throw new Error('Only one job size allowed when outputting EARL.'); +} const manifest = options.manifest || { '@context': 'https://json-ld.org/test-suite/context.jsonld', @@ -22,7 +122,9 @@ const manifest = options.manifest || { '@type': 'mf:Manifest', description: 'Top level jsonld.js manifest', name: 'jsonld.js', - sequence: options.entries || [], + // allow for async generated entries + // used for karma tests to allow async server exist check + sequence: (await Promise.all(options.entries || [])).flat().filter(e => e), filename: '/' }; @@ -268,7 +370,7 @@ const TEST_TYPES = { /manifest-urdna2015#test060/ ] }, - fn: 'normalize', + fn: 'canonize', params: [ readTestNQuads('action'), createTestOptions({ @@ -283,13 +385,46 @@ const TEST_TYPES = { const SKIP_TESTS = []; +// build test env from defaults +const testEnvFields = [ + 'label', 'arch', 'cpu', 'cpuCount', 'platform', 'runtime', 'runtimeVersion', + 'comment', 'version' +]; +let testEnv = null; +if(options.env.TEST_ENV) { + let _test_env = options.env.TEST_ENV; + if(!isFalse(_test_env)) { + testEnv = {}; + if(isTrue(_test_env)) { + _test_env = 'auto'; + } + _test_env.split(',').forEach(pair => { + if(pair === 'auto') { + testEnvFields.forEach(f => testEnv[f] = 'auto'); + } else { + const kv = pair.split('='); + if(kv.length === 1) { + testEnv[kv[0]] = 'auto'; + } else { + testEnv[kv[0]] = kv.slice(1).join('='); + } + } + }); + testEnvFields.forEach(f => { + if(testEnv[f] === 'auto') { + testEnv[f] = options.testEnvDefaults[f]; + } + }); + } +} + // create earl report if(options.earl && options.earl.filename) { options.earl.report = new EarlReport({ - env: options.testEnv + env: testEnv }); - if(options.benchmarkOptions) { - options.earl.report.setupForBenchmarks({testEnv: options.testEnv}); + if(benchmarkOptions.enabled) { + options.earl.report.setupForBenchmarks({testEnv}); } } @@ -445,37 +580,69 @@ async function addTest(manifest, test, tests) { test.manifest = manifest; const description = test_id + ' ' + (test.purpose || test.name); - const _test = { - title: description, - f: makeFn({ - test, - run: ({test, testInfo, params}) => { - return jsonld[testInfo.fn](...params); - } - }) - }; - // 'only' based on test manifest - // 'skip' handled via skip() - if('only' in test) { - _test.only = test.only; - } - tests.push(_test); + // build test options for omit checks + const testInfo = TEST_TYPES[getJsonLdTestType(test)]; + const params = testInfo.params.map(param => param(test)); + const testOptions = params[1]; + + // number of parallel jobs for benchmarks + const jobTests = benchmarkOptions.enabled ? benchmarkOptions.jobs : [1]; + const fast1 = benchmarkOptions.enabled ? benchmarkOptions.fast1 : true; + + jobTests.forEach(jobs => { + const _test = { + title: description + ` (jobs=${jobs})`, + f: makeFn({ + test, + run: ({test, testInfo, params}) => { + // skip Promise.all + if(jobs === 1 && fast1) { + return jsonld[testInfo.fn](...params); + } + const all = []; + for(let j = 0; j < jobs; j++) { + all.push(jsonld[testInfo.fn](...params)); + } + return Promise.all(all); + }, + jobs, + isBenchmark: benchmarkOptions.enabled + }) + }; + // 'only' based on test manifest + // 'skip' handled via skip() + if('only' in test) { + _test.only = test.only; + } + tests.push(_test); + }); } function makeFn({ test, adjustParams = p => p, run, - ignoreResult = false + jobs, + isBenchmark = false, + unsupportedInBrowser = false }) { return async function() { const self = this; - self.timeout(5000); + self.timeout(10000); const testInfo = TEST_TYPES[getJsonLdTestType(test)]; + // skip if unsupported in browser + if(unsupportedInBrowser) { + if(verboseSkip) { + console.log('Skipping test due no browser support:', + {id: test['@id'], name: test.name}); + } + self.skip(); + } + // skip based on test manifest if('skip' in test && test.skip) { - if(options.verboseSkip) { + if(verboseSkip) { console.log('Skipping test due to manifest:', {id: test['@id'], name: test.name}); } @@ -485,7 +652,7 @@ function makeFn({ // skip based on unknown test type const testTypes = Object.keys(TEST_TYPES); if(!isJsonLdType(test, testTypes)) { - if(options.verboseSkip) { + if(verboseSkip) { const type = [].concat( getJsonLdValues(test, '@type'), getJsonLdValues(test, 'type') @@ -498,7 +665,7 @@ function makeFn({ // skip based on test type if(isJsonLdType(test, SKIP_TESTS)) { - if(options.verboseSkip) { + if(verboseSkip) { const type = [].concat( getJsonLdValues(test, '@type'), getJsonLdValues(test, 'type') @@ -511,7 +678,7 @@ function makeFn({ // skip based on type info if(testInfo.skip && testInfo.skip.type) { - if(options.verboseSkip) { + if(verboseSkip) { console.log('Skipping test due to type info:', {id: test['@id'], name: test.name}); } @@ -522,7 +689,7 @@ function makeFn({ if(testInfo.skip && testInfo.skip.idRegex) { testInfo.skip.idRegex.forEach(function(re) { if(re.test(test['@id'])) { - if(options.verboseSkip) { + if(verboseSkip) { console.log('Skipping test due to id:', {id: test['@id']}); } @@ -536,8 +703,11 @@ function makeFn({ testInfo.skip.descriptionRegex.forEach(function(re) { if(re.test(description)) { if(options.verboseSkip) { - console.log('Skipping test due to description:', - {id: test['@id'], name: test.name, description}); + console.log('Skipping test due to description:', { + id: test['@id'], + name: test.name, + description + }); } self.skip(); } @@ -613,7 +783,7 @@ function makeFn({ try { if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { - if(!ignoreResult) { + if(!isBenchmark) { await compareExpectedError(test, err); } } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || @@ -622,7 +792,7 @@ function makeFn({ if(err) { throw err; } - if(!ignoreResult) { + if(!isBenchmark) { await testInfo.compare(test, result); } } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) { @@ -632,21 +802,27 @@ function makeFn({ } let benchmarkResult = null; - if(options.benchmarkOptions) { + if(benchmarkOptions.enabled) { + const bparams = adjustParams(testInfo.params.map(param => param(test, { + // pre-load params to avoid doc loader and parser timing + load: true + }))); + // resolve test data + const bvalues = await Promise.all(bparams); + const result = await runBenchmark({ test, testInfo, + jobs, run, - params: testInfo.params.map(param => param(test, { - // pre-load params to avoid doc loader and parser timing - load: true - })), + params: bvalues, mochaTest: self }); benchmarkResult = { // FIXME use generic prefix '@type': 'jldb:BenchmarkResult', - 'jldb:hz': result.target.hz, + // normalize to jobs/sec from overall ops/sec + 'jldb:hz': result.target.hz * jobs, 'jldb:rme': result.target.stats.rme }; } @@ -661,13 +837,13 @@ function makeFn({ // FIXME: for now, explicitly disabling tests. //if(!normativeTest) { // // failure ok - // if(options.verboseSkip) { + // if(verboseSkip) { // console.log('Skipping non-normative test due to failure:', // {id: test['@id'], name: test.name}); // } // self.skip(); //} - if(options.bailOnError) { + if(bailOnError) { if(err.name !== 'AssertionError') { console.error('\nError: ', JSON.stringify(err, null, 2)); } @@ -682,16 +858,14 @@ function makeFn({ }; } -async function runBenchmark({test, testInfo, params, run, mochaTest}) { - const values = await Promise.all(params); - +async function runBenchmark({test, testInfo, jobs, params, run, mochaTest}) { return new Promise((resolve, reject) => { const suite = new benchmark.Suite(); suite.add({ name: test.name, defer: true, fn: deferred => { - run({test, testInfo, params: values}).then(() => { + run({test, testInfo, params}).then(() => { deferred.resolve(); }); } @@ -699,10 +873,13 @@ async function runBenchmark({test, testInfo, params, run, mochaTest}) { suite .on('start', e => { // set timeout to a bit more than max benchmark time - mochaTest.timeout((e.target.maxTime + 2) * 1000); + mochaTest.timeout((e.target.maxTime + 10) * 1000 * jobs); }) .on('cycle', e => { - console.log(String(e.target)); + const jobsHz = e.target.hz * jobs; + const jobsPerSec = jobsHz.toFixed(jobsHz < 100 ? 2 : 0); + const msg = `${String(e.target)} (${jobsPerSec} jobs/sec)`; + console.log(msg); }) .on('error', err => { reject(new Error(err)); @@ -808,6 +985,7 @@ function createTestOptions(opts) { }; const httpOptions = ['contentType', 'httpLink', 'httpStatus', 'redirectTo']; const testOptions = test.option || {}; + Object.assign(options, testOptions); for(const key in testOptions) { if(httpOptions.indexOf(key) === -1) { options[key] = testOptions[key]; @@ -815,9 +993,7 @@ function createTestOptions(opts) { } if(opts) { // extend options - for(const key in opts) { - options[key] = opts[key]; - } + Object.assign(options, opts); } return options; }; @@ -857,7 +1033,7 @@ async function compareExpectedNQuads(test, result) { expect = await readTestNQuads(_getExpectProperty(test))(test); assert.strictEqual(result, expect); } catch(ex) { - if(options.bailOnError) { + if(bailOnError) { console.log('\nTEST FAILED\n'); console.log('EXPECTED:\n' + expect); console.log('ACTUAL:\n' + result); From b7de0afa1a941680b6b3b63ad936cc8a0bc3fc7b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 18 Apr 2023 19:56:25 -0400 Subject: [PATCH 138/181] Update benchmark compare script. - Add "present" mode to only show env items that are present. - Change label handling. - Fix various bugs and data handling issues. --- CHANGELOG.md | 1 + benchmarks/compare/compare.js | 87 ++++++++++++++++++++++++++++------- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0947b8cb..91128491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Conditionally load test suites. - Fix various minor bugs. - Add multiple jobs benchmarking support. +- Update benchmark compare script. ### Fixed - Improve safe mode for `@graph` use cases. diff --git a/benchmarks/compare/compare.js b/benchmarks/compare/compare.js index 12803c9b..e537495e 100755 --- a/benchmarks/compare/compare.js +++ b/benchmarks/compare/compare.js @@ -27,7 +27,7 @@ yargs(hideBin(process.argv)) }) .option('env', { alias: 'e', - choices: ['none', 'all', 'combined'], + choices: ['none', 'all', 'present', 'combined'], default: 'none', description: 'Output environment format' }) @@ -50,6 +50,7 @@ async function compare({ fn: f, content: await fs.readFile(f, 'utf8') }))); + //console.log(contents); const results = contents .map(c => ({ fn: c.fn, @@ -57,11 +58,14 @@ async function compare({ // map of test id => assertion testMap: new Map() })) + .map(c => { + //console.log('C', c); + return c; + }) .map(c => ({ ...c, - // FIXME process properly - env: c.content['@included'][0], - label: c.content['@included'][0]['jldb:label'] + env: c.content['@included']?.[0] || {}, + label: c.content['@included']?.[0]?.['jldb:label'] })); //console.log(JSON.stringify(results, null, 2)); // order of tests found in each result set @@ -96,14 +100,17 @@ async function compare({ hz(r.testMap.get(t)))) .map(d => relative ? d.toFixed(2) + '%' : d.toFixed(2)) ]); - //console.log(compared); - //console.log(results); + //console.log('COMPARED', compared); + //console.log('RESULTS', results); const fnprefixlen = commonPathPrefix(file).length; + function label(res) { + return res.label || res.fn.slice(fnprefixlen); + } console.log('## Comparison'); console.log(markdownTable([ [ 'Test', - ...results.map(r => r.label || r.fn.slice(fnprefixlen)) + ...results.map(label) ], ...compared ], { @@ -130,15 +137,58 @@ async function compare({ ['Comment', 'jldb:comment'] ]; + // show all properites if(env === 'all') { console.log(); console.log('## Environment'); - console.log(markdownTable([ - envProps.map(p => p[0]), - ...results.map(r => envProps.map(p => r.env[p[1]] || '')) + //const data = results.map(r => envProps.map(p => { + // return (p[1] === 'jldb:label') ? label(r) : r.env[p[1]] || ''; + //})); + const data = results.map(r => [ + label(r), + ...envProps.slice(1).map(p => r.env[p[1]] || '') + ]); + if(data.length > 0) { + console.log(markdownTable([ + envProps.map(p => p[0]), + ...data + ])); + } else { + console.log('*not specified*'); + } + } + + // show present properites + if(env === 'present') { + console.log(); + console.log('## Environment'); + // get all data + const data = results.map(r => [ + label(r), + ...envProps.slice(1).map(p => r.env[p[1]] || '') + ]); + // count present truthy fields per col + const propCounts = envProps.slice(1) + .map(p => results.reduce((c, r) => r.env[p[1]] ? ++c : c, 0)); + const presentProps = [ + envProps[0], + ...envProps.slice(1).filter((v, i) => propCounts[i] > 0) + ]; + const presentData = data.map(d => ([ + d[0], + ...d.slice(1).filter((v, i) => propCounts[i] > 0) ])); + if(data.length > 0) { + console.log(markdownTable([ + presentProps.map(p => p[0]), + ...presentData + ])); + } else { + console.log('*not specified*'); + } } + // show combined grouping of properties if(env === 'combined') { console.log(); console.log('## Environment'); @@ -149,11 +199,16 @@ async function compare({ ); return [key, values.size ? [...values].join(', ') : []]; } - console.log(markdownTable([ - ['Key', 'Values'], - ...envProps - .map(p => envline(p[0], p[1])) - .filter(p => p[1].length) - ])); + const data = envProps + .map(p => envline(p[0], p[1])) + .filter(p => p[1].length); + if(data.length > 0) { + console.log(markdownTable([ + ['Key', 'Values'], + ...data + ])); + } else { + console.log('*not specified*'); + } } } From 26246d3b7a8d96828a795c4e72ad6530a773435c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 18 Apr 2023 20:31:15 -0400 Subject: [PATCH 139/181] Add lint ignore paths. --- .eslintrc.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index eb45c536..d3e20e93 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,9 @@ module.exports = { 'digitalbazaar' ], ignorePatterns: [ + 'coverage/', 'dist/', + 'test-suites', 'tests/webidl/WebIDLParser.js', 'tests/webidl/idlharness.js', 'tests/webidl/testharness.js' From f4c61b62f9fd6255651bfc07a6ebb9c4cf3572e9 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 18 Apr 2023 20:31:46 -0400 Subject: [PATCH 140/181] Fix lint issues. --- tests/earl-report.js | 92 ++++++++++++++++++++++---------------------- tests/test.js | 8 +++- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/tests/earl-report.js b/tests/earl-report.js index d969e0d8..79b8c940 100644 --- a/tests/earl-report.js +++ b/tests/earl-report.js @@ -6,6 +6,52 @@ * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. */ +/* eslint-disable quote-props */ +const _benchmarkContext = { + 'jldb': 'http://json-ld.org/benchmarks/vocab#', + 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', + 'xsd': 'http://www.w3.org/2001/XMLSchema#', + + // environment description + 'jldb:Environment': {'@type': '@id'}, + + // per environment + // label + // ex: 'Setup 1' (for reports) + 'jldb:label': {'@type': 'xsd:string'}, + // architecture type + // ex: x86 + 'jldb:arch': {'@type': 'xsd:string'}, + // cpu model description (may show multiple cpus) + // ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' + 'jldb:cpu': {'@type': 'xsd:string'}, + // count of cpus, may not be uniform, just informative + 'jldb:cpuCount': {'@type': 'xsd:integer'}, + // platform name + // ex: linux + 'jldb:platform': {'@type': 'xsd:string'}, + // runtime name + // ex: Node.js, Chromium, Ruby + 'jldb:runtime': {'@type': 'xsd:string'}, + // runtime version + // ex: v14.19.0 + 'jldb:runtimeVersion': {'@type': 'xsd:string'}, + // arbitrary comment + 'jldb:comment': 'rdfs:comment', + + // benchmark result + 'jldb:BenchmarkResult': {'@type': '@id'}, + + // use in earl:Assertion, type jldb:BenchmarkResult + 'jldb:result': {'@type': '@id'}, + + // per BenchmarkResult + 'jldb:environment': {'@type': '@id'}, + 'jldb:hz': {'@type': 'xsd:float'}, + 'jldb:rme': {'@type': 'xsd:float'} +}; +/* eslint-enable quote-props */ + /** * EARL Reporter */ @@ -154,50 +200,4 @@ class EarlReport { } } -/* eslint-disable quote-props */ -const _benchmarkContext = { - 'jldb': 'http://json-ld.org/benchmarks/vocab#', - 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - - // environment description - 'jldb:Environment': {'@type': '@id'}, - - // per environment - // label - // ex: 'Setup 1' (for reports) - 'jldb:label': {'@type': 'xsd:string'}, - // architecture type - // ex: x86 - 'jldb:arch': {'@type': 'xsd:string'}, - // cpu model description (may show multiple cpus) - // ex: 'Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz' - 'jldb:cpu': {'@type': 'xsd:string'}, - // count of cpus, may not be uniform, just informative - 'jldb:cpuCount': {'@type': 'xsd:integer'}, - // platform name - // ex: linux - 'jldb:platform': {'@type': 'xsd:string'}, - // runtime name - // ex: Node.js, Chromium, Ruby - 'jldb:runtime': {'@type': 'xsd:string'}, - // runtime version - // ex: v14.19.0 - 'jldb:runtimeVersion': {'@type': 'xsd:string'}, - // arbitrary comment - 'jldb:comment': 'rdfs:comment', - - // benchmark result - 'jldb:BenchmarkResult': {'@type': '@id'}, - - // use in earl:Assertion, type jldb:BenchmarkResult - 'jldb:result': {'@type': '@id'}, - - // per BenchmarkResult - 'jldb:environment': {'@type': '@id'}, - 'jldb:hz': {'@type': 'xsd:float'}, - 'jldb:rme': {'@type': 'xsd:float'} -}; -/* eslint-enable quote-props */ - module.exports = EarlReport; diff --git a/tests/test.js b/tests/test.js index 0d910bee..4f2449a1 100644 --- a/tests/test.js +++ b/tests/test.js @@ -580,10 +580,12 @@ async function addTest(manifest, test, tests) { test.manifest = manifest; const description = test_id + ' ' + (test.purpose || test.name); + /* // build test options for omit checks const testInfo = TEST_TYPES[getJsonLdTestType(test)]; const params = testInfo.params.map(param => param(test)); const testOptions = params[1]; + */ // number of parallel jobs for benchmarks const jobTests = benchmarkOptions.enabled ? benchmarkOptions.jobs : [1]; @@ -594,7 +596,7 @@ async function addTest(manifest, test, tests) { title: description + ` (jobs=${jobs})`, f: makeFn({ test, - run: ({test, testInfo, params}) => { + run: ({/*test, */testInfo, params}) => { // skip Promise.all if(jobs === 1 && fast1) { return jsonld[testInfo.fn](...params); @@ -699,10 +701,11 @@ function makeFn({ } // skip based on description regex + /* if(testInfo.skip && testInfo.skip.descriptionRegex) { testInfo.skip.descriptionRegex.forEach(function(re) { if(re.test(description)) { - if(options.verboseSkip) { + if(verboseSkip) { console.log('Skipping test due to description:', { id: test['@id'], name: test.name, @@ -713,6 +716,7 @@ function makeFn({ } }); } + */ // Make expandContext absolute to the manifest if(test.hasOwnProperty('option') && test.option.expandContext) { From 726570ce77e3201bf3bd9203fb71c13508935129 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 18 Apr 2023 21:13:04 -0400 Subject: [PATCH 141/181] Remove old tests. --- tests/test-karma.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test-karma.js b/tests/test-karma.js index e15cb2a4..724ab438 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -90,7 +90,6 @@ if(process.env.TESTS) { // other tests entries.push(join(_top, 'tests/misc.js')); entries.push(join(_top, 'tests/graph-container.js')); - entries.push(join(_top, 'tests/new-embed-api')); // WebIDL tests entries.push(webidl); From 6f221e94bff6740e548cd17218c2749c230572c6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 18 Apr 2023 21:13:34 -0400 Subject: [PATCH 142/181] Add import filename to error. --- tests/test-karma.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-karma.js b/tests/test-karma.js index 724ab438..19ba045b 100644 --- a/tests/test-karma.js +++ b/tests/test-karma.js @@ -146,7 +146,7 @@ const options = { }, /* eslint-disable-next-line no-unused-vars */ import: f => { - console.error('import not implemented'); + console.error('import not implemented for "' + f + '"'); } }; From fcbe90cd4182926043f891a99cca239ad6695d7c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 19 May 2023 18:47:00 -0400 Subject: [PATCH 143/181] Fix `@json` frame test 0069. --- CHANGELOG.md | 1 + lib/frame.js | 5 +++-- tests/test.js | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91128491..b25aece2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Fixed - Improve safe mode for `@graph` use cases. +- Fix `@json` frame test 0069. ## 8.1.1 - 2023-02-25 diff --git a/lib/frame.js b/lib/frame.js index 27675fb7..5244d04e 100644 --- a/lib/frame.js +++ b/lib/frame.js @@ -464,8 +464,9 @@ function _validateFrame(frame) { if('@type' in frame[0]) { for(const type of util.asArray(frame[0]['@type'])) { - // @id must be wildcard or an IRI - if(!(types.isObject(type) || url.isAbsolute(type)) || + // @type must be wildcard, IRI, or @json + if(!(types.isObject(type) || url.isAbsolute(type) || + (type === '@json')) || (types.isString(type) && type.indexOf('_:') === 0)) { throw new JsonLdError( 'Invalid JSON-LD syntax; invalid @type in frame.', diff --git a/tests/test.js b/tests/test.js index 4f2449a1..d2e3e4ec 100644 --- a/tests/test.js +++ b/tests/test.js @@ -247,7 +247,6 @@ const TEST_TYPES = { // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ - /frame-manifest#t0069$/, ] }, fn: 'frame', From 4c04518a8c976fc5b1e8bdfb634b547170b817fd Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 19 May 2023 19:24:06 -0400 Subject: [PATCH 144/181] Update minor dependencies. --- benchmarks/compare/package.json | 4 ++-- package.json | 38 ++++++++++++++++----------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/benchmarks/compare/package.json b/benchmarks/compare/package.json index 141699a3..d5ad0026 100644 --- a/benchmarks/compare/package.json +++ b/benchmarks/compare/package.json @@ -26,8 +26,8 @@ "main": "compare.js", "dependencies": { "common-path-prefix": "^3.0.0", - "markdown-table": "^3.0.2", - "yargs": "^17.5.1" + "markdown-table": "^3.0.3", + "yargs": "^17.7.2" }, "engines": { "node": ">=14" diff --git a/package.json b/package.json index 7a0bd8ee..a6f8ccc4 100644 --- a/package.json +++ b/package.json @@ -32,51 +32,51 @@ "@digitalbazaar/http-client": "^3.4.1", "canonicalize": "^1.0.1", "lru-cache": "^6.0.0", - "rdf-canonize": "^3.0.0" + "rdf-canonize": "^3.4.0" }, "devDependencies": { - "@babel/core": "^7.13.14", - "@babel/plugin-proposal-object-rest-spread": "^7.13.8", - "@babel/plugin-transform-modules-commonjs": "^7.13.8", - "@babel/plugin-transform-runtime": "^7.13.10", - "@babel/preset-env": "^7.13.12", - "@babel/runtime": "^7.13.10", + "@babel/core": "^7.21.8", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-transform-modules-commonjs": "^7.21.5", + "@babel/plugin-transform-runtime": "^7.21.4", + "@babel/preset-env": "^7.21.5", + "@babel/runtime": "^7.21.5", "babel-loader": "^8.2.2", "benchmark": "^2.1.4", "browserify": "^17.0.0", - "chai": "^4.3.4", - "core-js": "^3.10.0", - "cors": "^2.7.1", + "chai": "^4.3.7", + "core-js": "^3.30.2", + "cors": "^2.8.5", "cross-env": "^7.0.3", "envify": "^4.1.0", - "eslint": "^8.17.0", + "eslint": "^8.41.0", "eslint-config-digitalbazaar": "^3.0.0", "esmify": "^2.1.1", - "express": "^4.16.4", + "express": "^4.18.2", "fs-extra": "^9.1.0", "join-path-js": "0.0.0", "karma": "^5.2.3", - "karma-babel-preprocessor": "^8.0.1", - "karma-browserify": "^8.0.0", - "karma-chrome-launcher": "^3.1.0", + "karma-babel-preprocessor": "^8.0.2", + "karma-browserify": "^8.1.0", + "karma-chrome-launcher": "^3.2.0", "karma-edge-launcher": "^0.4.2", - "karma-firefox-launcher": "^2.1.0", + "karma-firefox-launcher": "^2.1.2", "karma-ie-launcher": "^1.0.0", "karma-mocha": "^2.0.1", "karma-mocha-reporter": "^2.2.5", "karma-safari-launcher": "^1.0.0", - "karma-server-side": "^1.7.0", + "karma-server-side": "^1.8.0", "karma-sourcemap-loader": "^0.3.7", "karma-tap-reporter": "0.0.6", "karma-webpack": "^4.0.2", - "klona": "^2.0.5", + "klona": "^2.0.6", "mocha": "^8.3.2", "mocha-lcov-reporter": "^1.3.0", "nyc": "^15.1.0", "watchify": "^3.11.1", "webpack": "^4.46.0", "webpack-cli": "^4.5.0", - "webpack-merge": "^5.7.3" + "webpack-merge": "^5.8.0" }, "engines": { "node": ">=14" From cb351dcc49b5f1905a673cdb167a66687fc7e47f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 19 May 2023 21:56:03 -0400 Subject: [PATCH 145/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b25aece2..9293312f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.2.0 - 2023-03-xx +## 8.2.0 - 2023-05-19 ### Changed - Update for latest [rdf-canon][] changes: test suite location, README, links, From db4b3e12a003b70ad86568a58d886aa8760d41ec Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 19 May 2023 21:56:04 -0400 Subject: [PATCH 146/181] Release 8.2.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6f8ccc4..ae46edfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.1.2-0", + "version": "8.2.0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 1f34863d0ba9e0fc631beea084da63f3e6c8eaf6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 19 May 2023 21:57:01 -0400 Subject: [PATCH 147/181] Start 8.2.1-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae46edfa..92d736f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.2.0", + "version": "8.2.1-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 8101388fd0eff0ccaff5fcaf749bdff30957b7c1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 15:59:21 -0400 Subject: [PATCH 148/181] Fix graph property empty array and safe mode. - Fix handling of graph property with empty array. - Fix safe mode for `@graph` use cases. - Check all elements of graph property with array. --- CHANGELOG.md | 7 ++ lib/expand.js | 24 ++++-- tests/misc.js | 208 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 229 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9293312f..100a242a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # jsonld ChangeLog +## 8.2.1 - 2023-xx-xx + +### Fixed +- Fix handling of graph property with empty array. +- Fix safe mode for `@graph` use cases. + - Check all elements of graph property with array. + ## 8.2.0 - 2023-05-19 ### Changed diff --git a/lib/expand.js b/lib/expand.js index 280d67db..bae49994 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -386,6 +386,12 @@ api.expand = async ({ /** * Drop empty object, top-level @value/@list, or object with only @id + * + * @param value Value to check. + * @param count Number of properties in object. + * @param options The expansion options. + * + * @return null if dropped, value otherwise. */ function _dropUnsafeObject({ value, @@ -948,15 +954,17 @@ async function _expandObject({ // index cases handled above if(container.includes('@graph') && !container.some(key => key === '@id' || key === '@index')) { - // ensure expanded values are arrays - // ensure an array + // ensure expanded values are in an array expandedValue = _asArray(expandedValue); - // check if needs to be dropped - const count = Object.keys(expandedValue[0]).length; - if(!options.isFrame && _dropUnsafeObject({ - value: expandedValue[0], count, options - }) === null) { - // skip adding and continue + if(!options.isFrame) { + // drop items if needed + expandedValue = expandedValue.filter(v => { + const count = Object.keys(v).length; + return _dropUnsafeObject({value: v, count, options}) !== null; + }); + } + if(expandedValue.length === 0) { + // all items dropped, skip adding and continue continue; } // convert to graph diff --git a/tests/misc.js b/tests/misc.js index f6b5b3f4..91915cdd 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1885,6 +1885,72 @@ _:b0 "[null]"^^ . }); }); + it('should emit for @graph with empty array (1)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + } + }, + "@id": "urn:id", + "p": [] +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'object with only @id' + ], + testNotSafe: true + }); + }); + + it('should not emit for @graph with empty array (2)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + }, + "urn:t": { + "@type": "@id" + } + }, + "@id": "urn:id", + "urn:t": "urn:id", + "p": [] +} +; + const expected = +[ + { + "@id": "urn:id", + "urn:t": [ + { + "@id": "urn:id" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [] + }); + }); + it('should emit for @graph with relative @id (1)', async () => { const input = { @@ -1915,6 +1981,144 @@ _:b0 "[null]"^^ . it('should emit for @graph with relative @id (2)', async () => { const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + } + }, + "@id": "urn:id", + "p": [ + "rel0", + "rel1" + ] +} +; + const expected = []; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'object with only @id', + 'object with only @id', + 'object with only @id' + ], + testNotSafe: true + }); + }); + + it('should emit for @graph with relative @id (3)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + } + }, + "@id": "urn:g0", + "p": [ + { + "@id": "urn:g1", + "urn:p1": "v1" + }, + "rel" + ] +} +; + const expected = +[ + { + "@id": "urn:g0", + "urn:p": [ + { + "@graph": [ + { + "@id": "urn:g1", + "urn:p1": [ + { + "@value": "v1" + } + ] + } + ] + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'object with only @id' + ], + testNotSafe: true + }); + }); + + it('should emit for @graph with relative @id (4)', async () => { + const input = +{ + "@context": { + "p": { + "@id": "urn:p", + "@type": "@id", + "@container": "@graph" + } + }, + "@id": "urn:g0", + "p": [ + "rel", + { + "@id": "urn:g1", + "urn:p1": "v1" + } + ] +} +; + const expected = +[ + { + "@id": "urn:g0", + "urn:p": [ + { + "@graph": [ + { + "@id": "urn:g1", + "urn:p1": [ + { + "@value": "v1" + } + ] + } + ] + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'object with only @id' + ], + testNotSafe: true + }); + }); + + it('should emit for @graph with relative @id (5)', async () => { + const input = { "@context": { "p": { @@ -1955,7 +2159,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (3)', async () => { + it('should emit for @graph with relative @id (6)', async () => { const input = { "@context": { @@ -1997,7 +2201,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (4)', async () => { + it('should emit for @graph with relative @id (7)', async () => { const input = { "@context": { From 9802161d42f16337c516126427c777e9fbb888be Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 16:26:20 -0400 Subject: [PATCH 149/181] Improve graph tests. - Use URNs for tests that fail on "only @id" cases. - Update property names. --- tests/misc.js | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index 91915cdd..92e3bbcf 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1951,7 +1951,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (1)', async () => { + it('should emit for @graph with only @id (1)', async () => { const input = { "@context": { @@ -1962,7 +1962,7 @@ _:b0 "[null]"^^ . } }, "@id": "urn:id", - "p": ["rel"] + "p": ["urn:id0"] } ; const expected = []; @@ -1979,7 +1979,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (2)', async () => { + it('should emit for @graph with only @id (2)', async () => { const input = { "@context": { @@ -1991,8 +1991,8 @@ _:b0 "[null]"^^ . }, "@id": "urn:id", "p": [ - "rel0", - "rel1" + "urn:id0", + "urn:id1" ] } ; @@ -2011,7 +2011,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (3)', async () => { + it('should emit for @graph with only @id (3)', async () => { const input = { "@context": { @@ -2024,10 +2024,10 @@ _:b0 "[null]"^^ . "@id": "urn:g0", "p": [ { - "@id": "urn:g1", - "urn:p1": "v1" + "@id": "urn:id0", + "urn:p0": "v0" }, - "rel" + "urn:id1" ] } ; @@ -2039,10 +2039,10 @@ _:b0 "[null]"^^ . { "@graph": [ { - "@id": "urn:g1", - "urn:p1": [ + "@id": "urn:id0", + "urn:p0": [ { - "@value": "v1" + "@value": "v0" } ] } @@ -2064,7 +2064,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (4)', async () => { + it('should emit for @graph with only @id (4)', async () => { const input = { "@context": { @@ -2076,9 +2076,9 @@ _:b0 "[null]"^^ . }, "@id": "urn:g0", "p": [ - "rel", + "urn:id0", { - "@id": "urn:g1", + "@id": "urn:id1", "urn:p1": "v1" } ] @@ -2092,7 +2092,7 @@ _:b0 "[null]"^^ . { "@graph": [ { - "@id": "urn:g1", + "@id": "urn:id1", "urn:p1": [ { "@value": "v1" @@ -2117,7 +2117,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (5)', async () => { + it('should emit for @graph with only @id (5)', async () => { const input = { "@context": { @@ -2132,7 +2132,7 @@ _:b0 "[null]"^^ . }, "@id": "urn:id", "urn:t": "urn:id", - "p": ["rel"] + "p": ["urn:id0"] } ; const expected = @@ -2159,7 +2159,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with relative @id (6)', async () => { + it('should emit for @graph with only @id (6)', async () => { const input = { "@context": { @@ -2174,7 +2174,7 @@ _:b0 "[null]"^^ . }, "@id": "urn:id", "urn:t": "urn:id", - "p": "rel" + "p": "urn:id0" } ; const expected = @@ -2218,7 +2218,7 @@ _:b0 "[null]"^^ . "urn:t": "urn:id", "p": { "@id": "rel", - "urn:t": "urn:id2" + "urn:t": "urn:id0" } } ; @@ -2238,7 +2238,7 @@ _:b0 "[null]"^^ . "@id": "rel", "urn:t": [ { - "@id": "urn:id2" + "@id": "urn:id0" } ] } From 29e85caa384eeb76ff2de404b429200eac08f383 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 17:30:05 -0400 Subject: [PATCH 150/181] Fix protected redefinition of equivalent id terms. - Fix test `pr41` of protected redefinition of equivalent id terms. - The `String.match` logic and expression like `(true && null)` was evaluating to `null` which later failed a strict comparison with `false`. Fixed by forcing boolean logic. --- CHANGELOG.md | 1 + lib/context.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 100a242a..47b8b82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fix handling of graph property with empty array. - Fix safe mode for `@graph` use cases. - Check all elements of graph property with array. +- Fix test `pr41` of protected redefinition of equivalent id terms. ## 8.2.0 - 2023-05-19 diff --git a/lib/context.js b/lib/context.js index 175a6775..2d282f84 100644 --- a/lib/context.js +++ b/lib/context.js @@ -708,7 +708,7 @@ api.createTermDefinition = ({ // indicate if this term may be used as a compact IRI prefix mapping._prefix = (simpleTerm && !mapping._termHasColon && - id.match(/[:\/\?#\[\]@]$/)); + id.match(/[:\/\?#\[\]@]$/) !== null); } } From 7b64cd0b16e885e7754f458babafce824a097b1d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 17:40:50 -0400 Subject: [PATCH 151/181] Improve test names. --- tests/misc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/misc.js b/tests/misc.js index 92e3bbcf..0a78fc83 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1885,7 +1885,7 @@ _:b0 "[null]"^^ . }); }); - it('should emit for @graph with empty array (1)', async () => { + it('should emit with only @id and @graph with empty array', async () => { const input = { "@context": { @@ -1912,7 +1912,7 @@ _:b0 "[null]"^^ . }); }); - it('should not emit for @graph with empty array (2)', async () => { + it('should not emit for @graph with empty array', async () => { const input = { "@context": { From 7f2d83b39fbdc32b6f1186921121688c8c199dcd Mon Sep 17 00:00:00 2001 From: ndr_brt Date: Tue, 1 Aug 2023 10:32:02 +0200 Subject: [PATCH 152/181] fix: incorrect @id expansion --- .gitignore | 1 + lib/url.js | 2 ++ tests/misc.js | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/.gitignore b/.gitignore index 75f7bfdf..9c86ac7a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,6 @@ coverage dist node_modules npm-debug.log +package-lock.json tests/webidl/*-new v8.log diff --git a/lib/url.js b/lib/url.js index 7855f693..69c09b47 100644 --- a/lib/url.js +++ b/lib/url.js @@ -109,6 +109,8 @@ api.prependBase = (base, iri) => { path = path.substr(0, path.lastIndexOf('/') + 1); if((path.length > 0 || base.authority) && path.substr(-1) !== '/') { path += '/'; + } else if (rel.protocol) { + path += rel.protocol; } path += rel.path; diff --git a/tests/misc.js b/tests/misc.js index 0a78fc83..91cc931b 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1491,6 +1491,7 @@ _:b0 "v" . testSafe: true }); }); + }); describe('values', () => { @@ -2816,6 +2817,37 @@ _:b0 "[null]"^^ . }); }); + it('should be called on relative IRI for id term [4]', async () => { + const input = +{ + "@id": "34:relativeiri", + "urn:test": "value" +} +; + const expected = +[ + { + "@id": "34:relativeiri", + "urn:test": [ + { + "@value": "value" + } + ] + } +] +; + + await _test({ + type: 'expand', + input, + expected, + eventCodeLog: [ + 'relative @id reference' + ], + testNotSafe: true + }); + }); + it('should be called on relative IRI for id term (nested)', async () => { const input = { From cf26930f64fe00e4daa4a7de24fd9976617a1be1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 19:52:49 -0400 Subject: [PATCH 153/181] Fix relative IRI parsing. - Undo previous "protocol" fix for `123:abc` style relative IRIs. - Update IRI parser to be more strict for RFC3986 URI schemes. --- CHANGELOG.md | 1 + lib/url.js | 4 +--- tests/misc.js | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b8b82f..5992e9a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Fix safe mode for `@graph` use cases. - Check all elements of graph property with array. - Fix test `pr41` of protected redefinition of equivalent id terms. +- Fix relative IRI parsing. ## 8.2.0 - 2023-05-19 diff --git a/lib/url.js b/lib/url.js index 69c09b47..7e597791 100644 --- a/lib/url.js +++ b/lib/url.js @@ -28,7 +28,7 @@ api.parsers = { 'hostname', 'port', 'path', 'directory', 'file', 'query', 'fragment' ], /* eslint-disable-next-line max-len */ - regex: /^(([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(?:(((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ + regex: /^(([a-zA-Z][a-zA-Z0-9+-.]*):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(?:(((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ } }; api.parse = (str, parser) => { @@ -109,8 +109,6 @@ api.prependBase = (base, iri) => { path = path.substr(0, path.lastIndexOf('/') + 1); if((path.length > 0 || base.authority) && path.substr(-1) !== '/') { path += '/'; - } else if (rel.protocol) { - path += rel.protocol; } path += rel.path; diff --git a/tests/misc.js b/tests/misc.js index 91cc931b..d43d5817 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1491,7 +1491,6 @@ _:b0 "v" . testSafe: true }); }); - }); describe('values', () => { From 5cb04f79c04b38d6c1cfb4147ad0061ce7c16fa2 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 20:06:00 -0400 Subject: [PATCH 154/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5992e9a7..fa20ff2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.2.1 - 2023-xx-xx +## 8.2.1 - 2023-08-31 ### Fixed - Fix handling of graph property with empty array. From aec8209c0602f452c25bec8ca9dbffc7026178fe Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 20:06:03 -0400 Subject: [PATCH 155/181] Release 8.2.1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 92d736f1..d69e71e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.2.1-0", + "version": "8.2.1", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 7ba788e9d054b0e96f46e5ea8d43396da63d5f2f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 31 Aug 2023 20:06:53 -0400 Subject: [PATCH 156/181] Start 8.2.2-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d69e71e3..f02dd88c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.2.1", + "version": "8.2.2-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 5e4ab33b003c5f1e8dc03d02d37d3c8589b084d1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Sep 2023 15:05:43 -0400 Subject: [PATCH 157/181] Update test comment style. --- tests/test.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test.js b/tests/test.js index d2e3e4ec..f06676af 100644 --- a/tests/test.js +++ b/tests/test.js @@ -134,9 +134,9 @@ const TEST_TYPES = { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], - // FIXME // NOTE: idRegex format: - //MMM-manifest#tNNN$/, + // /MMM-manifest#tNNN$/, + // FIXME idRegex: [ /compact-manifest#t0112$/, /compact-manifest#t0113$/, @@ -160,9 +160,9 @@ const TEST_TYPES = { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], - // FIXME // NOTE: idRegex format: - //MMM-manifest#tNNN$/, + // /MMM-manifest#tNNN$/, + // FIXME idRegex: [ // spec issues // Unclear how to handle {"@id": null} edge case @@ -219,9 +219,9 @@ const TEST_TYPES = { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], - // FIXME // NOTE: idRegex format: - //MMM-manifest#tNNN$/, + // /MMM-manifest#tNNN$/, + // FIXME idRegex: [ // html /html-manifest#tf001$/, @@ -243,11 +243,10 @@ const TEST_TYPES = { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], - // FIXME // NOTE: idRegex format: - //MMM-manifest#tNNN$/, - idRegex: [ - ] + // /MMM-manifest#tNNN$/, + // FIXME + idRegex: [] }, fn: 'frame', params: [ @@ -262,9 +261,9 @@ const TEST_TYPES = { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], - // FIXME // NOTE: idRegex format: - //MMM-manifest#tNNN$/, + // /MMM-manifest#tNNN$/, + // FIXME idRegex: [ // direction (compound-literal) /fromRdf-manifest#tdi11$/, @@ -291,9 +290,9 @@ const TEST_TYPES = { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], - // FIXME // NOTE: idRegex format: - //MMM-manifest#tNNN$/, + // /MMM-manifest#tNNN$/, + // FIXME idRegex: [ // spec issues // Unclear how to handle {"@id": null} edge case @@ -363,7 +362,8 @@ const TEST_TYPES = { 'rdfc:Urdna2015EvalTest': { skip: { // NOTE: idRegex format: - //manifest-urdna2015#testNNN$/, + // /manifest-urdna2015#testNNN$/, + // FIXME idRegex: [ // Unsupported U escape /manifest-urdna2015#test060/ From d12ccd9b932c63cb8bfd6dd1fa333f23f1499c2f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Sep 2023 15:15:43 -0400 Subject: [PATCH 158/181] Fix lint issues. --- benchmarks/compare/compare.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmarks/compare/compare.js b/benchmarks/compare/compare.js index e537495e..f3f32011 100755 --- a/benchmarks/compare/compare.js +++ b/benchmarks/compare/compare.js @@ -1,10 +1,10 @@ #!/usr/bin/env node -import yargs from 'yargs'; -import {hideBin} from 'yargs/helpers'; +import commonPathPrefix from 'common-path-prefix'; import {promises as fs} from 'node:fs'; +import {hideBin} from 'yargs/helpers'; import {markdownTable} from 'markdown-table'; -import commonPathPrefix from 'common-path-prefix'; +import yargs from 'yargs'; yargs(hideBin(process.argv)) .alias('h', 'help') From 8f9f88826f664718b562081eccc7ce324593a35f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Sep 2023 16:30:25 -0400 Subject: [PATCH 159/181] Improve `@direction` support. - Emit `toRdf` warning if `@direction` is used and `rdfDirection` is not set. - Add safe mode support for `@direction`. Using `@direction` without `rdfDirection` set will cause a safe mode failure. - Add tests. --- CHANGELOG.md | 9 ++ lib/events.js | 4 +- lib/fromRdf.js | 22 +++- lib/jsonld.js | 17 ++- lib/toRdf.js | 56 ++++++++- tests/misc.js | 325 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.js | 2 + 7 files changed, 419 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa20ff2c..b8fb39d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # jsonld ChangeLog +## 8.3.0 - 2023-09-xx + +### Added +- Emit `toRdf` warning if `@direction` is used and `rdfDirection` is not set. + +### Fixed +- Add safe mode support for `@direction`. Using `@direction` without + `rdfDirection` set will cause a safe mode failure. + ## 8.2.1 - 2023-08-31 ### Fixed diff --git a/lib/events.js b/lib/events.js index 3dbd6046..8650b9f6 100644 --- a/lib/events.js +++ b/lib/events.js @@ -123,7 +123,9 @@ const _notSafeEventCodes = new Set([ 'relative graph reference', 'relative object reference', 'relative predicate reference', - 'relative subject reference' + 'relative subject reference', + // toRDF / fromRDF + 'rdfDirection not set' ]); // safe handler that rejects unsafe warning conditions diff --git a/lib/fromRdf.js b/lib/fromRdf.js index afddfdb9..01098353 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; @@ -52,14 +52,28 @@ api.fromRDF = async ( dataset, options ) => { - const defaultGraph = {}; - const graphMap = {'@default': defaultGraph}; - const referencedOnce = {}; const { useRdfType = false, useNativeTypes = false, rdfDirection = null } = options; + // FIXME: use Maps? + const defaultGraph = {}; + const graphMap = {'@default': defaultGraph}; + const referencedOnce = {}; + if(rdfDirection) { + if(rdfDirection === 'compound-literal') { + throw new JsonLdError( + 'Unsupported rdfDirection value.', + 'jsonld.InvalidRdfDirection', + {value: rdfDirection}); + } else if(rdfDirection !== 'i18n-datatype') { + throw new JsonLdError( + 'Unknown rdfDirection value.', + 'jsonld.InvalidRdfDirection', + {value: rdfDirection}); + } + } for(const quad of dataset) { // TODO: change 'name' to 'graph' diff --git a/lib/jsonld.js b/lib/jsonld.js index ce9b868c..c6931aeb 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -118,7 +118,8 @@ const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); * [graph] true to always output a top-level graph (default: false). * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip - * expansion, false not to, defaults to false. + * expansion, false not to, defaults to false. Some well-formed + * and safe-mode checks may be omitted. * [documentLoader(url, options)] the document loader. * [framing] true if compaction is occuring during a framing operation. * [safe] true to use safe mode. (default: false) @@ -538,13 +539,16 @@ jsonld.link = async function(input, ctx, options) { * [base] the base IRI to use (default: `null`). * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip - * expansion, false not to, defaults to false. + * expansion, false not to, defaults to false. Some well-formed + * and safe-mode checks may be omitted. * [inputFormat] the format if input is not JSON-LD: * 'application/n-quads' for N-Quads. * [format] the format if output is a string: * 'application/n-quads' for N-Quads. * [documentLoader(url, options)] the document loader. * [useNative] true to use a native canonize algorithm + * [rdfDirection] null or 'i18n-datatype' to support RDF + * transformation of @direction (default: null). * [safe] true to use safe mode. (default: true). * [contextResolver] internal use only. * @@ -601,8 +605,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). + * [rdfDirection] null or 'i18n-datatype' to support RDF + * transformation of @direction (default: null). * [safe] true to use safe mode. (default: false) * * @return a Promise that resolves to the JSON-LD document. @@ -647,13 +651,16 @@ jsonld.fromRDF = async function(dataset, options) { * [base] the base IRI to use. * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip - * expansion, false not to, defaults to false. + * expansion, false not to, defaults to false. Some well-formed + * and safe-mode checks may be omitted. * [format] the format to use to output a string: * 'application/n-quads' for N-Quads. * [produceGeneralizedRdf] true to output generalized RDF, false * to produce only standard RDF (default: false). * [documentLoader(url, options)] the document loader. * [safe] true to use safe mode. (default: false) + * [rdfDirection] null or 'i18n-datatype' to support RDF + * transformation of @direction (default: null). * [contextResolver] internal use only. * * @return a Promise that resolves to the RDF dataset. diff --git a/lib/toRdf.js b/lib/toRdf.js index d823a97e..f576f6d8 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; @@ -7,6 +7,7 @@ const {createNodeMap} = require('./nodeMap'); const {isKeyword} = require('./context'); const graphTypes = require('./graphTypes'); const jsonCanonicalize = require('canonicalize'); +const JsonLdError = require('./JsonLdError'); const types = require('./types'); const util = require('./util'); @@ -312,18 +313,61 @@ function _objectToRDF( } else if(types.isNumber(value)) { object.value = value.toFixed(0); object.datatype.value = datatype || XSD_INTEGER; - } else if(rdfDirection === 'i18n-datatype' && - '@direction' in item) { - const datatype = 'https://www.w3.org/ns/i18n#' + - (item['@language'] || '') + - `_${item['@direction']}`; + } else if('@direction' in item && rdfDirection === 'i18n-datatype') { + const language = (item['@language'] || '').toLowerCase(); + const direction = item['@direction']; + const datatype = `https://www.w3.org/ns/i18n#${language}_${direction}`; object.datatype.value = datatype; object.value = value; + } else if('@direction' in item && rdfDirection === 'compound-literal') { + throw new JsonLdError( + 'Unsupported rdfDirection value.', + 'jsonld.InvalidRdfDirection', + {value: rdfDirection}); + } else if('@direction' in item && rdfDirection) { + throw new JsonLdError( + 'Unknown rdfDirection value.', + 'jsonld.InvalidRdfDirection', + {value: rdfDirection}); } else if('@language' in item) { + if('@direction' in item && rdfDirection === null) { + if(options.eventHandler) { + // FIXME: only emit once? + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'rdfDirection not set', + level: 'warning', + message: 'rdfDirection not set for @direction.', + details: { + object: object.value + } + }, + options + }); + } + } object.value = value; object.datatype.value = datatype || RDF_LANGSTRING; object.language = item['@language']; } else { + if('@direction' in item && rdfDirection === null) { + if(options.eventHandler) { + // FIXME: only emit once? + _handleEvent({ + event: { + type: ['JsonLdEvent'], + code: 'rdfDirection not set', + level: 'warning', + message: 'rdfDirection not set for @direction.', + details: { + object: object.value + } + }, + options + }); + } + } object.value = value; object.datatype.value = datatype || XSD_STRING; } diff --git a/tests/misc.js b/tests/misc.js index d43d5817..a552d7f9 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -3631,6 +3631,72 @@ _:b0 "[null]"^^ . }); }); + // inputs/outputs for @direction+rdfDirection fromRDF/toRDF tests + const _json_dir_nl_nd = +[ + { + "@id": "urn:id", + "ex:p": [ + { + "@value": "v" + } + ] + } +] +; + const _json_dir_nl_d = +[ + { + "@id": "urn:id", + "ex:p": [ + { + "@direction": "ltr", + "@value": "v" + } + ] + } +] +; + const _json_dir_l_nd = +[ + { + "@id": "urn:id", + "ex:p": [ + { + "@language": "en-us", + "@value": "v" + } + ] + } +] +; + const _json_dir_l_d = +[ + { + "@id": "urn:id", + "ex:p": [ + { + "@direction": "ltr", + "@language": "en-us", + "@value": "v" + } + ] + } +] +; + const _nq_dir_nl_nd = `\ + "v" . +`; + const _nq_dir_l_nd_ls = `\ + "v"@en-us . +`; + const _nq_dir_nl_d_i18n = `\ + "v"^^ . +`; + const _nq_dir_l_d_i18n = `\ + "v"^^ . +`; + describe('fromRDF', () => { it('should emit for invalid N-Quads @language value', async () => { // N-Quads with invalid language tag (too long) @@ -3724,6 +3790,79 @@ _:b0 "[null]"^^ . testNotSafe: true }); }); + + // 'should handle [no] @lang, [no] @dir, rdfDirection=null' + // no tests due to no special N-Quads handling + + // other tests only check that rdfDirection type of input + // tests mixing rdfDirection formats not tested + + it('should handle no @lang, no @dir, rdfDirection=i18n', async () => { + const input = _nq_dir_nl_nd; + const expected = _json_dir_nl_nd; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle no @lang, @dir, rdfDirection=i18n', async () => { + const input = _nq_dir_nl_d_i18n; + const expected = _json_dir_nl_d; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle @lang, no @dir, rdfDirection=i18n', async () => { + const input = _nq_dir_l_nd_ls; + const expected = _json_dir_l_nd; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle @lang, @dir, rdfDirection=i18n', async () => { + const input = _nq_dir_l_d_i18n; + const expected = _json_dir_l_d; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle bad rdfDirection', async () => { + const input = _nq_dir_l_d_i18n; + + await _test({ + type: 'fromRDF', + input, + options: {skipExpansion: true, rdfDirection: 'bogus'}, + exception: 'jsonld.InvalidRdfDirection' + }); + }); }); describe('toRDF', () => { @@ -3933,6 +4072,192 @@ _:b0 "v" . testSafe: true }); }); + + it('should handle no @lang, no @dir, rdfDirection=null', async () => { + const input = _json_dir_nl_nd; + const nq = _nq_dir_nl_nd; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: null}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle no @lang, no @dir, rdfDirection=i18n', async () => { + const input = _json_dir_nl_nd; + const nq = _nq_dir_nl_nd; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle no @lang, @dir, rdfDirection=null', async () => { + const input = _json_dir_nl_d; + const nq = _nq_dir_nl_nd; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: null}, + expected: nq, + eventCodeLog: [ + 'rdfDirection not set' + ], + testNotSafe: true + }); + }); + + it('should handle no @lang, @dir, rdfDirection=i18n', async () => { + const input = _json_dir_nl_d; + const nq = _nq_dir_nl_d_i18n; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle @lang, no @dir, rdfDirection=null', async () => { + const input = _json_dir_l_nd; + const nq = _nq_dir_l_nd_ls; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: null}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle @lang, no @dir, rdfDirection=i18n', async () => { + const input = _json_dir_l_nd; + const nq = _nq_dir_l_nd_ls; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle @lang, @dir, rdfDirection=null', async () => { + const input = _json_dir_l_d; + const nq = _nq_dir_l_nd_ls; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: null}, + expected: nq, + eventCodeLog: [ + 'rdfDirection not set' + ], + testNotSafe: true + }); + }); + + it('should handle @lang, @dir, rdfDirection=i18n', async () => { + const input = _json_dir_l_d; + const nq = _nq_dir_l_d_i18n; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'i18n-datatype'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); + + it('should handle bad rdfDirection', async () => { + const input = _json_dir_l_d; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true, rdfDirection: 'bogus'}, + exception: 'jsonld.InvalidRdfDirection' + }); + }); + + /* eslint-disable-next-line */ + // https://www.w3.org/TR/json-ld/#example-76-expanded-term-definition-with-language-and-direction + // simplified complex context example + const _ctx_dir_input = +{ + "@context": { + "@version": 1.1, + "@language": "ar-EG", + "@direction": "rtl", + "ex": "urn:ex:", + "publisher": {"@id": "ex:publisher", "@direction": null}, + "title": {"@id": "ex:title"}, + "title_en": {"@id": "ex:title", "@language": "en", "@direction": "ltr"} + }, + "publisher": "NULL", + "title": "RTL", + "title_en": "LTR" +} +; + + it('should handle ctx @lang/@dir/rdfDirection=null', async () => { + const input = _ctx_dir_input; + const nq = `\ +_:b0 "NULL"@ar-eg . +_:b0 "LTR"@en . +_:b0 "RTL"@ar-eg . +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: false, rdfDirection: null}, + expected: nq, + eventCodeLog: [ + 'rdfDirection not set', + 'rdfDirection not set' + ], + testNotSafe: true + }); + }); + + it('should handle ctx @lang/@dir/rdfDirection=i18n', async () => { + const input = _ctx_dir_input; + const nq = `\ +_:b0 "NULL"@ar-eg . +_:b0 "LTR"^^ . +_:b0 "RTL"^^ . +`; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: false, rdfDirection: 'i18n-datatype'}, + expected: nq, + eventCodeLog: [], + testSafe: true + }); + }); }); describe('various', () => { diff --git a/tests/test.js b/tests/test.js index f06676af..a0af0eba 100644 --- a/tests/test.js +++ b/tests/test.js @@ -266,6 +266,8 @@ const TEST_TYPES = { // FIXME idRegex: [ // direction (compound-literal) + /fromRdf-manifest#tdi09$/, + /fromRdf-manifest#tdi10$/, /fromRdf-manifest#tdi11$/, /fromRdf-manifest#tdi12$/, ] From ea6d0b325e0a884211fd8b027e430a02ff6ecc4a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Sep 2023 20:50:02 -0400 Subject: [PATCH 160/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8fb39d1..b2046c8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.3.0 - 2023-09-xx +## 8.3.0 - 2023-09-06 ### Added - Emit `toRdf` warning if `@direction` is used and `rdfDirection` is not set. From 7544b47b0ce85671d65b3d162e253269255a0591 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Sep 2023 20:50:07 -0400 Subject: [PATCH 161/181] Release 8.3.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f02dd88c..397331a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.2.2-0", + "version": "8.3.0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From e297249d5f212b32e526aeba30d7a50230f84fb6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Sep 2023 20:51:10 -0400 Subject: [PATCH 162/181] Start 8.3.1-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 397331a5..0f4477e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.3.0", + "version": "8.3.1-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 93e2785f9f208710d29d1029975a112ffdebe45d Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Sep 2023 22:31:19 -0400 Subject: [PATCH 163/181] Handle unset `rdfDirection` the same as `null`. --- CHANGELOG.md | 5 +++++ lib/toRdf.js | 4 ++-- tests/misc.js | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2046c8e..a55d89a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # jsonld ChangeLog +## 8.3.1 - 2023-09-xx + +### Fixed +- Handle unset `rdfDirection` the same as `null`. + ## 8.3.0 - 2023-09-06 ### Added diff --git a/lib/toRdf.js b/lib/toRdf.js index f576f6d8..53f20af4 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -330,7 +330,7 @@ function _objectToRDF( 'jsonld.InvalidRdfDirection', {value: rdfDirection}); } else if('@language' in item) { - if('@direction' in item && rdfDirection === null) { + if('@direction' in item && !rdfDirection) { if(options.eventHandler) { // FIXME: only emit once? _handleEvent({ @@ -351,7 +351,7 @@ function _objectToRDF( object.datatype.value = datatype || RDF_LANGSTRING; object.language = item['@language']; } else { - if('@direction' in item && rdfDirection === null) { + if('@direction' in item && !rdfDirection) { if(options.eventHandler) { // FIXME: only emit once? _handleEvent({ diff --git a/tests/misc.js b/tests/misc.js index a552d7f9..908052cd 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -4101,6 +4101,22 @@ _:b0 "v" . }); }); + it('should handle no @lang, @dir, no rdfDirection', async () => { + const input = _json_dir_nl_d; + const nq = _nq_dir_nl_nd; + + await _test({ + type: 'toRDF', + input, + options: {skipExpansion: true}, + expected: nq, + eventCodeLog: [ + 'rdfDirection not set' + ], + testNotSafe: true + }); + }); + it('should handle no @lang, @dir, rdfDirection=null', async () => { const input = _json_dir_nl_d; const nq = _nq_dir_nl_nd; From 6705026a58a6a6862ba9c22449db379ba1588c28 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Sep 2023 22:35:35 -0400 Subject: [PATCH 164/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a55d89a3..e2a84a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.3.1 - 2023-09-xx +## 8.3.1 - 2023-09-06 ### Fixed - Handle unset `rdfDirection` the same as `null`. From 822af1c12e92dad3ea4d5b4f9b97cc0a7eb30c45 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Sep 2023 22:35:37 -0400 Subject: [PATCH 165/181] Release 8.3.1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f4477e9..24ebcfe2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.3.1-0", + "version": "8.3.1", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From e2f6523b5c01f8d44a198e21e2b206586db4472e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Sep 2023 22:36:28 -0400 Subject: [PATCH 166/181] Start 8.3.2-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 24ebcfe2..d096b402 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.3.1", + "version": "8.3.2-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From d5ad0d91719a2d568658ca175117e23bde05d949 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Dec 2023 00:20:25 -0500 Subject: [PATCH 167/181] Fix `@graph` `@container` term `@context` handling. When a term scoped context is nullified, it also nullifies the container information. This fix gets the `@container` value from the active context. In particular, this is needed for proper `@graph` container handling. --- CHANGELOG.md | 5 +++++ lib/expand.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a84a1d..90921276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # jsonld ChangeLog +## 8.3.2 - 2023-xx-xx + +### Fixed +- Fix handling of a `@graph` `@container` term that has a `null` `@context`. + ## 8.3.1 - 2023-09-06 ### Fixed diff --git a/lib/expand.js b/lib/expand.js index bae49994..dc52fc68 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -856,7 +856,7 @@ async function _expandObject({ }); } - const container = _getContextValue(termCtx, key, '@container') || []; + const container = _getContextValue(activeCtx, key, '@container') || []; if(container.includes('@language') && _isObject(value)) { const direction = _getContextValue(termCtx, key, '@direction'); From 9851c5d8a64fe7b6615dc456474ce15530278e4a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Dec 2023 14:33:25 -0500 Subject: [PATCH 168/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90921276..7c0926a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.3.2 - 2023-xx-xx +## 8.3.2 - 2023-12-06 ### Fixed - Fix handling of a `@graph` `@container` term that has a `null` `@context`. From c7eb16ab0186e37a3992ab9e1f798c434c703fc2 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Dec 2023 14:33:28 -0500 Subject: [PATCH 169/181] Release 8.3.2. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d096b402..73e9018f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.3.2-0", + "version": "8.3.2", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 5a7efc396987c7fd07046c5a5c5aa7b85da0cd4f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Dec 2023 16:07:47 -0500 Subject: [PATCH 170/181] Start 8.3.3-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73e9018f..d1e84e37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.3.2", + "version": "8.3.3-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 5367858d28b6200aaf832d93eb666d4b819d5d4f Mon Sep 17 00:00:00 2001 From: Benjamin Young Date: Tue, 5 Dec 2023 12:23:09 -0500 Subject: [PATCH 171/181] Ignore `test-suites/` directory. It should never be committed as it contains upstream test suite content. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9c86ac7a..ebfaef10 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ npm-debug.log package-lock.json tests/webidl/*-new v8.log +test-suites/ From d8696b6982bb76ee24838d3d03b41bf36f02766c Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Mon, 2 Oct 2023 15:11:02 -0700 Subject: [PATCH 172/181] Added URL to context resolution error message. --- CHANGELOG.md | 5 +++++ lib/ContextResolver.js | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0926a8..87a652b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # jsonld ChangeLog +## 8.3.3 - xxxx-xx-xx + +### Added +- Added URL to context resolution error message. + ## 8.3.2 - 2023-12-06 ### Fixed diff --git a/lib/ContextResolver.js b/lib/ContextResolver.js index e70ba98a..8a3729a7 100644 --- a/lib/ContextResolver.js +++ b/lib/ContextResolver.js @@ -171,8 +171,8 @@ module.exports = class ContextResolver { } } catch(e) { throw new JsonLdError( - 'Dereferencing a URL did not result in a valid JSON-LD object. ' + - 'Possible causes are an inaccessible URL perhaps due to ' + + 'Dereferencing the URL ' + url + ' did not result in a valid JSON-LD ' + + 'object. Possible causes are an inaccessible URL perhaps due to ' + 'a same-origin policy (ensure the server uses CORS if you are ' + 'using client-side JavaScript), too many redirects, a ' + 'non-JSON response, or more than one HTTP Link Header was ' + @@ -184,8 +184,8 @@ module.exports = class ContextResolver { // ensure ctx is an object if(!_isObject(context)) { throw new JsonLdError( - 'Dereferencing a URL did not result in a JSON object. The ' + - 'response was valid JSON, but it was not a JSON object.', + 'Dereferencing the URL ' + url + ' did not result in a JSON object. ' + + 'The response was valid JSON, but it was not a JSON object.', 'jsonld.InvalidUrl', {code: 'invalid remote context', url}); } From 7cf70fb5b64d06ab724b39641e92336f05a9c64f Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Mon, 2 Oct 2023 15:17:34 -0700 Subject: [PATCH 173/181] Update lib/ContextResolver.js Co-authored-by: Dave Longley --- lib/ContextResolver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ContextResolver.js b/lib/ContextResolver.js index 8a3729a7..ef105da2 100644 --- a/lib/ContextResolver.js +++ b/lib/ContextResolver.js @@ -184,7 +184,7 @@ module.exports = class ContextResolver { // ensure ctx is an object if(!_isObject(context)) { throw new JsonLdError( - 'Dereferencing the URL ' + url + ' did not result in a JSON object. ' + + `Dereferencing the URL "${url}" did not result in a JSON object. ` + 'The response was valid JSON, but it was not a JSON object.', 'jsonld.InvalidUrl', {code: 'invalid remote context', url}); } From 8c166112de9983ecc3b9ff5a039c3069e84d3bf0 Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Mon, 2 Oct 2023 15:17:38 -0700 Subject: [PATCH 174/181] Update lib/ContextResolver.js Co-authored-by: Dave Longley --- lib/ContextResolver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ContextResolver.js b/lib/ContextResolver.js index ef105da2..10877b7c 100644 --- a/lib/ContextResolver.js +++ b/lib/ContextResolver.js @@ -171,7 +171,7 @@ module.exports = class ContextResolver { } } catch(e) { throw new JsonLdError( - 'Dereferencing the URL ' + url + ' did not result in a valid JSON-LD ' + + `Dereferencing the URL "${url}" did not result in a valid JSON-LD ` + 'object. Possible causes are an inaccessible URL perhaps due to ' + 'a same-origin policy (ensure the server uses CORS if you are ' + 'using client-side JavaScript), too many redirects, a ' + From ccd9065f392879efe7d7d95068b7d2004720c754 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 21 Dec 2024 15:46:17 -0500 Subject: [PATCH 175/181] Put URL at end of messages. --- lib/ContextResolver.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/ContextResolver.js b/lib/ContextResolver.js index 10877b7c..29579ac6 100644 --- a/lib/ContextResolver.js +++ b/lib/ContextResolver.js @@ -171,12 +171,13 @@ module.exports = class ContextResolver { } } catch(e) { throw new JsonLdError( - `Dereferencing the URL "${url}" did not result in a valid JSON-LD ` + - 'object. Possible causes are an inaccessible URL perhaps due to ' + + 'Dereferencing a URL did not result in a valid JSON-LD object. ' + + 'Possible causes are an inaccessible URL perhaps due to ' + 'a same-origin policy (ensure the server uses CORS if you are ' + 'using client-side JavaScript), too many redirects, a ' + 'non-JSON response, or more than one HTTP Link Header was ' + - 'provided for a remote context.', + 'provided for a remote context. ' + + `URL: "${url}".`, 'jsonld.InvalidUrl', {code: 'loading remote context failed', url, cause: e}); } @@ -184,8 +185,9 @@ module.exports = class ContextResolver { // ensure ctx is an object if(!_isObject(context)) { throw new JsonLdError( - `Dereferencing the URL "${url}" did not result in a JSON object. ` + - 'The response was valid JSON, but it was not a JSON object.', + 'Dereferencing a URL did not result in a JSON object. The ' + + 'response was valid JSON, but it was not a JSON object. ' + + `URL: "${url}".`, 'jsonld.InvalidUrl', {code: 'invalid remote context', url}); } From 3a2610597f92bcbbb9745db643aeeea578298d0c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 21 Dec 2024 16:04:11 -0500 Subject: [PATCH 176/181] Ignore failing tests. --- tests/test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test.js b/tests/test.js index a0af0eba..998647ed 100644 --- a/tests/test.js +++ b/tests/test.js @@ -174,6 +174,7 @@ const TEST_TYPES = { /expand-manifest#tc037$/, /expand-manifest#tc038$/, /expand-manifest#ter54$/, + /expand-manifest#ter56$/, // html /html-manifest#te001$/, @@ -228,6 +229,7 @@ const TEST_TYPES = { /html-manifest#tf002$/, /html-manifest#tf003$/, /html-manifest#tf004$/, + /html-manifest#tf005$/, ] }, fn: 'flatten', @@ -306,6 +308,7 @@ const TEST_TYPES = { /toRdf-manifest#tc037$/, /toRdf-manifest#tc038$/, /toRdf-manifest#ter54$/, + /toRdf-manifest#ter56$/, /toRdf-manifest#tli12$/, /toRdf-manifest#tli14$/, From 2dd36eb7145531e1f612a47f6774ef8752484a95 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 21 Dec 2024 16:05:26 -0500 Subject: [PATCH 177/181] Test on Node.js 22.x. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08332857..e0d16550 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x, 16.x, 18.x, 20.x] + node-version: [14.x, 16.x, 18.x, 20.x, 22.x] steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} From 253cccf4c4725a80e74e55ffe5177d7379267848 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 21 Dec 2024 16:11:22 -0500 Subject: [PATCH 178/181] Update actions. - Update action versions. - Use codecov token. --- .github/workflows/main.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0d16550..37bbc675 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,9 +10,9 @@ jobs: matrix: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install @@ -25,9 +25,9 @@ jobs: matrix: node-version: [14.x, 16.x, 18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install @@ -44,9 +44,9 @@ jobs: node-version: [16.x] bundler: [webpack, browserify] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install @@ -64,9 +64,9 @@ jobs: matrix: node-version: [16.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install @@ -80,9 +80,9 @@ jobs: matrix: node-version: [18.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install @@ -92,7 +92,8 @@ jobs: - name: Generate coverage report run: npm run coverage-ci - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: file: ./coverage/lcov.info fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} From 2619f0e1f95f9719b6081db822d1cfb461b7449a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 21 Dec 2024 16:18:24 -0500 Subject: [PATCH 179/181] Update changelog. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a652b0..02bc075f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # jsonld ChangeLog -## 8.3.3 - xxxx-xx-xx +## 8.3.3 - 2024-12-21 ### Added - Added URL to context resolution error message. From 76e853afdcdcec0b904b008ba86a87bf7eee03a8 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 21 Dec 2024 16:18:25 -0500 Subject: [PATCH 180/181] Release 8.3.3. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1e84e37..9e60e1a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.3.3-0", + "version": "8.3.3", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { From 71656073c2abe14b2538215ec8592cd9548b7a35 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 21 Dec 2024 16:19:18 -0500 Subject: [PATCH 181/181] Start 8.3.4-0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e60e1a1..facc56b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonld", - "version": "8.3.3", + "version": "8.3.4-0", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": {