Skip to content

Allow override of a protected term that uses the same definition. #316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 21, 2019
Merged
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# jsonld ChangeLog

## 1.6.2 - 2019-05-xx

### Fixed
- Allow overriding of protected terms when redefining to the same
definition, modulo the `protected` flag itself.
- Fix type-scoped context application:
- Ensure values and subject references are expanded and compacted
using type-scoped contexts, if available.
- Ensure `@type` values are evaluated against the previous context,
not the type-scoped context.

## 1.6.1 - 2019-05-13

### Fixed
Expand Down
22 changes: 14 additions & 8 deletions lib/compact.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ api.compact = ({

const rval = {};

// revert type scoped terms
activeCtx = activeCtx.revertTypeScopedTerms();
// revert type scoped context
activeCtx = activeCtx.revertTypeScopedContext();

if(options.link && '@id' in element) {
// store linked element
Expand All @@ -161,12 +161,15 @@ api.compact = ({
if(types.length > 1) {
types = Array.from(types).sort();
}
// find all type-scoped contexts based on current context, prior to
// updating it
const typeContext = activeCtx;
for(const type of types) {
const compactedType = api.compactIri(
{activeCtx, iri: type, relativeTo: {vocab: true}});
{activeCtx: typeContext, iri: type, relativeTo: {vocab: true}});

// Use any type-scoped context defined on this value
const ctx = _getContextValue(activeCtx, compactedType, '@context');
const ctx = _getContextValue(typeContext, compactedType, '@context');
if(!_isUndefined(ctx)) {
activeCtx = _processContext({
activeCtx,
Expand All @@ -184,13 +187,16 @@ api.compact = ({

// compact @id and @type(s)
if(expandedProperty === '@id' || expandedProperty === '@type') {
// if using a type-scoped context, resolve type values against previous
// context
const isType = expandedProperty === '@type';
const valueContext = isType ?
(activeCtx.previousContext || activeCtx) : activeCtx;
let compactedValue = _asArray(expandedValue).map(
expandedIri => api.compactIri({
activeCtx,
activeCtx: valueContext,
iri: expandedIri,
relativeTo: {
vocab: expandedProperty === '@type'
}
relativeTo: {vocab: isType}
}));
if(compactedValue.length === 1) {
compactedValue = compactedValue[0];
Expand Down
204 changes: 131 additions & 73 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ api.process = ({
return activeCtx;
}

// track the previous context
const previousContext = activeCtx.previousContext || activeCtx;

// if context is property scoped and there's a previous context, amend it,
// not the current one
if(isPropertyTermScopedContext && activeCtx.previousContext) {
// TODO: consider optimizing to a shallow copy
activeCtx = activeCtx.clone();
activeCtx.previousContext = api.process({
activeCtx: activeCtx.previousContext,
localCtx: ctxs,
options,
isPropertyTermScopedContext
});
return activeCtx;
}

// process each context in order, update active context
// on each iteration to ensure proper caching
let rval = activeCtx;
Expand All @@ -75,15 +92,6 @@ api.process = ({
// update active context to one computed from last iteration
activeCtx = rval;

// get context from cache if available
if(api.cache) {
const cached = api.cache.get(activeCtx, ctx);
if(cached) {
rval = activeCtx = cached;
continue;
}
}

// reset to initial context
if(ctx === null) {
// We can't nullify if there are protected terms and we're
Expand All @@ -102,7 +110,7 @@ api.process = ({
console.warn('WARNING: invalid context nullification');
const oldActiveCtx = activeCtx;
// copy all protected term definitions to fresh initial context
rval = activeCtx = api.getInitialContext(options);
rval = activeCtx = api.getInitialContext(options).clone();
for(const [term, _protected] of
Object.entries(oldActiveCtx.protected)) {
if(_protected) {
Expand All @@ -124,10 +132,23 @@ api.process = ({
'jsonld.SyntaxError',
{code: 'invalid protected mode', context: localCtx, protectedMode});
}
rval = activeCtx = api.getInitialContext(options);
rval = activeCtx = api.getInitialContext(options).clone();
// if context is type-scoped, ensure previous context has been set
if(isTypeScopedContext) {
rval.previousContext = previousContext.clone();
}
continue;
}

// get context from cache if available
if(api.cache) {
const cached = api.cache.get(activeCtx, ctx);
if(cached) {
rval = activeCtx = cached;
continue;
}
}

// dereference @context key if present
if(_isObject(ctx) && '@context' in ctx) {
ctx = ctx['@context'];
Expand All @@ -140,6 +161,9 @@ api.process = ({
'jsonld.SyntaxError', {code: 'invalid local context', context: ctx});
}

// TODO: there is likely a `preivousContext` cloning optimization that
// could be applied here (no need to copy it under certain conditions)

// clone context before updating it
rval = rval.clone();

Expand Down Expand Up @@ -239,7 +263,12 @@ api.process = ({
for(const key in ctx) {
api.createTermDefinition(
rval, ctx, key, defined, options,
isPropertyTermScopedContext, isTypeScopedContext);
isPropertyTermScopedContext);
}

// if context is type-scoped, ensure previous context has been set
if(isTypeScopedContext && !rval.previousContext) {
rval.previousContext = previousContext.clone();
}

// cache result
Expand All @@ -248,6 +277,11 @@ api.process = ({
}
}

// if context is type-scoped, ensure previous context has been set
if(isTypeScopedContext && !rval.previousContext) {
rval.previousContext = previousContext.clone();
}

return rval;
};

Expand All @@ -265,13 +299,10 @@ api.process = ({
* signal a warning.
* @param isPropertyTermScopedContext `true` if `localCtx` is a scoped context
* from a property term.
* @param isTypeScopedContext `true` if `localCtx` is a scoped context
* from a type.
*/
api.createTermDefinition = (
activeCtx, localCtx, term, defined, options,
isPropertyTermScopedContext = false,
isTypeScopedContext = false) => {
isPropertyTermScopedContext = false) => {
if(defined.has(term)) {
// term already defined
if(defined.get(term)) {
Expand Down Expand Up @@ -301,33 +332,11 @@ api.createTermDefinition = (
{code: 'invalid term definition', context: localCtx});
}

// FIXME if(1.1) ... ?
if(activeCtx.protected.hasOwnProperty(term) &&
!isPropertyTermScopedContext) {
const protectedMode = (options && options.protectedMode) || 'error';
if(protectedMode === 'error') {
throw new JsonLdError(
'Invalid JSON-LD syntax; tried to redefine 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.',
'jsonld.SyntaxError',
{code: 'invalid protected mode', context: localCtx, term,
protectedMode});
}
// keep reference to previous mapping for potential `@protected` check
const previousMapping = activeCtx.mappings.get(term);

// remove old mapping
let previousMapping = null;
if(activeCtx.mappings.has(term)) {
if(isTypeScopedContext) {
previousMapping = activeCtx.mappings.get(term);
}
activeCtx.mappings.delete(term);
}

Expand Down Expand Up @@ -362,11 +371,6 @@ api.createTermDefinition = (
// create new mapping
const mapping = {};
activeCtx.mappings.set(term, mapping);
if(isTypeScopedContext) {
activeCtx.hasTypeScopedTerms = true;
mapping.isTypeScopedTerm = true;
mapping.previousMapping = previousMapping;
}
mapping.reverse = false;

// make sure term definition only has expected keywords
Expand Down Expand Up @@ -652,6 +656,33 @@ api.createTermDefinition = (
'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.',
'jsonld.SyntaxError', {code: 'invalid keyword alias', context: localCtx});
}

// FIXME if(1.1) ... ?
if(previousMapping && previousMapping.protected &&
!isPropertyTermScopedContext) {
// force new term to continue to be protected and see if the mappings would
// be equal
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 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.',
'jsonld.SyntaxError',
{code: 'invalid protected mode', context: localCtx, term,
protectedMode});
}
}
};

/**
Expand Down Expand Up @@ -781,7 +812,7 @@ api.getInitialContext = options => {
inverse: null,
getInverse: _createInverseContext,
clone: _cloneActiveContext,
revertTypeScopedTerms: _revertTypeScopedTerms,
revertTypeScopedContext: _revertTypeScopedContext,
protected: {}
};
// TODO: consider using LRU cache instead
Expand Down Expand Up @@ -957,7 +988,10 @@ api.getInitialContext = options => {
child.inverse = null;
child.getInverse = this.getInverse;
child.protected = util.clone(this.protected);
child.revertTypeScopedTerms = this.revertTypeScopedTerms;
if(this.previousContext) {
child.previousContext = this.previousContext.clone();
}
child.revertTypeScopedContext = this.revertTypeScopedContext;
if('@language' in this) {
child['@language'] = this['@language'];
}
Expand All @@ -968,35 +1002,14 @@ api.getInitialContext = options => {
}

/**
* Reverts any type-scoped terms in this active context to their previous
* mappings.
* Reverts any type-scoped context in this active context to the previous
* context.
*/
function _revertTypeScopedTerms() {
// optimization: no type-scoped terms to remove, reuse active context
if(!this.hasTypeScopedTerms) {
function _revertTypeScopedContext() {
if(!this.previousContext) {
return this;
}
// create clone without type scoped terms
const child = this.clone();
const entries = child.mappings.entries();
for(const [term, mapping] of entries) {
if(mapping.isTypeScopedTerm) {
if(mapping.previousMapping) {
child.mappings.set(term, mapping.previousMapping);
if(mapping.previousMapping.protected) {
child.protected[term] = true;
} else {
delete child.protected[term];
}
} else {
child.mappings.delete(term);
if(child.protected[term]) {
delete child.protected[term];
}
}
}
}
return child;
return this.previousContext.clone();
}
};

Expand Down Expand Up @@ -1303,3 +1316,48 @@ function _findContextUrls(input, urls, replace, base) {
}
}
}

function _deepCompare(x1, x2) {
// compare `null` or primitive types directly
if((!(x1 && typeof x1 === 'object')) ||
(!(x2 && typeof x2 === 'object'))) {
return x1 === x2;
}
// x1 and x2 are objects (also potentially arrays)
const x1Array = Array.isArray(x1);
if(x1Array !== Array.isArray(x2)) {
return false;
}
if(x1Array) {
if(x1.length !== x2.length) {
return false;
}
for(let i = 0; i < x1.length; ++i) {
if(!_deepCompare(x1[i], x2[i])) {
return false;
}
}
return true;
}
// x1 and x2 are non-array objects
const k1s = Object.keys(x1);
const k2s = Object.keys(x2);
if(k1s.length !== k2s.length) {
return false;
}
for(const k1 in x1) {
let v1 = x1[k1];
let v2 = x2[k1];
// special case: `@container` can be in any order
if(k1 === '@container') {
if(Array.isArray(v1) && Array.isArray(v2)) {
v1 = v1.slice().sort();
v2 = v2.slice().sort();
}
}
if(!_deepCompare(v1, v2)) {
return false;
}
}
return true;
}
Loading