Skip to content

Commit

Permalink
Handle embedded contexts with disabled propagation
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensworks committed Mar 16, 2020
1 parent d2fce0e commit 0e6a33d
Show file tree
Hide file tree
Showing 3 changed files with 288 additions and 1 deletion.
40 changes: 39 additions & 1 deletion lib/ParsingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export class ParsingContext {
}

// Determine the closest context
const contextData = await this.contextTree.getContext(keys) || { context: await this.rootContext, depth: 1 };
const contextData = await this.getContextPropagationAware(keys);
let context: IJsonLdContextNormalized = contextData.context;

// Process property-scoped contexts (high-to-low)
Expand Down Expand Up @@ -201,6 +201,44 @@ export class ParsingContext {
return context;
}

/**
* Get the context at the given path.
* Non-propagating contexts will be skipped,
* unless the context at that exact depth is retrieved.
*
* This ONLY takes into account context propagation logic,
* so this should usually not be called directly,
* call {@link #getContext} instead.
*
* @param keys The path of keys to get the context at.
* @return {Promise<{ context: IJsonLdContextNormalized, depth: number }>} A context and its depth.
*/
public async getContextPropagationAware(keys: string[]):
Promise<{ context: IJsonLdContextNormalized, depth: number }> {
const originalDepth = keys.length;
let contextData: { context: IJsonLdContextNormalized, depth: number } | null = null;
do {
// If we had a previous iteration, jump to the parent of context depth.
// We must do this because once we get here, last context had propagation disabled,
// so we check its first parent instead.
if (contextData) {
keys = keys.slice(0, contextData.depth - 1);
}

contextData = await this.contextTree.getContext(keys) || { context: await this.rootContext, depth: 0 };
} while (contextData.depth > 0
&& contextData.context['@propagate'] === false
&& contextData.depth !== originalDepth);

// Special case for root context that does not allow propagation.
// Fallback to empty context in that case.
if (contextData.depth === 0 && contextData.context['@propagate'] === false && contextData.depth !== originalDepth) {
contextData.context = {};
}

return contextData;
}

/**
* Start a new job for parsing the given value.
* @param {any[]} keys The stack of keys.
Expand Down
84 changes: 84 additions & 0 deletions test/JsonLdParser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8015,6 +8015,90 @@ describe('JsonLdParser', () => {

});

describe('embedded contexts', () => {

it('should override a single property', async () => {
const stream = streamifyString(`
{
"@context": {
"@vocab": "http://vocab.org/"
},
"@id": "http://ex.org/myid",
"foo": {
"@context": {
"@vocab": "http://vocab.1.org/"
},
"@id": "http://ex.org/myinnerid",
"bar": "baz"
}
}`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(namedNode('http://ex.org/myid'), namedNode('http://vocab.org/foo'),
namedNode('http://ex.org/myinnerid')),
quad(namedNode('http://ex.org/myinnerid'), namedNode('http://vocab.1.org/bar'),
literal('baz')),
]);
});

it('should override a single property and propagate to children', async () => {
const stream = streamifyString(`
{
"@context": {
"@vocab": "http://vocab.org/"
},
"@id": "http://ex.org/myid",
"foo": {
"@context": {
"@vocab": "http://vocab.1.org/"
},
"@id": "http://ex.org/myinnerid",
"bar": {
"@id": "http://ex.org/myinnerinnerid",
"baz": "buzz"
}
}
}`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(namedNode('http://ex.org/myid'), namedNode('http://vocab.org/foo'),
namedNode('http://ex.org/myinnerid')),
quad(namedNode('http://ex.org/myinnerid'), namedNode('http://vocab.1.org/bar'),
namedNode('http://ex.org/myinnerinnerid')),
quad(namedNode('http://ex.org/myinnerinnerid'), namedNode('http://vocab.1.org/baz'),
literal('buzz')),
]);
});

it('should override a single property and not propagate to children with @propagate: false', async () => {
const stream = streamifyString(`
{
"@context": {
"@vocab": "http://vocab.org/"
},
"@id": "http://ex.org/myid",
"foo": {
"@context": {
"@propagate": false,
"@vocab": "http://vocab.1.org/"
},
"@id": "http://ex.org/myinnerid",
"bar": {
"@id": "http://ex.org/myinnerinnerid",
"baz": "buzz"
}
}
}`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(namedNode('http://ex.org/myid'), namedNode('http://vocab.org/foo'),
namedNode('http://ex.org/myinnerid')),
quad(namedNode('http://ex.org/myinnerid'), namedNode('http://vocab.1.org/bar'),
namedNode('http://ex.org/myinnerinnerid')),
quad(namedNode('http://ex.org/myinnerinnerid'), namedNode('http://vocab.org/baz'),
literal('buzz')),
]);
});

});

describe('scoped contexts', () => {

describe('property scoped contexts', () => {
Expand Down
165 changes: 165 additions & 0 deletions test/ParsingContext-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,171 @@ describe('ParsingContext', () => {
});
});

describe('getContextPropagationAware', () => {

it('should return the root context when no contexts have been set', async () => {
return expect(await parsingContext.getContextPropagationAware(['']))
.toEqual({
context: {
'@vocab': 'http://vocab.org/',
},
depth: 0,
});
});

it('should return the root context when a non-matching context has been set', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({}));
return expect(await parsingContext.getContextPropagationAware(['']))
.toEqual({
context: {
'@vocab': 'http://vocab.org/',
},
depth: 0,
});
});

it('should return a set context', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({ '@vocab': 'http://bla.org/' }));
return expect(await parsingContext.getContextPropagationAware(['', 'a']))
.toEqual({
context: {
'@vocab': 'http://bla.org/',
},
depth: 2,
});
});

it('should propagate to a direct parent', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({ '@vocab': 'http://bla.org/' }));
return expect(await parsingContext.getContextPropagationAware(['', 'a', 'b']))
.toEqual({
context: {
'@vocab': 'http://bla.org/',
},
depth: 2,
});
});

it('should propagate to indirect parents', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({ '@vocab': 'http://bla.org/' }));
return expect(await parsingContext.getContextPropagationAware(['', 'a', 'b', 'c', 'd']))
.toEqual({
context: {
'@vocab': 'http://bla.org/',
},
depth: 2,
});
});

it('should skip a non-propagating parent', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({
'@propagate': false,
'@vocab': 'http://bla.org/',
}));
return expect(await parsingContext.getContextPropagationAware(['', 'a', 'b']))
.toEqual({
context: {
'@vocab': 'http://vocab.org/',
},
depth: 0,
});
});

it('should return a non-propagating context if at that exact depth', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({
'@propagate': false,
'@vocab': 'http://bla.org/',
}));
return expect(await parsingContext.getContextPropagationAware(['', 'a']))
.toEqual({
context: {
'@propagate': false,
'@vocab': 'http://bla.org/',
},
depth: 2,
});
});

it('should skip an indirect non-propagating parent', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({
'@propagate': false,
'@vocab': 'http://bla.org/',
}));
return expect(await parsingContext.getContextPropagationAware(['', 'a', 'b', 'c', 'd']))
.toEqual({
context: {
'@vocab': 'http://vocab.org/',
},
depth: 0,
});
});

it('should skip multiple indirect non-propagating parent', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({
'@propagate': false,
'@vocab': 'http://bla1.org/',
}));
parsingContext.contextTree.setContext(['', 'a', 'b', 'c'], Promise.resolve({
'@propagate': false,
'@vocab': 'http://bla2.org/',
}));
return expect(await parsingContext.getContextPropagationAware(['', 'a', 'b', 'c', 'd']))
.toEqual({
context: {
'@vocab': 'http://vocab.org/',
},
depth: 0,
});
});

it('should ignore non-propagating contexts from above', async () => {
parsingContext.contextTree.setContext(['', 'a'], Promise.resolve({
'@propagate': false,
'@vocab': 'http://bla1.org/',
}));
parsingContext.contextTree.setContext(['', 'a', 'b', 'c'], Promise.resolve({
'@propagate': true,
'@vocab': 'http://bla2.org/',
}));
return expect(await parsingContext.getContextPropagationAware(['', 'a', 'b', 'c', 'd']))
.toEqual({
context: {
'@propagate': true,
'@vocab': 'http://bla2.org/',
},
depth: 4,
});
});

it('should return an empty context when the root is non-propagating', async () => {
parsingContext = new ParsingContextMocked({
context: { '@vocab': 'http://vocab.org/', '@propagate': false },
parser: <any> null,
});
return expect(await parsingContext.getContextPropagationAware(['']))
.toEqual({
context: {},
depth: 0,
});
});

it('should return an empty context when the root is non-propagating unless root is retrieved', async () => {
parsingContext = new ParsingContextMocked({
context: { '@vocab': 'http://vocab.org/', '@propagate': false },
parser: <any> null,
});
return expect(await parsingContext.getContextPropagationAware([]))
.toEqual({
context: {
'@propagate': false,
'@vocab': 'http://vocab.org/',
},
depth: 0,
});
});

});

describe('getContext', () => {

describe('for basic context trees', () => {
Expand Down

0 comments on commit 0e6a33d

Please sign in to comment.