Skip to content

Commit

Permalink
Parse @type: @JSON as JSON string literals
Browse files Browse the repository at this point in the history
  • Loading branch information
rubensworks committed Mar 16, 2020
1 parent de47949 commit fcf8750
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 19 deletions.
4 changes: 3 additions & 1 deletion lib/JsonLdParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,9 @@ export class JsonLdParser extends Transform {
this.parsingContext.emittedStack.splice(this.lastDepth, 1);
this.parsingContext.idStack.splice(this.lastDepth, 1);
this.parsingContext.graphStack.splice(this.lastDepth + 1, 1);
this.parsingContext.literalStack.splice(this.lastDepth, 1);
this.parsingContext.jsonLiteralStack.splice(this.lastDepth, 1);
this.parsingContext.validationStack.splice(this.lastDepth - 1, 2);
this.parsingContext.literalStack.splice(this.lastDepth, 1);
}
this.lastDepth = depth;

Expand Down Expand Up @@ -348,6 +349,7 @@ export class JsonLdParser extends Transform {
}
this.parsingContext.unidentifiedValuesBuffer.splice(depth, 1);
this.parsingContext.literalStack.splice(depth, 1);
this.parsingContext.jsonLiteralStack.splice(depth, 1);
}

// Flush graphs at this level
Expand Down
3 changes: 3 additions & 0 deletions lib/ParsingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export class ParsingContext {
public readonly validationStack: { valid: boolean, property: boolean }[];
// Stack with cached unaliased keywords.
public readonly unaliasedKeywordCacheStack: any[];
// Stack of flags indicating if the node is a JSON literal
public readonly jsonLiteralStack: boolean[];
// Triples that don't know their subject @id yet.
// L0: stack depth; L1: values
public readonly unidentifiedValuesBuffer: { predicate: RDF.Term, object: RDF.Term, reverse: boolean }[][];
Expand Down Expand Up @@ -95,6 +97,7 @@ export class ParsingContext {
this.literalStack = [];
this.validationStack = [];
this.unaliasedKeywordCacheStack = [];
this.jsonLiteralStack = [];
this.unidentifiedValuesBuffer = [];
this.unidentifiedGraphsBuffer = [];

Expand Down
23 changes: 21 additions & 2 deletions lib/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import {ContextParser, ERROR_CODES, ErrorCoded, IJsonLdContextNormalized} from "
import * as RDF from "rdf-js";
import {ParsingContext} from "./ParsingContext";

// tslint:disable-next-line:no-var-requires
const canonicalizeJson = require('canonicalize');

/**
* Utility functions and methods.
*/
Expand All @@ -18,6 +21,7 @@ export class Util {
public readonly rdfRest: RDF.NamedNode;
public readonly rdfNil: RDF.NamedNode;
public readonly rdfType: RDF.NamedNode;
public readonly rdfJson: RDF.NamedNode;

private readonly parsingContext: ParsingContext;

Expand All @@ -29,6 +33,7 @@ export class Util {
this.rdfRest = this.dataFactory.namedNode(Util.RDF + 'rest');
this.rdfNil = this.dataFactory.namedNode(Util.RDF + 'nil');
this.rdfType = this.dataFactory.namedNode(Util.RDF + 'type');
this.rdfJson = this.dataFactory.namedNode(Util.RDF + 'JSON');
}

/**
Expand Down Expand Up @@ -159,6 +164,11 @@ export class Util {
*/
public async valueToTerm(context: IJsonLdContextNormalized, key: string,
value: any, depth: number, keys: string[]): Promise<RDF.Term> {
// Skip further processing if we have an @type: @json
if (Util.getContextValueType(context, key) === '@json') {
return this.dataFactory.literal(this.valueToJsonString(value), this.rdfJson);
}

const type: string = typeof value;
switch (type) {
case 'object':
Expand Down Expand Up @@ -534,6 +544,15 @@ export class Util {
}
}

/**
* Stringify the given JSON object to a canonical JSON string.
* @param value Any valid JSON value.
* @return {string} A canonical JSON string.
*/
public valueToJsonString(value: any): string {
return canonicalizeJson(value);
}

/**
* If the key is not a keyword, try to check if it is an alias for a keyword,
* and if so, un-alias it.
Expand Down Expand Up @@ -598,7 +617,7 @@ export class Util {
}

/**
* Check if we are processing a literal at the given depth.
* Check if we are processing a literal (including JSON literals) at the given depth.
* This will also check higher levels,
* because if a parent is a literal,
* then the deeper levels are definitely a literal as well.
Expand All @@ -607,7 +626,7 @@ export class Util {
*/
public isLiteral(depth: number): boolean {
for (let i = depth; i >= 0; i--) {
if (this.parsingContext.literalStack[i]) {
if (this.parsingContext.literalStack[i] || this.parsingContext.jsonLiteralStack[i]) {
return true;
}
}
Expand Down
46 changes: 30 additions & 16 deletions lib/entryhandler/EntryHandlerPredicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,18 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {

public async validate(parsingContext: ParsingContext, util: Util, keys: any[], depth: number, inProperty: boolean)
: Promise<boolean> {
return keys[depth] && !!await util.predicateToTerm(await parsingContext.getContext(keys), keys[depth]);
const key = keys[depth];
if (key) {
const context = await parsingContext.getContext(keys);
if (await util.predicateToTerm(context, keys[depth])) {
// If this valid predicate is of type @json, mark it so in the stack so that no deeper handling of nodes occurs.
if (Util.getContextValueType(context, key) === '@json') {
parsingContext.jsonLiteralStack[depth + 1] = true;
}
return true;
}
}
return false;
}

public async test(parsingContext: ParsingContext, util: Util, key: any, keys: any[], depth: number)
Expand All @@ -99,22 +110,25 @@ export class EntryHandlerPredicate implements IEntryHandler<boolean> {
let object = await util.valueToTerm(objectContext, key, value, depth, keys);
if (object) {
const reverse = Util.isPropertyReverse(context, keyOriginal, parentKey);
// Special case if our term was defined as an @list, but does not occur in an array,
// In that case we just emit it as an RDF list with a single element.
const listValueContainer = Util.getContextValueContainer(context, key) === '@list';
if (listValueContainer || value['@list']) {
if ((listValueContainer || (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,
util.getDefaultGraph()));
parsingContext.emitQuad(depth, util.dataFactory.quad(listPointer, util.rdfFirst, object,
util.getDefaultGraph()));
object = listPointer;
}

// Lists are not allowed in @reverse'd properties
if (reverse && !parsingContext.allowSubjectList) {
throw new Error(`Found illegal list value in subject position at ${key}`);
if (value) {
// Special case if our term was defined as an @list, but does not occur in an array,
// In that case we just emit it as an RDF list with a single element.
const listValueContainer = Util.getContextValueContainer(context, key) === '@list';
if (listValueContainer || value['@list']) {
if ((listValueContainer || (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,
util.getDefaultGraph()));
parsingContext.emitQuad(depth, util.dataFactory.quad(listPointer, util.rdfFirst, object,
util.getDefaultGraph()));
object = listPointer;
}

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

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"dependencies": {
"@rdfjs/data-model": "^1.1.1",
"@types/rdf-js": "^2.0.1",
"canonicalize": "^1.0.1",
"jsonld-context-parser": "^1.3.3",
"jsonparse": "^1.3.1"
},
Expand Down
131 changes: 131 additions & 0 deletions test/JsonLdParser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as dataFactory from "@rdfjs/data-model";
import {blankNode, defaultGraph, literal, namedNode, quad, triple} from "@rdfjs/data-model";
import each from 'jest-each';
import "jest-rdf";
import {ERROR_CODES, ErrorCoded} from "jsonld-context-parser";
import {PassThrough} from "stream";
import {Util} from "../lib/Util";

Expand Down Expand Up @@ -5607,6 +5608,136 @@ describe('JsonLdParser', () => {
]);
});
});

describe('JSON literals', () => {
it('should error in 1.0', async () => {
parser = new JsonLdParser({ processingMode: '1.0' });
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": true
}
`);
return expect(arrayifyStream(stream.pipe(parser))).rejects
.toThrow(new ErrorCoded(`A context @type must be an absolute IRI, found: 'e': '@json'`,
ERROR_CODES.INVALID_TYPE_MAPPING));
});

it('with a single literal value', async () => {
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": true
}
`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(blankNode(''),
namedNode('http://example.com/vocab/json'),
literal('true', namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'))),
]);
});

it('with a single null value', async () => {
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": null
}
`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(blankNode(''),
namedNode('http://example.com/vocab/json'),
literal('null', namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'))),
]);
});

it('with a JSON object', async () => {
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": { "a": true }
}
`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(blankNode(''),
namedNode('http://example.com/vocab/json'),
literal('{"a":true}', namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'))),
]);
});

it('with a JSON object that contains an entry looking like a valid URI', async () => {
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": { "http://example.org/predicate": true }
}
`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(blankNode(''),
namedNode('http://example.com/vocab/json'),
literal('{"http://example.org/predicate":true}',
namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'))),
]);
});

it('with a JSON object that should be canonicalized', async () => {
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": { "zzz": "z", "b": 3, "a": true }
}
`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(blankNode(''),
namedNode('http://example.com/vocab/json'),
literal('{"a":true,"b":3,"zzz":"z"}', namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'))),
]);
});

it('with a JSON array', async () => {
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": [ "a", true ]
}
`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(blankNode(''),
namedNode('http://example.com/vocab/json'),
literal('["a",true]', namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'))),
]);
});

it('with nested JSON', async () => {
const stream = streamifyString(`
{
"@context": {
"e": {"@id": "http://example.com/vocab/json", "@type": "@json"}
},
"e": { "a": [ "a", true ] }
}
`);
return expect(await arrayifyStream(stream.pipe(parser))).toBeRdfIsomorphic([
quad(blankNode(''),
namedNode('http://example.com/vocab/json'),
literal('{"a":["a",true]}', namedNode('http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON'))),
]);
});
});
});

describe('should not parse', () => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,11 @@ camelcase@^5.0.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==

canonicalize@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.1.tgz#657b4f3fa38a6ecb97a9e5b7b26d7a19cc6e0da9"
integrity sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg==

capture-exit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
Expand Down

0 comments on commit fcf8750

Please sign in to comment.