diff --git a/CHANGELOG.md b/CHANGELOG.md index cebdefb4..3f9b554a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # jsonld ChangeLog +### Added +- Support literal JSON. + - **NOTE**: The JSON serialization is based on the JSON Canonicalization + Scheme (JCS) drafts. Changes in the JCS algorithm could cause changes in + the `toRdf` output. + ## 1.7.0 - 2019-08-30 ### Added diff --git a/lib/constants.js b/lib/constants.js index 17511c75..d503e7c2 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -17,6 +17,7 @@ module.exports = { RDF_TYPE: RDF + 'type', RDF_PLAIN_LITERAL: RDF + 'PlainLiteral', RDF_XML_LITERAL: RDF + 'XMLLiteral', + RDF_JSON_LITERAL: RDF + 'JSON', RDF_OBJECT: RDF + 'object', RDF_LANGSTRING: RDF + 'langString', diff --git a/lib/context.js b/lib/context.js index 6143ed5a..5f629050 100644 --- a/lib/context.js +++ b/lib/context.js @@ -499,7 +499,7 @@ api.createTermDefinition = ( {code: 'invalid type mapping', context: localCtx}); } - if(type !== '@id' && type !== '@vocab') { + if(type !== '@id' && type !== '@vocab' && type !== '@json') { // expand @type to full IRI type = _expandIri( activeCtx, type, {vocab: true, base: false}, localCtx, defined, @@ -1114,6 +1114,7 @@ api.isKeyword = v => { case '@graph': case '@id': case '@index': + case '@json': case '@language': case '@list': case '@nest': diff --git a/lib/expand.js b/lib/expand.js index 994debaf..56458e45 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -23,7 +23,8 @@ const { expandIri: _expandIri, getContextValue: _getContextValue, isKeyword: _isKeyword, - process: _processContext + process: _processContext, + processingMode: _processingMode } = require('./context'); const { @@ -289,6 +290,8 @@ api.expand = ({ 'Invalid JSON-LD syntax; only strings may be language-tagged.', 'jsonld.SyntaxError', {code: 'invalid language-tagged value', element: rval}); + } else if(_processingMode(activeCtx, 1.1) && types.includes('@json') && types.length === 1) { + // Any value of @value is okay if @type: @json } else if(!types.every(t => (_isAbsoluteIri(t) && !(_isString(t) && t.indexOf('_:') === 0) || _isEmptyObject(t)))) { @@ -494,16 +497,7 @@ function _expandObject({ 'jsonld.SyntaxError', {code: 'invalid @graph value', value}); } - // @value must not be an object or an array (unless framing) if(expandedProperty === '@value') { - if((_isObject(value) || _isArray(value)) && !options.isFrame) { - throw new JsonLdError( - 'Invalid JSON-LD syntax; "@value" value must not be an ' + - 'object or an array.', - 'jsonld.SyntaxError', - {code: 'invalid value object value', value}); - } - _addValue( expandedParent, '@value', value, {propertyIsArray: options.isFrame}); continue; @@ -669,6 +663,12 @@ function _expandObject({ insideList: isList, expansionMap }); + } else if( + _getContextValue(activeCtx, key, '@type') === '@json') { + expandedValue = { + '@type': '@json', + '@value': value + }; } else { // recursively expand value with key as new active property expandedValue = api.expand({ @@ -747,6 +747,20 @@ function _expandObject({ }); } + // @value must not be an object or an array (unless framing) or if @type is @json + if('@value' in element) { + const value = element['@value']; + if(element['@type'] === '@json' && _processingMode(activeCtx, 1.1)) { + // allow any value, to be verified when the object is fully expanded and the @type is @json. + } else if((_isObject(value) || _isArray(value)) && !options.isFrame) { + throw new JsonLdError( + 'Invalid JSON-LD syntax; "@value" value must not be an ' + + 'object or an array.', + 'jsonld.SyntaxError', + {code: 'invalid value object value', value}); + } + } + // expand each nested key for(const key of nests) { const nestedValues = _isArray(element[key]) ? element[key] : [element[key]]; diff --git a/lib/fromRdf.js b/lib/fromRdf.js index 5ce1ed2e..02217763 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -3,6 +3,7 @@ */ 'use strict'; +const JsonLdError = require('./JsonLdError'); const graphTypes = require('./graphTypes'); const types = require('./types'); const util = require('./util'); @@ -17,6 +18,7 @@ const { RDF_TYPE, // RDF_PLAIN_LITERAL, // RDF_XML_LITERAL, + RDF_JSON_LITERAL, // RDF_OBJECT, // RDF_LANGSTRING, @@ -285,6 +287,17 @@ function _RDFToObject(o, useNativeTypes) { if(!type) { type = XSD_STRING; } + if(type === RDF_JSON_LITERAL) { + type = '@json'; + try { + rval['@value'] = JSON.parse(rval['@value']); + } catch(e) { + throw new JsonLdError( + 'JSON literal could not be parsed.', + 'jsonld.InvalidJsonLiteral', + {code: 'invalid JSON literal', value: rval['@value'], cause: e}); + } + } // use native types for certain xsd types if(useNativeTypes) { if(type === XSD_BOOLEAN) { diff --git a/lib/toRdf.js b/lib/toRdf.js index 634cc01d..e6a3281f 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -6,6 +6,7 @@ const {createNodeMap} = require('./nodeMap'); const {isKeyword} = require('./context'); const graphTypes = require('./graphTypes'); +const jsonCanonicalize = require('canonicalize'); const types = require('./types'); const util = require('./util'); @@ -18,6 +19,7 @@ const { RDF_TYPE, // RDF_PLAIN_LITERAL, // RDF_XML_LITERAL, + RDF_JSON_LITERAL, // RDF_OBJECT, RDF_LANGSTRING, @@ -224,8 +226,11 @@ function _objectToRDF(item, issuer, dataset, graphTerm) { let value = item['@value']; const datatype = item['@type'] || null; - // convert to XSD datatypes as appropriate - if(types.isBoolean(value)) { + // convert to XSD/JSON datatypes as appropriate + if(datatype === '@json') { + object.value = jsonCanonicalize(value); + object.datatype.value = RDF_JSON_LITERAL; + } else if(types.isBoolean(value)) { object.value = value.toString(); object.datatype.value = datatype || XSD_BOOLEAN; } else if(types.isDouble(value) || datatype === XSD_DOUBLE) { diff --git a/package.json b/package.json index c1659e17..c6199456 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "lib/**/*.js" ], "dependencies": { + "canonicalize": "^1.0.1", "rdf-canonize": "^1.0.2", "request": "^2.88.0", "semver": "^5.6.0", diff --git a/tests/misc.js b/tests/misc.js index e8139f00..d65bf477 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -519,3 +519,20 @@ describe('js keywords', () => { assert.deepStrictEqual(e, ex); }); }); + +describe('literal JSON', () => { + it('handles error', done => { + const d = +'_:b0 "bogus"^^ .' +; + const p = jsonld.fromRDF(d); + assert(p instanceof Promise); + p.then(() => { + assert.fail(); + }).catch(e => { + assert(e); + assert.equal(e.name, 'jsonld.InvalidJsonLiteral'); + done(); + }); + }); +}); diff --git a/tests/test-common.js b/tests/test-common.js index fdec75f7..fd06d5a6 100644 --- a/tests/test-common.js +++ b/tests/test-common.js @@ -60,16 +60,6 @@ const TEST_TYPES = { /compact-manifest.jsonld#tpi04$/, /compact-manifest.jsonld#tpi05$/, /compact-manifest.jsonld#tpi06$/, - // JSON literals - /compact-manifest.jsonld#tjs01$/, - /compact-manifest.jsonld#tjs02$/, - /compact-manifest.jsonld#tjs03$/, - /compact-manifest.jsonld#tjs04$/, - /compact-manifest.jsonld#tjs05$/, - /compact-manifest.jsonld#tjs06$/, - /compact-manifest.jsonld#tjs07$/, - /compact-manifest.jsonld#tjs08$/, - /compact-manifest.jsonld#tjs09$/, // IRI confusion /compact-manifest.jsonld#te002$/, // @propogate @@ -171,18 +161,6 @@ const TEST_TYPES = { /expand-manifest.jsonld#tpi09$/, /expand-manifest.jsonld#tpi10$/, /expand-manifest.jsonld#tpi11$/, - // JSON literals - /expand-manifest.jsonld#tjs01$/, - /expand-manifest.jsonld#tjs02$/, - /expand-manifest.jsonld#tjs03$/, - /expand-manifest.jsonld#tjs04$/, - /expand-manifest.jsonld#tjs05$/, - /expand-manifest.jsonld#tjs06$/, - /expand-manifest.jsonld#tjs07$/, - /expand-manifest.jsonld#tjs08$/, - /expand-manifest.jsonld#tjs09$/, - /expand-manifest.jsonld#tjs10$/, - /expand-manifest.jsonld#tjs11$/, // misc /expand-manifest.jsonld#te043$/, /expand-manifest.jsonld#te044$/, @@ -336,14 +314,6 @@ const TEST_TYPES = { specVersion: ['json-ld-1.0'], // FIXME idRegex: [ - // JSON literals - /fromRdf-manifest.jsonld#tjs01$/, - /fromRdf-manifest.jsonld#tjs02$/, - /fromRdf-manifest.jsonld#tjs03$/, - /fromRdf-manifest.jsonld#tjs04$/, - /fromRdf-manifest.jsonld#tjs05$/, - /fromRdf-manifest.jsonld#tjs06$/, - /fromRdf-manifest.jsonld#tjs07$/, ] }, fn: 'fromRDF', @@ -406,20 +376,6 @@ const TEST_TYPES = { /html-manifest.jsonld#tr020$/, /html-manifest.jsonld#tr021$/, /html-manifest.jsonld#tr022$/, - // JSON literal - /toRdf-manifest.jsonld#tjs01$/, - /toRdf-manifest.jsonld#tjs02$/, - /toRdf-manifest.jsonld#tjs03$/, - /toRdf-manifest.jsonld#tjs04$/, - /toRdf-manifest.jsonld#tjs05$/, - /toRdf-manifest.jsonld#tjs06$/, - /toRdf-manifest.jsonld#tjs07$/, - /toRdf-manifest.jsonld#tjs08$/, - /toRdf-manifest.jsonld#tjs09$/, - /toRdf-manifest.jsonld#tjs10$/, - /toRdf-manifest.jsonld#tjs11$/, - /toRdf-manifest.jsonld#tjs12$/, - /toRdf-manifest.jsonld#tjs13$/, // number fixes /toRdf-manifest.jsonld#trt01$/, // IRI resolution