diff --git a/CHANGELOG.md b/CHANGELOG.md index 01f734a9..ea7a1ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,15 @@ - 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 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 48a9ec08..66369c18 100644 --- a/lib/compact.js +++ b/lib/compact.js @@ -521,7 +521,33 @@ 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]; @@ -545,6 +571,19 @@ 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 +1102,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..bb33d82f 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) { @@ -560,6 +560,21 @@ 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) @@ -595,6 +610,23 @@ 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..87bf0289 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, + 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,27 @@ 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 +961,23 @@ 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..b250d51d 100644 --- a/lib/util.js +++ b/lib/util.js @@ -234,6 +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) */ api.addValue = (subject, property, value, options) => { options = options || {}; @@ -246,6 +248,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 +259,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 +279,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 729a8135..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$/, @@ -55,10 +50,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$/, @@ -142,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$/, @@ -203,9 +182,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 +449,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