diff --git a/lib/context.js b/lib/context.js index 1a0161b5..5f0de789 100644 --- a/lib/context.js +++ b/lib/context.js @@ -1041,19 +1041,102 @@ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { } } - // prepend vocab - if(relativeTo.vocab && '@vocab' in activeCtx) { - return activeCtx['@vocab'] + value; + // A flag that captures whether the iri being expanded is + // the value for an @type + let typeExpansion = false; + + if(options !== undefined && options.typeExpansion !== undefined) { + typeExpansion = options.typeExpansion; } - // prepend base - if(relativeTo.base && '@base' in activeCtx) { - if(activeCtx['@base']) { - // The null case preserves value as potentially relative - return prependBase(prependBase(options.base, activeCtx['@base']), value); + 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, + }, + activeCtx, + options + }); + + } + if(expansionMapResult !== undefined) { + value = expansionMapResult; + } else { + // the null case preserves value as potentially relative + value = prependedResult; } } else if(relativeTo.base) { - return prependBase(options.base, value); + // prepend base + let prependedResult; + let expansionMapResult; + let base; + if('@base' in activeCtx) { + if(activeCtx['@base']) { + base = prependBase(options.base, activeCtx['@base']); + prependedResult = prependBase(base, value); + } else { + base = activeCtx['@base']; + prependedResult = value; + } + } else { + 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, + }, + activeCtx, + options + }); + } + if(expansionMapResult !== undefined) { + value = expansionMapResult; + } else { + // 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, + options + }); + if(expandedResult !== undefined) { + value = expandedResult; + } } return value; diff --git a/lib/expand.js b/lib/expand.js index 44c5102f..737def7b 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -75,6 +75,10 @@ api.expand = async ({ typeScopedContext = null, expansionMap = () => undefined }) => { + + // add expansion map to the processing options + options = {...options, expansionMap}; + // nothing to expand if(element === null || element === undefined) { return null; @@ -420,11 +424,14 @@ async function _expandObject({ const nests = []; let unexpandedValue; + // add expansion map to the processing options + options = {...options, expansionMap}; + // Figure out if this is the type for a JSON literal const isJsonType = element[typeKey] && _expandIri(activeCtx, (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), - {vocab: true}, options) === '@json'; + {vocab: true}, {...options, typeExpansion: true}) === '@json'; for(const key of keys) { let value = element[key]; @@ -520,7 +527,8 @@ async function _expandObject({ value = Object.fromEntries(Object.entries(value).map(([k, v]) => [ _expandIri(typeScopedContext, k, {vocab: true}), _asArray(v).map(vv => - _expandIri(typeScopedContext, vv, {base: true, vocab: true}) + _expandIri(typeScopedContext, vv, {base: true, vocab: true}, + {...options, typeExpansion: true}) ) ])); } @@ -530,7 +538,8 @@ async function _expandObject({ _asArray(value).map(v => _isString(v) ? _expandIri(typeScopedContext, v, - {base: true, vocab: true}, options) : v), + {base: true, vocab: true}, + {...options, typeExpansion: true}) : v), {propertyIsArray: options.isFrame}); continue; } @@ -930,7 +939,8 @@ function _expandValue({activeCtx, activeProperty, value, options}) { if(expandedProperty === '@id') { return _expandIri(activeCtx, value, {base: true}, options); } else if(expandedProperty === '@type') { - return _expandIri(activeCtx, value, {vocab: true, base: true}, options); + return _expandIri(activeCtx, value, {vocab: true, base: true}, + {...options, typeExpansion: true}); } // get type definition from context diff --git a/lib/util.js b/lib/util.js index 77da8f61..1458005a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -130,7 +130,7 @@ api.parseLinkHeader = header => { while((match = REGEX_LINK_HEADER_PARAMS.exec(params))) { result[match[1]] = (match[2] === undefined) ? match[3] : match[2]; } - const rel = result['rel'] || ''; + const rel = result.rel || ''; if(Array.isArray(rval[rel])) { rval[rel].push(result); } else if(rval.hasOwnProperty(rel)) { diff --git a/tests/misc.js b/tests/misc.js index 1c3758e0..d9dae4fc 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -478,3 +478,531 @@ describe('literal JSON', () => { }); }); }); + +describe('expansionMap', () => { + describe('unmappedProperty', () => { + it('should be called on unmapped term', async () => { + const docWithUnMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + definedTerm: "is defined", + testUndefined: "is undefined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.unmappedProperty === 'testUndefined') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on nested unmapped term', async () => { + const docWithUnMappedTerm = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + definedTerm: { + testUndefined: "is undefined" + } + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.unmappedProperty === 'testUndefined') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithUnMappedTerm, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + }); + + describe('relativeIri', () => { + it('should be called on relative iri for id term', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + '@id': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for id term (nested)', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + '@id': "urn:absoluteIri", + definedTerm: { + '@id': "relativeiri" + } + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for aliased id term', async () => { + const docWithRelativeIriId = { + '@context': { + 'id': '@id', + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for type term', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for type\ + term in scoped context', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedType': { + '@id': 'https://example.com#definedType', + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + + } + } + }, + 'id': "urn:absoluteiri", + '@type': "definedType", + definedTerm: { + '@type': 'relativeiri' + } + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for \ + type term with multiple relative iri types', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': ["relativeiri", "anotherRelativeiri" ], + definedTerm: "is defined" + }; + + let expansionMapCalledTimes = 0; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri' || + info.relativeIri === 'anotherRelativeiri') { + expansionMapCalledTimes++; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalledTimes, 3); + }); + + it('should be called on relative iri for \ + type term with multiple relative iri types in scoped context', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedType': { + '@id': 'https://example.com#definedType', + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + + } + } + }, + 'id': "urn:absoluteiri", + '@type': "definedType", + definedTerm: { + '@type': ["relativeiri", "anotherRelativeiri" ] + } + }; + + let expansionMapCalledTimes = 0; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri' || + info.relativeIri === 'anotherRelativeiri') { + expansionMapCalledTimes++; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalledTimes, 3); + }); + + it('should be called on relative iri for \ + type term with multiple types', async () => { + const docWithRelativeIriId = { + '@context': { + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + '@type': ["relativeiri", "definedTerm" ], + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it('should be called on relative iri for aliased type term', async () => { + const docWithRelativeIriId = { + '@context': { + 'type': "@type", + 'definedTerm': 'https://example.com#definedTerm' + }, + 'id': "urn:absoluteiri", + 'type': "relativeiri", + definedTerm: "is defined" + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === 'relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when \ + @base value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@base": "./", + }, + '@id': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when \ + @base value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@base": "./", + }, + '@id': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called on relative iri when \ + @vocab value is './'", async () => { + const docWithRelativeIriId = { + '@context': { + "@vocab": "./", + }, + '@type': "relativeiri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.relativeIri === '/relativeiri') { + expansionMapCalled = true; + } + }; + + await jsonld.expand(docWithRelativeIriId, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + }); + + describe('prependedIri', () => { + it("should be called when property is \ + being expanded with `@vocab`", async () => { + const doc = { + '@context': { + "@vocab": "http://example.com/", + }, + 'term': "termValue", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + assert.deepStrictEqual(info.prependedIri, { + type: '@vocab', + vocab: 'http://example.com/', + value: 'term', + typeExpansion: false, + result: 'http://example.com/term' + }); + expansionMapCalled = true; + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when '@type' is \ + being expanded with `@vocab`", async () => { + const doc = { + '@context': { + "@vocab": "http://example.com/", + }, + '@type': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + assert.deepStrictEqual(info.prependedIri, { + type: '@vocab', + vocab: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when aliased '@type' is \ + being expanded with `@vocab`", async () => { + const doc = { + '@context': { + "@vocab": "http://example.com/", + "type": "@type" + }, + 'type': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + assert.deepStrictEqual(info.prependedIri, { + type: '@vocab', + vocab: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when '@id' is being \ + expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + }, + '@id': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: false, + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when aliased '@id' \ + is being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + "id": "@id" + }, + 'id': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: false, + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when '@type' is \ + being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + }, + '@type': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + + it("should be called when aliased '@type' is \ + being expanded with `@base`", async () => { + const doc = { + '@context': { + "@base": "http://example.com/", + "type": "@type" + }, + 'type': "relativeIri", + }; + + let expansionMapCalled = false; + const expansionMap = info => { + if(info.prependedIri) { + assert.deepStrictEqual(info.prependedIri, { + type: '@base', + base: 'http://example.com/', + value: 'relativeIri', + typeExpansion: true, + result: 'http://example.com/relativeIri' + }); + expansionMapCalled = true; + } + }; + + await jsonld.expand(doc, {expansionMap}); + + assert.equal(expansionMapCalled, true); + }); + }); +}); diff --git a/tests/test-common.js b/tests/test-common.js index b9982182..9cc520c7 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -398,7 +398,7 @@ function addManifest(manifest, parent) { */ function addTest(manifest, test, tests) { // expand @id and input base - const test_id = test['@id'] || test['id']; + const test_id = test['@id'] || test.id; //var number = test_id.substr(2); test['@id'] = manifest.baseIri + @@ -958,10 +958,10 @@ function createDocumentLoader(test) { } // If not JSON-LD, alternate may point there - if(linkHeaders['alternate'] && - linkHeaders['alternate'].type == 'application/ld+json' && + if(linkHeaders.alternate && + linkHeaders.alternate.type == 'application/ld+json' && !(contentType || '').match(/^application\/(\w*\+)?json$/)) { - doc.documentUrl = prependBase(url, linkHeaders['alternate'].target); + doc.documentUrl = prependBase(url, linkHeaders.alternate.target); } } }