From d9a9efe194a86086c87c4026a334b5c0d9ebd8ac Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 15 Dec 2021 00:54:06 -0500 Subject: [PATCH 01/51] 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 27d40c1d656f73b3cb1b666a6c23e3d6f0f05b04 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 15 Dec 2021 12:36:15 -0500 Subject: [PATCH 02/51] 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 e37991f2f9c364ed9cbe01e691078f34f2fc9884 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 00:06:09 -0400 Subject: [PATCH 03/51] 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 a05a9264c23236c3f73d04a5b730a1afc4f50f1e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 21:17:44 -0500 Subject: [PATCH 04/51] 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 3d33a3ebd945b5978a8f01a725f6f7e9fd0b177e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 21:25:17 -0500 Subject: [PATCH 05/51] 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 b5c8847e9e9e222a374d75e56f70fba449cef72e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 22 Feb 2020 23:18:29 -0500 Subject: [PATCH 06/51] 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 8df3db0b51f0c3b0decefe5259b983e081158356 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 24 Feb 2020 13:15:31 -0500 Subject: [PATCH 07/51] 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 f2a0ba78a63f4fbd831cda43d09cff7e43298f43 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 24 Feb 2020 13:22:17 -0500 Subject: [PATCH 08/51] 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 ccaec147aef604766e78b17f7ee7b461160d777b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 01:01:21 -0400 Subject: [PATCH 09/51] 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 75e9cbe04e520244267d36f0aeaf70124c215b54 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 01:01:57 -0400 Subject: [PATCH 10/51] 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 78c29c48e93eac97f243b99ead252c8c8c22f81a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 02:22:59 -0400 Subject: [PATCH 11/51] 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 62874cc2ecd61946c8fcd05f773a762fe4b22660 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 24 Mar 2022 02:49:47 -0400 Subject: [PATCH 12/51] 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 bfd180f32e2446f092f1687bbdc9cb092e5aa067 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 21:02:28 -0400 Subject: [PATCH 13/51] 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 2e04ea315c1e16d3f54c4ca26c1e50277d9f26bf Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 22:50:19 -0400 Subject: [PATCH 14/51] 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 4f5eddf3c4091c16906f66076313241e1f9d765a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 22:51:10 -0400 Subject: [PATCH 15/51] 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 e35c1ff92c274803017341a8eb724c13d060ddc0 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 25 Mar 2022 23:14:45 -0400 Subject: [PATCH 16/51] 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 399e0e2fc41fb8220dcdc88709f22779b4549610 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:09:03 -0400 Subject: [PATCH 17/51] 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 2d389929182b3d5fb3c98c1a116e69f9cb752b8a Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:09:49 -0400 Subject: [PATCH 18/51] 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 9ddd16e68442ef9f5fb823e734249a501eb4d229 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:10:30 -0400 Subject: [PATCH 19/51] 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 c4081942975d5cb6f75cf691416fd5c7a232fe2c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 1 Apr 2022 22:25:20 -0400 Subject: [PATCH 20/51] 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 5322f093699633e4e0b46af4071e348f4f50570c Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 2 Apr 2022 17:39:38 -0400 Subject: [PATCH 21/51] 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 317e3309e18866016d24c9295f7fd65b203207db Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 2 Apr 2022 18:41:54 -0400 Subject: [PATCH 22/51] 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 c6eb0fe23e2515ad288905c8074bf8f3ccee087e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sun, 3 Apr 2022 01:28:16 -0400 Subject: [PATCH 23/51] 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 f53c907078d88cae16623f2bb16c611b0cb7247e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sun, 3 Apr 2022 01:29:08 -0400 Subject: [PATCH 24/51] 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 d6ac40b7733d3292810f5e80cfe6d80c4e8fb51e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Mon, 4 Apr 2022 16:08:25 -0400 Subject: [PATCH 25/51] 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 0f981aa889a2be3885941812e68d3bbc543cdb38 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 15:40:20 -0400 Subject: [PATCH 26/51] 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 9cfa81278f30f665ae0b55ac5cc5d470b39d3e17 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 21:05:20 -0400 Subject: [PATCH 27/51] 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 06eb166daf6554b31ec16ef3fe292c8dcee5d56e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 21:09:42 -0400 Subject: [PATCH 28/51] 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 5720351a2a1a33726face47e6365808e84d07afd Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 22:09:47 -0400 Subject: [PATCH 29/51] 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 92716dd2ba75d408c6731a0a74d8918882697337 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 22:10:23 -0400 Subject: [PATCH 30/51] 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 27e8b2dbc784078d7eb84b9fa1c0f19dd58047d6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 22:23:13 -0400 Subject: [PATCH 31/51] 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 d79283dc4f6b39d84f65dafa7462791bc8f804e4 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 23:32:02 -0400 Subject: [PATCH 32/51] 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 b6c88f3e21812feba03a1bc4029a7a476398891b Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 5 Apr 2022 23:39:46 -0400 Subject: [PATCH 33/51] 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 475e22d351ad7cc12e87a9abb61f9cd0746837cf Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 6 Apr 2022 00:27:58 -0400 Subject: [PATCH 34/51] 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 9abe338fc9ec7cdf3b9841f9199efa22fefe8e37 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 8 Apr 2022 01:53:46 -0400 Subject: [PATCH 35/51] 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 911c5a757f16a9cc9ff353fd546c801abe9852de Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 12 Apr 2022 00:34:18 -0400 Subject: [PATCH 36/51] 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 7ab1749db59d6212205f17e81c57ddc9a376852e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 12 Apr 2022 00:56:52 -0400 Subject: [PATCH 37/51] 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 2a35d621fa114f797bde6d0318e3fdb52fc69832 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 12 Apr 2022 01:06:49 -0400 Subject: [PATCH 38/51] 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 ad39494bc60d2c2e94ad1a1e8f29815bc9bcc29e Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 20:28:59 -0400 Subject: [PATCH 39/51] 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 13de319c7d6e70fe2cee579215f6fd5535514079 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:07:41 -0400 Subject: [PATCH 40/51] 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 923b4e31693b84ed0008cb30eefbd6ecdae002f1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:31:33 -0400 Subject: [PATCH 41/51] 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 72f2ac3a8c52a89c07975f73479da74128c96dc1 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:45:05 -0400 Subject: [PATCH 42/51] 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 27ef24cca030bc59fb7c97298b7a67a912aae4d6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Thu, 4 Aug 2022 23:53:01 -0400 Subject: [PATCH 43/51] 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 006aa104148d826848d57fdc5de3761a5ff98e84 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 5 Aug 2022 00:06:21 -0400 Subject: [PATCH 44/51] 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 15b794ef1c7dfa98078582ec29867db030e89224 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 5 Aug 2022 00:31:39 -0400 Subject: [PATCH 45/51] 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 c0be80e82d82838c12f95ed7b9ad75f11d0ef086 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 5 Aug 2022 00:32:10 -0400 Subject: [PATCH 46/51] 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 52530f8ee961e45496410d164c31732aacc48303 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 6 Aug 2022 01:21:24 -0400 Subject: [PATCH 47/51] 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 f0eff29f44d5e5c5aa35113be1c56da8b3b97ba3 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 6 Aug 2022 01:29:16 -0400 Subject: [PATCH 48/51] 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 9165327c510f69c4708b9c18a66ae8e27757dd99 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Sat, 6 Aug 2022 02:05:21 -0400 Subject: [PATCH 49/51] 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 3c9e39d8dc0cf26c91271238c6a017bd073cc049 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 10 Aug 2022 10:34:23 -0400 Subject: [PATCH 50/51] 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 87585c25635058f695dc2728a1a9dc4cca443eb2 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Wed, 10 Aug 2022 10:57:15 -0400 Subject: [PATCH 51/51] 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