Skip to content

Commit

Permalink
Refactor list handling to allow nested lists in 1.1
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensworks committed Mar 16, 2020
1 parent a9d3ac5 commit b5eee80
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 64 deletions.
12 changes: 7 additions & 5 deletions lib/JsonLdParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ export class JsonLdParser extends Transform {
// Check if we had any RDF lists that need to be terminated with an rdf:nil
const listPointer = this.parsingContext.listPointerStack[this.lastDepth];
if (listPointer) {
if ('term' in listPointer) {
this.emit('data', this.util.dataFactory.quad(listPointer.term, this.util.rdfRest, this.util.rdfNil,
// Terminate the list if the had at least one value
if (listPointer.value) {
this.emit('data', this.util.dataFactory.quad(listPointer.value, this.util.rdfRest, this.util.rdfNil,
this.util.getDefaultGraph()));
} else {
this.parsingContext.getUnidentifiedValueBufferSafe(listPointer.listRootDepth)
.push({ predicate: listPointer.initialPredicate, object: this.util.rdfNil, reverse: false });
}

// Add the list id to the id stack, so it can be used higher up in the stack
this.parsingContext.idStack[listPointer.listRootDepth + 1] = [ listPointer.listId ];

this.parsingContext.listPointerStack.splice(this.lastDepth, 1);
}

Expand Down
3 changes: 1 addition & 2 deletions lib/ParsingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ export class ParsingContext {
// Stack of graph overrides when in an @container: @graph
public readonly graphContainerTermStack: ({ [index: string]: RDF.NamedNode | RDF.BlankNode })[];
// Stack of RDF list pointers (for @list)
public readonly listPointerStack
: ({ term: RDF.Term, listRootDepth: number } | { initialPredicate: RDF.Term, listRootDepth: number })[];
public readonly listPointerStack: ({ value?: RDF.Term, listRootDepth: number, listId: RDF.Term })[];
// Stack of active contexts
public readonly contextTree: ContextTree;
// Stack of flags indicating if the node is a literal
Expand Down
12 changes: 8 additions & 4 deletions lib/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,12 @@ export class Util {
if (Array.isArray(value)) {
// We handle arrays at value level so we can emit earlier, so this is handled already when we get here.
// Empty context-based lists are emitted at this place, because our streaming algorithm doesn't detect those.
if ('@list' in Util.getContextValueContainer(context, key) && value.length === 0) {
return [ this.rdfNil ];
if ('@list' in Util.getContextValueContainer(context, key)) {
if (value.length === 0) {
return [ this.rdfNil ];
} else {
return this.parsingContext.idStack[depth + 1] || [];
}
}
await this.validateValueIndexes(value);
return [];
Expand Down Expand Up @@ -359,7 +363,7 @@ export class Util {
// No need to do anything here, this is handled at the deeper level.
return [];
} else if ('@list' in value) {
// No other entries are allow in this value
// No other entries are allowed in this value
if (Object.keys(value).length > 1) {
throw new Error(`Found illegal neighbouring entries next to @set in value: ${JSON.stringify(value)}`);
}
Expand All @@ -371,7 +375,7 @@ export class Util {
if (listValue.length === 0) {
return [ this.rdfNil ];
} else {
return [];
return this.parsingContext.idStack[depth + 1] || [];
}
} else {
// We only have a single list element here, so emit this directly as single element
Expand Down
60 changes: 17 additions & 43 deletions lib/entryhandler/EntryHandlerArrayValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,38 +35,29 @@ export class EntryHandlerArrayValue implements IEntryHandler<boolean> {
// Our value is part of an array

// Determine the list root key
let listRootKey = null;
let listRootKey: string | number | null = null;
let listRootDepth = 0;
for (let i = depth - 2; i > 0; i--) {
const keyOption = keys[i];
if (typeof keyOption === 'string') {
if (typeof keyOption === 'string' || typeof keyOption === 'number') {
listRootDepth = i;
listRootKey = keyOption;
break;
}
}

// Throw an error if we encounter a nested list
if (listRootKey === '@list' ||
(listRootKey !== null && listRootDepth !== depth - 2 && typeof keys[depth - 2] === 'number'
&& '@list' in Util.getContextValueContainer(await parsingContext
.getContext(keys, listRootDepth - depth), listRootKey))) {
throw new Error(`Lists of lists are not supported: '${listRootKey}'`);
}

if (listRootKey !== null) {
// Emit the given objects as list elements
const values = await util.valueToTerm(await parsingContext.getContext(keys),
listRootKey, value, depth, keys);
<string> listRootKey, value, depth, keys);
for (const object of values) {
await this.handleListElement(parsingContext, util, object, depth, keys.slice(0, listRootDepth), listRootDepth,
listRootKey, keys);
await this.handleListElement(parsingContext, util, object, depth,
keys.slice(0, listRootDepth), listRootDepth);
}

// If no values were found, emit a falsy list element to force an empty RDF list to be emitted.
if (values.length === 0) {
await this.handleListElement(parsingContext, util, null, depth, keys.slice(0, listRootDepth), listRootDepth,
listRootKey, keys);
await this.handleListElement(parsingContext, util, null, depth, keys.slice(0, listRootDepth), listRootDepth);
}
}
} else if (parentKey === '@set') {
Expand All @@ -82,14 +73,12 @@ export class EntryHandlerArrayValue implements IEntryHandler<boolean> {
// Emit the given objects as list elements
const values = await util.valueToTerm(await parsingContext.getContext(keys), parentKey, value, depth, keys);
for (const object of values) {
await this.handleListElement(parsingContext, util, object, depth, keys.slice(0, -1), depth - 1,
parentKey, keys);
await this.handleListElement(parsingContext, util, object, depth, keys.slice(0, -1), depth - 1);
}

// If no values were found, emit a falsy list element to force an empty RDF list to be emitted.
if (values.length === 0) {
await this.handleListElement(parsingContext, util, null, depth, keys.slice(0, -1), depth - 1,
parentKey, keys);
await this.handleListElement(parsingContext, util, null, depth, keys.slice(0, -1), depth - 1);
}
} else {
// Copy the stack values up one level so that the next job can access them.
Expand All @@ -105,49 +94,34 @@ export class EntryHandlerArrayValue implements IEntryHandler<boolean> {
}

protected async handleListElement(parsingContext: ParsingContext, util: Util, value: RDF.Term | null, depth: number,
listRootKeys: string[], listRootDepth: number, listRootKey: string, keys: any[]) {
// Buffer our value as an RDF list using the listRootKey as predicate
listRootKeys: string[], listRootDepth: number) {
// Buffer our value as an RDF list using the listRootKey as predicate
let listPointer = parsingContext.listPointerStack[depth];

if (value) {
if (!listPointer || !('term' in listPointer)) {
if (!listPointer || !listPointer.value) {
const linkTerm: RDF.BlankNode = util.dataFactory.blankNode();
const listRootContext = await parsingContext.getContext(listRootKeys, 0);
const predicate = await util.predicateToTerm(listRootContext, listRootKey);
const reverse = Util.isPropertyReverse(listRootContext, listRootKey, keys[listRootDepth - 1]);

// Lists are not allowed in @reverse'd properties
if (reverse && !parsingContext.allowSubjectList) {
throw new Error(`Found illegal list value in subject position at ${listRootKey}`);
}

if (predicate) {
parsingContext.getUnidentifiedValueBufferSafe(listRootDepth)
.push({predicate, object: linkTerm, reverse});
}
listPointer = { term: linkTerm, listRootDepth };
listPointer = { value: linkTerm, listRootDepth, listId: linkTerm };
} else {
// rdf:rest links are always emitted before the next element,
// as the blank node identifier is only created at that point.
// Because of this reason, the final rdf:nil is emitted when the stack depth is decreased.
const newLinkTerm: RDF.Term = util.dataFactory.blankNode();
parsingContext.emitQuad(depth, util.dataFactory.quad(listPointer.term, util.rdfRest, newLinkTerm,
parsingContext.emitQuad(depth, util.dataFactory.quad(listPointer.value, util.rdfRest, newLinkTerm,
util.getDefaultGraph()));

// Update the list pointer for the next element
listPointer.term = newLinkTerm;
listPointer.value = newLinkTerm;
}

// Emit a list element for the current value
parsingContext.emitQuad(depth, util.dataFactory.quad(<RDF.Term> listPointer.term, util.rdfFirst, value,
parsingContext.emitQuad(depth, util.dataFactory.quad(<RDF.Term> listPointer.value, util.rdfFirst, value,
util.getDefaultGraph()));
} else {
// A falsy list element if found.
// Just enable the list flag for this depth if it has not been set before.
// Mark it as an rdf:nil list until another valid list element comes in
if (!listPointer) {
const predicate = await util.predicateToTerm(await parsingContext.getContext(listRootKeys), listRootKey);
// Predicate won't be falsy because otherwise listPointer would be falsy as well.
listPointer = { initialPredicate: <RDF.Term> predicate, listRootDepth };
listPointer = { listRootDepth, listId: util.rdfNil };
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/entryhandler/EntryHandlerPredicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
// In that case we just emit it as an RDF list with a single element.
const listValueContainer = '@list' in Util.getContextValueContainer(context, key);
if (listValueContainer || value['@list']) {
if ((listValueContainer || (value['@list'] && !Array.isArray(value['@list'])))
if (((listValueContainer && !Array.isArray(value) && !value['@list'])
|| (value['@list'] && !Array.isArray(value['@list'])))
&& object !== util.rdfNil) {
const listPointer: RDF.Term = util.dataFactory.blankNode();
parsingContext.emitQuad(depth, util.dataFactory.quad(listPointer, util.rdfRest, util.rdfNil,
Expand Down
Loading

0 comments on commit b5eee80

Please sign in to comment.