Skip to content

Commit 0f54ecd

Browse files
committed
Handle embedded nodes as RDF-star statements
1 parent 79a5118 commit 0f54ecd

File tree

11 files changed

+556
-45
lines changed

11 files changed

+556
-45
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ Optionally, the following parameters can be set in the `JsonLdParser` constructo
162162
* `normalizeLanguageTags`: Whether or not language tags should be normalized to lowercase. _(Default: `false` for JSON-LD 1.1 (and higher), `true` for JSON-LD 1.0)_
163163
* `streamingProfileAllowOutOfOrderPlainType`: When the streaming profile flag is enabled, `@type` entries MUST come before other properties since they may defined a type-scoped context. However, when this flag is enabled, `@type` entries that do NOT define a type-scoped context may appear anywhere just like a regular property.. _(Default: `false`)_
164164
* `skipContextValidation`: If JSON-LD context validation should be skipped. This is useful when parsing large contexts that are known to be valid. _(Default: `false`)_
165+
* `rdfstar`: If embedded nodes and annotated objects should be parsed according to the [JSON-LD star specification](https://json-ld.github.io/json-ld-star/). _(Default: `true`)_
166+
* `rdfstarReverseInEmbedded`: If embedded nodes in JSON-LD star can have reverse properties. _(Default: `false`)_
165167

166168
```javascript
167169
new JsonLdParser({
@@ -179,6 +181,7 @@ new JsonLdParser({
179181
defaultGraph: namedNode('http://example.org/graph'),
180182
rdfDirection: 'i18n-datatype',
181183
normalizeLanguageTags: true,
184+
rdfstar: true,
182185
});
183186
```
184187

@@ -239,8 +242,9 @@ Other documents will still be parsed correctly as well, with a slightly lower ef
239242

240243
## Streaming Profile
241244

242-
This parser adheres to both the [JSON-LD 1.1](https://www.w3.org/TR/json-ld/) specification
243-
and the [JSON-LD 1.1 Streaming specification](https://w3c.github.io/json-ld-streaming/).
245+
This parser adheres to the [JSON-LD 1.1](https://www.w3.org/TR/json-ld/) specification,
246+
the [JSON-LD 1.1 Streaming](https://w3c.github.io/json-ld-streaming/) specification,
247+
and the [JSON-LD star](https://json-ld.github.io/json-ld-star/) specification.
244248

245249
By default, this parser assumes that JSON-LD document
246250
are *not* in the [streaming document form](https://w3c.github.io/json-ld-streaming/#streaming-document-form).
@@ -261,6 +265,8 @@ This parser implements the following [JSON-LD specifications](https://json-ld.or
261265
* JSON-LD 1.1 - Transform JSON-LD to RDF
262266
* JSON-LD 1.1 - Error handling
263267
* JSON-LD 1.1 - Streaming Transform JSON-LD to RDF
268+
* [JSON-LD star](https://json-ld.github.io/json-ld-star/) - Transform JSON-LD star to RDF
269+
* [JSON-LD star](https://json-ld.github.io/json-ld-star/) - Error handling
264270

265271
## Performance
266272

lib/JsonLdParser.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,13 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
318318
*/
319319
public async flushBuffer(depth: number, keys: any[]) {
320320
let subjects: RDF.Term[] = this.parsingContext.idStack[depth];
321-
if (!subjects) {
321+
const subjectsWasDefined = !!subjects;
322+
if (!subjectsWasDefined) {
322323
subjects = this.parsingContext.idStack[depth] = [ this.util.dataFactory.blankNode() ];
323324
}
324325

325326
// Flush values at this level
326-
const valueBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean }[] =
327+
const valueBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean, isEmbedded: boolean }[] =
327328
this.parsingContext.unidentifiedValuesBuffer[depth];
328329
if (valueBuffer) {
329330
for (const subject of subjects) {
@@ -336,13 +337,7 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
336337
// Flush values to stream if the graph @id is known
337338
this.parsingContext.emittedStack[depth] = true;
338339
for (const bufferedValue of valueBuffer) {
339-
if (bufferedValue.reverse) {
340-
this.parsingContext.emitQuad(depth, this.util.dataFactory.quad(
341-
bufferedValue.object, bufferedValue.predicate, subject, graph));
342-
} else {
343-
this.parsingContext.emitQuad(depth, this.util.dataFactory.quad(
344-
subject, bufferedValue.predicate, bufferedValue.object, graph));
345-
}
340+
this.util.emitQuadChecked(depth, subject, bufferedValue.predicate, bufferedValue.object, graph, bufferedValue.reverse, bufferedValue.isEmbedded);
346341
}
347342
}
348343
} else {
@@ -355,12 +350,14 @@ export class JsonLdParser extends Transform implements RDF.Sink<EventEmitter, RD
355350
object: subject,
356351
predicate: bufferedValue.predicate,
357352
subject: bufferedValue.object,
353+
isEmbedded: bufferedValue.isEmbedded,
358354
});
359355
} else {
360356
subGraphBuffer.push({
361357
object: bufferedValue.object,
362358
predicate: bufferedValue.predicate,
363359
subject,
360+
isEmbedded: bufferedValue.isEmbedded,
364361
});
365362
}
366363
}
@@ -638,8 +635,13 @@ export interface IJsonLdParserOptions {
638635
*/
639636
skipContextValidation?: boolean;
640637
/**
641-
* If nested triples should be parsed according to the JSON-LD star specification.
638+
* If embedded nodes and annotated objects should be parsed according to the JSON-LD star specification.
642639
* Defaults to true
643640
*/
644641
rdfstar?: boolean;
642+
/**
643+
* If embedded nodes may use reverse properties
644+
* Defaults to false.
645+
*/
646+
rdfstarReverseInEmbedded?: boolean;
645647
}

lib/ParsingContext.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class ParsingContext {
3737
public readonly normalizeLanguageTags?: boolean;
3838
public readonly streamingProfileAllowOutOfOrderPlainType?: boolean;
3939
public readonly rdfstar: boolean;
40+
public readonly rdfstarReverseInEmbedded?: boolean;
4041

4142
// Stack of indicating if a depth has been touched.
4243
public readonly processingStack: boolean[];
@@ -64,10 +65,10 @@ export class ParsingContext {
6465
public readonly jsonLiteralStack: boolean[];
6566
// Triples that don't know their subject @id yet.
6667
// L0: stack depth; L1: values
67-
public readonly unidentifiedValuesBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean }[][];
68+
public readonly unidentifiedValuesBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean, isEmbedded: boolean }[][];
6869
// Quads that don't know their graph @id yet.
6970
// L0: stack depth; L1: values
70-
public readonly unidentifiedGraphsBuffer: { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term }[][];
71+
public readonly unidentifiedGraphsBuffer: { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term, isEmbedded: boolean }[][];
7172

7273
// Depths that should be still flushed
7374
public pendingContainerFlushBuffers: { depth: number, keys: any[] }[];
@@ -94,6 +95,7 @@ export class ParsingContext {
9495
this.normalizeLanguageTags = options.normalizeLanguageTags;
9596
this.streamingProfileAllowOutOfOrderPlainType = options.streamingProfileAllowOutOfOrderPlainType;
9697
this.rdfstar = options.rdfstar !== false;
98+
this.rdfstarReverseInEmbedded = options.rdfstarReverseInEmbedded;
9799

98100
this.topLevelProperties = false;
99101
this.activeProcessingMode = parseFloat(this.processingMode);
@@ -344,7 +346,7 @@ export class ParsingContext {
344346
* @return {{predicate: Term; object: Term; reverse: boolean}[]} An element of
345347
* {@link ParsingContext.unidentifiedValuesBuffer}.
346348
*/
347-
public getUnidentifiedValueBufferSafe(depth: number): { predicate: RDF.Term, object: RDF.Term, reverse: boolean }[] {
349+
public getUnidentifiedValueBufferSafe(depth: number): { predicate: RDF.Term, object: RDF.Term, reverse: boolean, isEmbedded: boolean }[] {
348350
let buffer = this.unidentifiedValuesBuffer[depth];
349351
if (!buffer) {
350352
buffer = [];
@@ -359,7 +361,7 @@ export class ParsingContext {
359361
* @return {{predicate: Term; object: Term; reverse: boolean}[]} An element of
360362
* {@link ParsingContext.unidentifiedGraphsBuffer}.
361363
*/
362-
public getUnidentifiedGraphBufferSafe(depth: number): { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term }[] {
364+
public getUnidentifiedGraphBufferSafe(depth: number): { subject: RDF.Term, predicate: RDF.Term, object: RDF.Term, isEmbedded: boolean }[] {
363365
let buffer = this.unidentifiedGraphsBuffer[depth];
364366
if (!buffer) {
365367
buffer = [];

lib/Util.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ export class Util {
142142
return parentKey === '@reverse' !== Util.isContextValueReverse(context, key);
143143
}
144144

145+
/**
146+
* Check if the given key exists inside an embedded node as direct child.
147+
* @param {JsonLdContextNormalized} context A JSON-LD context.
148+
* @param {string} parentKey The parent key.
149+
* @return {boolean} If the property is embedded.
150+
*/
151+
public static isPropertyInEmbeddedNode(parentKey: string): boolean {
152+
return parentKey === '@id';
153+
}
154+
145155
/**
146156
* Check if the given IRI is valid.
147157
* @param {string} iri A potential IRI.
@@ -416,7 +426,18 @@ export class Util {
416426
if (value["@type"] === '@vocab') {
417427
return this.nullableTermToArray(this.createVocabOrBaseTerm(context, value["@id"]));
418428
} else {
419-
return this.nullableTermToArray(this.resourceToTerm(context, value["@id"]));
429+
const valueId = value["@id"];
430+
let valueTerm: RDF.Term | null;
431+
if (typeof valueId === 'object') {
432+
if (this.parsingContext.rdfstar) {
433+
valueTerm = this.parsingContext.idStack[depth + 1][0];
434+
} else {
435+
throw new ErrorCoded(`Found illegal @id '${value}'`, ERROR_CODES.INVALID_ID_VALUE);
436+
}
437+
} else {
438+
valueTerm = this.resourceToTerm(context, valueId);
439+
}
440+
return this.nullableTermToArray(valueTerm);
420441
}
421442
} else {
422443
// Only make a blank node if at least one triple was emitted at the value's level.
@@ -883,4 +904,60 @@ export class Util {
883904
return keyUnaliased === '@none' ? null : keyUnaliased;
884905
}
885906

907+
/**
908+
* Check if no reverse properties are present in embedded nodes.
909+
* @param key The current key.
910+
* @param reverse If a reverse property is active.
911+
* @param isEmbedded If we're in an embedded node.
912+
*/
913+
public validateReverseInEmbeddedNode(key: string, reverse: boolean, isEmbedded: boolean): void {
914+
if (isEmbedded && reverse && !this.parsingContext.rdfstarReverseInEmbedded) {
915+
throw new ErrorCoded(`Illegal reverse property in embedded node in ${key}`,
916+
ERROR_CODES.INVALID_EMBEDDED_NODE);
917+
}
918+
}
919+
920+
/**
921+
* Emit a quad, with checks.
922+
* @param depth The current depth.
923+
* @param subject S
924+
* @param predicate P
925+
* @param object O
926+
* @param graph G
927+
* @param reverse If a reverse property is active.
928+
* @param isEmbedded If we're in an embedded node.
929+
*/
930+
public emitQuadChecked(
931+
depth: number,
932+
subject: RDF.Term, predicate: RDF.Term, object: RDF.Term, graph: RDF.Term,
933+
reverse: boolean, isEmbedded: boolean,
934+
): void {
935+
// Create a quad
936+
let quad: RDF.BaseQuad;
937+
if (reverse) {
938+
this.validateReverseSubject(object);
939+
quad = this.dataFactory.quad(object, predicate, subject, graph);
940+
} else {
941+
quad = this.dataFactory.quad(subject, predicate, object, graph);
942+
}
943+
944+
// Emit the quad, unless it was created in an embedded node
945+
if (isEmbedded) {
946+
// Embedded nodes don't inherit the active graph
947+
if (quad.graph.termType !== 'DefaultGraph') {
948+
quad = this.dataFactory.quad(quad.subject, quad.predicate, quad.object);
949+
}
950+
951+
// Multiple embedded nodes are not allowed
952+
if (this.parsingContext.idStack[depth - 1]) {
953+
throw new ErrorCoded(`Illegal multiple properties in an embedded node`,
954+
ERROR_CODES.INVALID_EMBEDDED_NODE)
955+
}
956+
957+
this.parsingContext.idStack[depth - 1] = [ quad ];
958+
} else {
959+
this.parsingContext.emitQuad(depth, quad);
960+
}
961+
}
962+
886963
}

lib/containerhandler/ContainerHandlerIndex.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class ContainerHandlerIndex implements IContainerHandler {
6969
// Otherwise, attach the index to the node identifier
7070
for (const indexValue of indexValues) {
7171
await EntryHandlerPredicate.handlePredicateObject(parsingContext, util, keys, depth + 1,
72-
indexProperty, indexValue, false);
72+
indexProperty, indexValue, false, false);
7373
}
7474
}
7575
}

lib/containerhandler/ContainerHandlerType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class ContainerHandlerType implements IContainerHandler {
5959
if (type) {
6060
// Push the type to the stack using the rdf:type predicate
6161
await EntryHandlerPredicate.handlePredicateObject(parsingContext, util, keys, depth + 1,
62-
util.rdfType, type, false);
62+
util.rdfType, type, false, false);
6363
}
6464

6565
// Flush any pending flush buffers

lib/entryhandler/EntryHandlerPredicate.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
2020
* @param {Term} predicate The predicate.
2121
* @param {Term} object The object.
2222
* @param {boolean} reverse If the property is reversed.
23+
* @param {boolean} isEmbedded If the property exists in an embedded node as direct child.
2324
* @return {Promise<void>} A promise resolving when handling is done.
2425
*/
2526
public static async handlePredicateObject(parsingContext: ParsingContext, util: Util, keys: any[], depth: number,
26-
predicate: RDF.Term, object: RDF.Term, reverse: boolean) {
27+
predicate: RDF.Term, object: RDF.Term,
28+
reverse: boolean, isEmbedded: boolean) {
2729
const depthProperties: number = await util.getPropertiesDepth(keys, depth);
2830
const depthOffsetGraph = await util.getDepthOffsetGraph(depth, keys);
2931
const depthPropertiesGraph: number = depth - depthOffsetGraph;
@@ -39,41 +41,31 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
3941
if (graphs) {
4042
for (const graph of graphs) {
4143
// Emit our quad if graph @id is known
42-
if (reverse) {
43-
util.validateReverseSubject(object);
44-
parsingContext.emitQuad(depth, util.dataFactory.quad(object, predicate, subject, graph));
45-
} else {
46-
parsingContext.emitQuad(depth, util.dataFactory.quad(subject, predicate, object, graph));
47-
}
44+
util.emitQuadChecked(depth, subject, predicate, object, graph, reverse, isEmbedded);
4845
}
4946
} else {
5047
// Buffer our triple if graph @id is not known yet.
5148
if (reverse) {
5249
util.validateReverseSubject(object);
5350
parsingContext.getUnidentifiedGraphBufferSafe(depthPropertiesGraph - 1).push(
54-
{subject: object, predicate, object: subject});
51+
{subject: object, predicate, object: subject, isEmbedded });
5552
} else {
5653
parsingContext.getUnidentifiedGraphBufferSafe(depthPropertiesGraph - 1)
57-
.push({subject, predicate, object});
54+
.push({subject, predicate, object, isEmbedded});
5855
}
5956
}
6057
} else {
6158
// Emit if no @graph was applicable
6259
const graph = await util.getGraphContainerValue(keys, depthProperties);
63-
if (reverse) {
64-
util.validateReverseSubject(object);
65-
parsingContext.emitQuad(depth, util.dataFactory.quad(object, predicate, subject, graph));
66-
} else {
67-
parsingContext.emitQuad(depth, util.dataFactory.quad(subject, predicate, object, graph));
68-
}
60+
util.emitQuadChecked(depth, subject, predicate, object, graph, reverse, isEmbedded);
6961
}
7062
}
7163
} else {
7264
// Buffer until our @id becomes known, or we go up the stack
7365
if (reverse) {
7466
util.validateReverseSubject(object);
7567
}
76-
parsingContext.getUnidentifiedValueBufferSafe(depthProperties).push({predicate, object, reverse});
68+
parsingContext.getUnidentifiedValueBufferSafe(depthProperties).push({predicate, object, reverse, isEmbedded});
7769
}
7870
}
7971

@@ -116,7 +108,15 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
116108
const objects = await util.valueToTerm(context, key, value, depth, keys);
117109
if (objects.length) {
118110
for (let object of objects) {
119-
const reverse = Util.isPropertyReverse(context, keyOriginal, await util.unaliasKeywordParent(keys, depth));
111+
let parentKey = await util.unaliasKeywordParent(keys, depth);
112+
const reverse = Util.isPropertyReverse(context, keyOriginal, parentKey);
113+
if (parentKey === '@reverse') {
114+
// Check parent of parent when checking if we're in an embedded node if in @reverse
115+
depth--;
116+
parentKey = await util.unaliasKeywordParent(keys, depth);
117+
}
118+
const isEmbedded = Util.isPropertyInEmbeddedNode(parentKey);
119+
util.validateReverseInEmbeddedNode(key, reverse, isEmbedded);
120120

121121
if (value) {
122122
// Special case if our term was defined as an @list, but does not occur in an array,
@@ -143,7 +143,7 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
143143
}
144144

145145
await EntryHandlerPredicate.handlePredicateObject(parsingContext, util, keys, depth,
146-
predicate, object, reverse);
146+
predicate, object, reverse, isEmbedded);
147147
}
148148
}
149149
}

lib/entryhandler/keyword/EntryHandlerKeywordId.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ export class EntryHandlerKeywordId extends EntryHandlerKeyword {
1919
public async handle(parsingContext: ParsingContext, util: Util, key: any, keys: any[], value: any, depth: number)
2020
: Promise<any> {
2121
if (typeof value !== 'string') {
22-
parsingContext.emitError(new ErrorCoded(`Found illegal @id '${value}'`, ERROR_CODES.INVALID_ID_VALUE));
22+
// JSON-LD-star allows @id object values
23+
if (parsingContext.rdfstar && typeof value === 'object') {
24+
const valueKeys = Object.keys(value);
25+
if (valueKeys.length === 1 && valueKeys[0] === '@id') {
26+
parsingContext.emitError(new ErrorCoded(`Invalid embedded node without property with @id ${value['@id']}`,
27+
ERROR_CODES.INVALID_EMBEDDED_NODE))
28+
}
29+
} else {
30+
parsingContext.emitError(new ErrorCoded(`Found illegal @id '${value}'`, ERROR_CODES.INVALID_ID_VALUE));
31+
}
32+
return;
2333
}
2434

2535
// Determine the canonical place for this id.

lib/entryhandler/keyword/EntryHandlerKeywordType.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export class EntryHandlerKeywordType extends EntryHandlerKeyword {
2626
// as it's possible that the @type is used to identify the datatype of a literal, which we ignore here.
2727
const context = await parsingContext.getContext(keys);
2828
const predicate = util.rdfType;
29-
const reverse = Util.isPropertyReverse(context, keyOriginal, await util.unaliasKeywordParent(keys, depth));
29+
const parentKey = await util.unaliasKeywordParent(keys, depth);
30+
const reverse = Util.isPropertyReverse(context, keyOriginal, parentKey);
31+
const isEmbedded = Util.isPropertyInEmbeddedNode(parentKey);
32+
util.validateReverseInEmbeddedNode(key, reverse, isEmbedded);
3033

3134
// Handle multiple values if the value is an array
3235
const elements = Array.isArray(value) ? value : [ value ];
@@ -37,7 +40,7 @@ export class EntryHandlerKeywordType extends EntryHandlerKeyword {
3740
const type = util.createVocabOrBaseTerm(context, element);
3841
if (type) {
3942
await EntryHandlerPredicate.handlePredicateObject(parsingContext, util, keys, depth,
40-
predicate, type, reverse);
43+
predicate, type, reverse, isEmbedded);
4144
}
4245
}
4346

0 commit comments

Comments
 (0)