From 873d702971583f700b96ee2fe93cf752db678138 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 14 Nov 2019 16:13:16 -0800 Subject: [PATCH 1/3] * Indexing on `@type` requires `@type` to be either `@id` or `@vocab`, and defaults to `@id`. --- CHANGELOG.md | 2 ++ lib/compact.js | 14 +++++++++++++- lib/context.js | 14 ++++++++++++++ tests/test-common.js | 10 ---------- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f734a9..f3528fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Expanding the value of a graph container which is already a graph object generates a recursive graph object. - Compacting multiple nodes in a graph container places them in `@included`. +- Indexing on "@type" requires "@type" to be either "@id" or "@vocab", + and defaults to "@id". ### Changed - Default processing mode changed to json-ld-1.1. Allows a 1.1 context to be diff --git a/lib/compact.js b/lib/compact.js index 48a9ec08..d06a72a7 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -545,6 +545,18 @@ api.compact = async ({ compactedItem[typeKey] = types; break; } + + // If compactedItem contains a single entry + // whose key maps to @id, recompact without @type + if(Object.keys(compactedItem).length === 1 && '@id' in expandedItem) { + compactedItem = await api.compact({ + activeCtx, + activeProperty: itemActiveProperty, + element: {'@id': expandedItem['@id']}, + options, + compactionMap + }); + } } // if compacting this value which has no key, index on @none @@ -1063,7 +1075,7 @@ function _selectTerm( // determine prefs for @id based on whether or not value compacts to a term if((typeOrLanguageValue === '@id' || typeOrLanguageValue === '@reverse') && - _isSubjectReference(value)) { + _isObject(value) && '@id' in value) { // prefer @reverse first if(typeOrLanguageValue === '@reverse') { prefs.push('@reverse'); diff --git a/lib/context.js b/lib/context.js index e151fb47..cd4d9b42 100644 --- a/lib/context.js +++ b/lib/context.js @@ -560,6 +560,20 @@ api.createTermDefinition = ( // otherwise, container may also include @set isValid &= container.length <= (hasSet ? 2 : 1); } + + if(container.includes('@type')) { + // If mapping does not have an @type, + // set it to @id + mapping['@type'] = mapping['@type'] || '@id'; + + // type mapping must be either @id or @vocab + if(!['@id', '@vocab'].includes(mapping['@type'])) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; container: @type requires @type to be @id or @vocab.', + 'jsonld.SyntaxError', + {code: 'invalid type mapping', context: localCtx}); + } + } } else { // in JSON-LD 1.0, container must not be an array (it must be a string, // which is one of the validContainers) diff --git a/tests/test-common.js b/tests/test-common.js index 729a8135..4cf0b225 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -55,10 +55,6 @@ const TEST_TYPES = { /compact-manifest.jsonld#tin03$/, /compact-manifest.jsonld#tin04$/, /compact-manifest.jsonld#tin05$/, - // index on @type - /compact-manifest.jsonld#tm020$/, - /compact-manifest.jsonld#tm021$/, - /compact-manifest.jsonld#tm022$/, // context values /compact-manifest.jsonld#ts001$/, /compact-manifest.jsonld#ts002$/, @@ -203,9 +199,6 @@ const TEST_TYPES = { /expand-manifest.jsonld#tin07$/, /expand-manifest.jsonld#tin08$/, /expand-manifest.jsonld#tin09$/, - // index on @type - /expand-manifest.jsonld#tm017$/, - /expand-manifest.jsonld#tm020$/, // @nest /expand-manifest.jsonld#tn008$/, // keywords @@ -473,9 +466,6 @@ const TEST_TYPES = { /toRdf-manifest.jsonld#tin04$/, /toRdf-manifest.jsonld#tin05$/, /toRdf-manifest.jsonld#tin06$/, - // index on @type - /toRdf-manifest.jsonld#tm017$/, - /toRdf-manifest.jsonld#tm020$/, // @next /toRdf-manifest.jsonld#tn008$/, // keywords From 408e31b0736bc5706fd1988d4742cb2bea4d4740 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 18 Nov 2019 13:40:15 -0800 Subject: [PATCH 2/3] * Indexing on an arbitrary property, not just `@index`. --- CHANGELOG.md | 5 +++-- lib/compact.js | 26 +++++++++++++++++++++- lib/context.js | 17 ++++++++++++++- lib/expand.js | 52 ++++++++++++++++++++++++++++++++++---------- lib/util.js | 14 +++++++++++- tests/test-common.js | 21 ++---------------- 6 files changed, 100 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3528fb4..ea7a1ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ ### Changed - Default processing mode changed to json-ld-1.1. Allows a 1.1 context to be used after non-1.1 contexts. -- `@vocab` can be relative or a Compact IRI in 1.1, resolved against either a previous `@vocab`, - `@base` or document base. +- Indexing on an arbitrary property, not just "@index". +- `@vocab` can be relative or a Compact IRI in 1.1, resolved against either + a previous `@vocab`, `@base` or document base. - Better checking of absolute IRIs. - Terms that begin with a ':' are not considered absolute or compact IRIs. - Don't use terms with `"@prefix": false` or expanded term definitions to construct compact IRIs. diff --git a/lib/compact.js b/lib/compact.js index d06a72a7..d86e72f5 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -521,7 +521,31 @@ api.compact = async ({ } key = expandedItem['@language']; } else if(container.includes('@index')) { - key = expandedItem['@index']; + const indexKey = _getContextValue(activeCtx, itemActiveProperty, '@index') || '@index'; + const containerKey = api.compactIri({activeCtx, iri: indexKey, vocab: true}); + if(indexKey === '@index') { + key = expandedItem['@index']; + delete compactedItem[containerKey]; + } else { + let others; + [key, ...others] = _asArray(compactedItem[indexKey] || []); + if(!_isString(key)) { + // Will use @none if it isn't a string. + key = null; + } else { + switch(others.length) { + case 0: + delete compactedItem[indexKey]; + break; + case 1: + compactedItem[indexKey] = others[0]; + break; + default: + compactedItem[indexKey] = others; + break; + } + } + } } else if(container.includes('@id')) { const idKey = api.compactIri({activeCtx, iri: '@id', vocab: true}); key = compactedItem[idKey]; diff --git a/lib/context.js b/lib/context.js index cd4d9b42..23273383 100644 --- a/lib/context.js +++ b/lib/context.js @@ -375,7 +375,7 @@ api.createTermDefinition = ( // JSON-LD 1.1 support if(api.processingMode(activeCtx, 1.1)) { - validKeys.push('@context', '@nest', '@prefix', '@protected'); + validKeys.push('@context', '@index', '@nest', '@prefix', '@protected'); } for(const kw in value) { @@ -609,6 +609,21 @@ api.createTermDefinition = ( mapping['@container'] = container; } + // property indexing + if('@index' in value) { + if (!('@container' in value) || !mapping['@container'].includes('@index')) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; @index without @index in @container: ${value["@index"]} on term ${term}.', 'jsonld.SyntaxError', + {code: 'invalid term definition', context: localCtx}); + } + if (!_isString(value['@index']) || value['@index'].indexOf('@') === 0) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; "@index must expand to an IRI: ${value["@index"]} on term ${term}.', 'jsonld.SyntaxError', + {code: 'invalid term definition', context: localCtx}); + } + mapping['@index'] = value['@index'] + } + // scoped contexts if('@context' in value) { mapping['@context'] = value['@context']; diff --git a/lib/expand.js b/lib/expand.js index 91c3d8e0..2b341042 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -610,6 +610,10 @@ async function _expandObject({ } else if(container.includes('@index') && _isObject(value)) { // handle index container (skip if value is not an object) const asGraph = container.includes('@graph'); + const indexKey = _getContextValue(termCtx, key, '@index') || '@index'; + const propertyIndex = indexKey !== '@index' && + _expandIri(activeCtx, indexKey, {vocab: true}, options); + expandedValue = await _expandIndexMap({ activeCtx: termCtx, options, @@ -617,7 +621,8 @@ async function _expandObject({ value, expansionMap, asGraph, - indexKey: '@index' + indexKey: indexKey, + propertyIndex }); } else if(container.includes('@id') && _isObject(value)) { // handle id container (skip if value is not an object) @@ -890,7 +895,7 @@ function _expandLanguageMap(activeCtx, languageMap, options) { async function _expandIndexMap( {activeCtx, options, activeProperty, value, expansionMap, asGraph, - indexKey}) { + indexKey, propertyIndex}) { const rval = []; const keys = Object.keys(value).sort(); const isTypeIndex = indexKey === '@type'; @@ -913,15 +918,6 @@ async function _expandIndexMap( val = [val]; } - // expand for @type, but also for @none - const expandedKey = _expandIri(activeCtx, key, {vocab: true}, options); - if(indexKey === '@id') { - // expand document relative - key = _expandIri(activeCtx, key, {base: true}, options); - } else if(isTypeIndex) { - key = expandedKey; - } - val = await api.expand({ activeCtx, activeProperty, @@ -931,6 +927,26 @@ async function _expandIndexMap( insideIndex: true, expansionMap }); + + // expand for @type, but also for @none + let expandedKey; + if(propertyIndex) { + if(key === '@none') { + expandedKey = '@none' + } else { + expandedKey =_expandValue({activeCtx, activeProperty: indexKey, value: key, options}); + } + } else { + expandedKey = _expandIri(activeCtx, key, {vocab: true}, options); + } + + if(indexKey === '@id') { + // expand document relative + key = _expandIri(activeCtx, key, {base: true}, options); + } else if(isTypeIndex) { + key = expandedKey; + } + for(let item of val) { // If this is also a @graph container, turn items into graphs if(asGraph && !_isGraph(item)) { @@ -944,6 +960,20 @@ async function _expandIndexMap( } else { item['@type'] = [key]; } + } else if(_isValue(item) && !['@language', '@type', '@index'].includes(indexKey)) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; Attempt to add illegal key to value object: ${indexKey}.', + 'jsonld.SyntaxError', + {code: 'invalid value object', value: item}); + } else if(propertyIndex) { + // index is a property to be expanded, and values interpreted for that property + if(expandedKey !== '@none') { + // expand key as a value + _addValue(item, propertyIndex, expandedKey, { + propertyIsArray: true, + prependValue: true + }); + } } else if(expandedKey !== '@none' && !(indexKey in item)) { item[indexKey] = key; } diff --git a/lib/util.js b/lib/util.js index 2baaa2e5..0b83d19d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -234,6 +234,7 @@ api.hasValue = (subject, property, value) => { * an array (lists) (default: false). * [allowDuplicate] true to allow duplicates, false not to (uses a * simple shallow comparison of subject ID or value) (default: true). + * [prependValue] false to prepend value to any existing values. (default: false) */ api.addValue = (subject, property, value, options) => { options = options || {}; @@ -246,6 +247,9 @@ api.addValue = (subject, property, value, options) => { if(!('allowDuplicate' in options)) { options.allowDuplicate = true; } + if(!('prependValue' in options)) { + options.prependValue = false; + } if(options.valueIsArray) { subject[property] = value; @@ -254,6 +258,10 @@ api.addValue = (subject, property, value, options) => { !subject.hasOwnProperty(property)) { subject[property] = []; } + if(options.prependValue) { + value = value.concat(subject[property]); + subject[property] = []; + } for(let i = 0; i < value.length; ++i) { api.addValue(subject, property, value[i], options); } @@ -270,7 +278,11 @@ api.addValue = (subject, property, value, options) => { // add new value if(!hasValue) { - subject[property].push(value); + if(options.prependValue) { + subject[property].unshift(value) + } else { + subject[property].push(value); + } } } else { // add new value as set or single value diff --git a/tests/test-common.js b/tests/test-common.js index 4cf0b225..5781d7f5 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -39,13 +39,8 @@ const TEST_TYPES = { /compact-manifest.jsonld#ttn01$/, /compact-manifest.jsonld#ttn02$/, /compact-manifest.jsonld#ttn03$/, - // property-valued indexes - /compact-manifest.jsonld#tpi01$/, - /compact-manifest.jsonld#tpi02$/, - /compact-manifest.jsonld#tpi03$/, - /compact-manifest.jsonld#tpi04$/, - /compact-manifest.jsonld#tpi05$/, - /compact-manifest.jsonld#tpi06$/, + // IRI confusion + /compact-manifest.jsonld#te002$/, // @propogate /compact-manifest.jsonld#tc026$/, /compact-manifest.jsonld#tc027$/, @@ -138,18 +133,6 @@ const TEST_TYPES = { /expand-manifest.jsonld#thc05$/, // @type: @none /expand-manifest.jsonld#ttn02$/, - // property index maps - /expand-manifest.jsonld#tpi01$/, - /expand-manifest.jsonld#tpi02$/, - /expand-manifest.jsonld#tpi03$/, - /expand-manifest.jsonld#tpi04$/, - /expand-manifest.jsonld#tpi05$/, - /expand-manifest.jsonld#tpi06$/, - /expand-manifest.jsonld#tpi07$/, - /expand-manifest.jsonld#tpi08$/, - /expand-manifest.jsonld#tpi09$/, - /expand-manifest.jsonld#tpi10$/, - /expand-manifest.jsonld#tpi11$/, // misc /expand-manifest.jsonld#te043$/, /expand-manifest.jsonld#te044$/, From 10f4180a1a198182fe8896ad0f4c950cbb1df928 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Tue, 19 Nov 2019 13:38:00 -0500 Subject: [PATCH 3/3] Fix lint and string interpolation issues. --- lib/compact.js | 11 +++++++---- lib/context.js | 15 +++++++++------ lib/expand.js | 16 ++++++++++------ lib/util.js | 5 +++-- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/compact.js b/lib/compact.js index d86e72f5..66369c18 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -521,10 +521,12 @@ api.compact = async ({ } key = expandedItem['@language']; } else if(container.includes('@index')) { - const indexKey = _getContextValue(activeCtx, itemActiveProperty, '@index') || '@index'; - const containerKey = api.compactIri({activeCtx, iri: indexKey, vocab: true}); + const indexKey = _getContextValue( + activeCtx, itemActiveProperty, '@index') || '@index'; + const containerKey = api.compactIri( + {activeCtx, iri: indexKey, vocab: true}); if(indexKey === '@index') { - key = expandedItem['@index']; + key = expandedItem['@index']; delete compactedItem[containerKey]; } else { let others; @@ -572,7 +574,8 @@ api.compact = async ({ // If compactedItem contains a single entry // whose key maps to @id, recompact without @type - if(Object.keys(compactedItem).length === 1 && '@id' in expandedItem) { + if(Object.keys(compactedItem).length === 1 && + '@id' in expandedItem) { compactedItem = await api.compact({ activeCtx, activeProperty: itemActiveProperty, diff --git a/lib/context.js b/lib/context.js index 23273383..bb33d82f 100644 --- a/lib/context.js +++ b/lib/context.js @@ -569,7 +569,8 @@ api.createTermDefinition = ( // type mapping must be either @id or @vocab if(!['@id', '@vocab'].includes(mapping['@type'])) { throw new JsonLdError( - 'Invalid JSON-LD syntax; container: @type requires @type to be @id or @vocab.', + 'Invalid JSON-LD syntax; container: @type requires @type to be ' + + '@id or @vocab.', 'jsonld.SyntaxError', {code: 'invalid type mapping', context: localCtx}); } @@ -611,17 +612,19 @@ api.createTermDefinition = ( // property indexing if('@index' in value) { - if (!('@container' in value) || !mapping['@container'].includes('@index')) { + if(!('@container' in value) || !mapping['@container'].includes('@index')) { throw new JsonLdError( - 'Invalid JSON-LD syntax; @index without @index in @container: ${value["@index"]} on term ${term}.', 'jsonld.SyntaxError', + 'Invalid JSON-LD syntax; @index without @index in @container: ' + + `"${value['@index']}" on term "${term}".`, 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } - if (!_isString(value['@index']) || value['@index'].indexOf('@') === 0) { + if(!_isString(value['@index']) || value['@index'].indexOf('@') === 0) { throw new JsonLdError( - 'Invalid JSON-LD syntax; "@index must expand to an IRI: ${value["@index"]} on term ${term}.', 'jsonld.SyntaxError', + 'Invalid JSON-LD syntax; @index must expand to an IRI: ' + + `"${value['@index']}" on term "${term}".`, 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } - mapping['@index'] = value['@index'] + mapping['@index'] = value['@index']; } // scoped contexts diff --git a/lib/expand.js b/lib/expand.js index 2b341042..87bf0289 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -621,7 +621,7 @@ async function _expandObject({ value, expansionMap, asGraph, - indexKey: indexKey, + indexKey, propertyIndex }); } else if(container.includes('@id') && _isObject(value)) { @@ -932,9 +932,10 @@ async function _expandIndexMap( let expandedKey; if(propertyIndex) { if(key === '@none') { - expandedKey = '@none' + expandedKey = '@none'; } else { - expandedKey =_expandValue({activeCtx, activeProperty: indexKey, value: key, options}); + expandedKey = _expandValue( + {activeCtx, activeProperty: indexKey, value: key, options}); } } else { expandedKey = _expandIri(activeCtx, key, {vocab: true}, options); @@ -960,13 +961,16 @@ async function _expandIndexMap( } else { item['@type'] = [key]; } - } else if(_isValue(item) && !['@language', '@type', '@index'].includes(indexKey)) { + } else if(_isValue(item) && + !['@language', '@type', '@index'].includes(indexKey)) { throw new JsonLdError( - 'Invalid JSON-LD syntax; Attempt to add illegal key to value object: ${indexKey}.', + 'Invalid JSON-LD syntax; Attempt to add illegal key to value ' + + `object: "${indexKey}".`, 'jsonld.SyntaxError', {code: 'invalid value object', value: item}); } else if(propertyIndex) { - // index is a property to be expanded, and values interpreted for that property + // index is a property to be expanded, and values interpreted for that + // property if(expandedKey !== '@none') { // expand key as a value _addValue(item, propertyIndex, expandedKey, { diff --git a/lib/util.js b/lib/util.js index 0b83d19d..b250d51d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -234,7 +234,8 @@ api.hasValue = (subject, property, value) => { * an array (lists) (default: false). * [allowDuplicate] true to allow duplicates, false not to (uses a * simple shallow comparison of subject ID or value) (default: true). - * [prependValue] false to prepend value to any existing values. (default: false) + * [prependValue] false to prepend value to any existing values. + * (default: false) */ api.addValue = (subject, property, value, options) => { options = options || {}; @@ -279,7 +280,7 @@ api.addValue = (subject, property, value, options) => { // add new value if(!hasValue) { if(options.prependValue) { - subject[property].unshift(value) + subject[property].unshift(value); } else { subject[property].push(value); }