Skip to content

Add compound-literal support. #535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# jsonld ChangeLog

## 8.x.x - 2023-xx-xx

### Added
- Support `compound-literal` `rdfDirection` option.

## 8.3.0 - 2023-09-06

### Added
83 changes: 77 additions & 6 deletions lib/fromRdf.js
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ const {

// constants
const {
// RDF,
RDF,
RDF_LIST,
RDF_FIRST,
RDF_REST,
@@ -61,19 +61,21 @@ api.fromRDF = async (
const defaultGraph = {};
const graphMap = {'@default': defaultGraph};
const referencedOnce = {};
let processCompoundLiterals = false;
if(rdfDirection) {
if(rdfDirection === 'compound-literal') {
throw new JsonLdError(
'Unsupported rdfDirection value.',
'jsonld.InvalidRdfDirection',
{value: rdfDirection});
processCompoundLiterals = true;
} else if(rdfDirection !== 'i18n-datatype') {
throw new JsonLdError(
'Unknown rdfDirection value.',
'jsonld.InvalidRdfDirection',
{value: rdfDirection});
}
}
let compoundLiteralSubjects;
if(processCompoundLiterals) {
compoundLiteralSubjects = {};
}

for(const quad of dataset) {
// TODO: change 'name' to 'graph'
@@ -82,11 +84,18 @@ api.fromRDF = async (
if(!(name in graphMap)) {
graphMap[name] = {};
}
if(processCompoundLiterals && !(name in compoundLiteralSubjects)) {
compoundLiteralSubjects[name] = {};
}
if(name !== '@default' && !(name in defaultGraph)) {
defaultGraph[name] = {'@id': name};
}

const nodeMap = graphMap[name];
let compoundMap;
if(processCompoundLiterals) {
compoundMap = compoundLiteralSubjects[name];
}

// get subject, predicate, object
const s = quad.subject.value;
@@ -97,6 +106,9 @@ api.fromRDF = async (
nodeMap[s] = {'@id': s};
}
const node = nodeMap[s];
if(processCompoundLiterals && p === RDF + 'direction') {
compoundMap[s] = true;
}

const objectIsNode = o.termType.endsWith('Node');
if(objectIsNode && !(o.value in nodeMap)) {
@@ -208,6 +220,64 @@ api.fromRDF = async (
for(const name in graphMap) {
const graphObject = graphMap[name];

if(processCompoundLiterals) {
if(name in compoundLiteralSubjects) {
const cls = compoundLiteralSubjects[name];
for(const cl of Object.keys(cls)) {
const clEntry = referencedOnce[cl];
if(!clEntry) {
continue;
}
const node = clEntry.node;
const property = clEntry.property;
//const value = clEntry.value;
const clNode = graphObject[cl];
if(!types.isObject(clNode)) {
continue;
}
delete graphObject[cl];
for(const clReference of node[property]) {
if(clReference['@id'] === cl) {
delete clReference['@id'];
}
const value = clNode[RDF + 'value'];
// FIXME: error on !== 1 value
clReference['@value'] = value[0]['@value'];
const language = clNode[RDF + 'language'];
if(language) {
// FIXME: error on !== 1 language value
const v = language[0]['@value'];
if(!v.match(REGEX_BCP47)) {
throw new JsonLdError(
'Invalid RDF syntax; rdf:language must be valid BCP47.',
'jsonld.SyntaxError',
{
code: 'invalid language-tagged string',
value: v
});
}
clReference['@language'] = v;
}
const direction = clNode[RDF + 'direction'];
if(direction) {
// FIXME: error on !== 1 direction value
const v = direction[0]['@value'];
if(!(v === 'ltr' || v === 'rtl')) {
throw new JsonLdError(
'Invalid RDF syntax; rdf:direction must be "ltr" or "rtl".',
'jsonld.SyntaxError',
{
code: 'invalid base direction',
value: v
});
}
clReference['@direction'] = v;
}
}
}
}
}

// no @lists to be converted, continue
if(!(RDF_NIL in graphObject)) {
continue;
@@ -296,7 +366,8 @@ api.fromRDF = async (
*
* @param o the RDF triple object to convert.
* @param useNativeTypes true to output native types, false not to.
* @param rdfDirection text direction mode [null, i18n-datatype]
* @param rdfDirection text direction mode [null, i18n-datatype,
* compound-literal]
* @param options top level API options
*
* @return the JSON-LD object.
9 changes: 6 additions & 3 deletions lib/jsonld.js
Original file line number Diff line number Diff line change
@@ -547,7 +547,8 @@ jsonld.link = async function(input, ctx, options) {
* 'application/n-quads' for N-Quads.
* [documentLoader(url, options)] the document loader.
* [useNative] true to use a native canonize algorithm
* [rdfDirection] null or 'i18n-datatype' to support RDF
* [rdfDirection] Mode for RDF transformation of @direction. null,
* 'i18n-datatype', or 'compound-literal' (default: null).
* transformation of @direction (default: null).
* [safe] true to use safe mode. (default: true).
* [contextResolver] internal use only.
@@ -605,7 +606,8 @@ jsonld.normalize = jsonld.canonize = async function(input, options) {
* (default: false).
* [useNativeTypes] true to convert XSD types into native types
* (boolean, integer, double), false not to (default: false).
* [rdfDirection] null or 'i18n-datatype' to support RDF
* [rdfDirection] Mode for RDF transformation of @direction. null,
* 'i18n-datatype', or 'compound-literal' (default: null).
* transformation of @direction (default: null).
* [safe] true to use safe mode. (default: false)
*
@@ -659,7 +661,8 @@ jsonld.fromRDF = async function(dataset, options) {
* to produce only standard RDF (default: false).
* [documentLoader(url, options)] the document loader.
* [safe] true to use safe mode. (default: false)
* [rdfDirection] null or 'i18n-datatype' to support RDF
* [rdfDirection] Mode for RDF transformation of @direction. null,
* 'i18n-datatype', or 'compound-literal' (default: null).
* transformation of @direction (default: null).
* [contextResolver] internal use only.
*
74 changes: 69 additions & 5 deletions lib/toRdf.js
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ const {
} = require('./events');

const {
// RDF,
RDF,
// RDF_LIST,
RDF_FIRST,
RDF_REST,
@@ -320,10 +320,74 @@ function _objectToRDF(
object.datatype.value = datatype;
object.value = value;
} else if('@direction' in item && rdfDirection === 'compound-literal') {
throw new JsonLdError(
'Unsupported rdfDirection value.',
'jsonld.InvalidRdfDirection',
{value: rdfDirection});
const language = (item['@language'] || '').toLowerCase();
const direction = item['@direction'];
// blank node
object.termType = 'BlankNode';
object.value = issuer.getId();
object.datatype = undefined;
// value
dataset.push({
subject: {
termType: object.termType,
value: object.value
},
predicate: {
termType: 'NamedNode',
value: RDF + 'value'
},
object: {
termType: 'Literal',
value,
datatype: {
termType: 'NamedNode',
value: XSD_STRING
}
},
graph: graphTerm
});
// language if preset
if(language !== '') {
dataset.push({
subject: {
termType: object.termType,
value: object.value
},
predicate: {
termType: 'NamedNode',
value: RDF + 'language'
},
object: {
termType: 'Literal',
value: language,
datatype: {
termType: 'NamedNode',
value: XSD_STRING
}
},
graph: graphTerm
});
}
// direction
dataset.push({
subject: {
termType: object.termType,
value: object.value
},
predicate: {
termType: 'NamedNode',
value: RDF + 'direction'
},
object: {
termType: 'Literal',
value: direction,
datatype: {
termType: 'NamedNode',
value: XSD_STRING
}
},
graph: graphTerm
});
} else if('@direction' in item && rdfDirection) {
throw new JsonLdError(
'Unknown rdfDirection value.',
147 changes: 147 additions & 0 deletions tests/misc.js
Original file line number Diff line number Diff line change
@@ -3695,6 +3695,17 @@ _:b0 <ex:p> "[null]"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON> .
`;
const _nq_dir_l_d_i18n = `\
<urn:id> <ex:p> "v"^^<https://www.w3.org/ns/i18n#en-us_ltr> .
`;
const _nq_dir_nl_d_cl = `\
<urn:id> <ex:p> _:b0 .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#direction> "ltr" .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#value> "v" .
`;
const _nq_dir_l_d_cl = `\
<urn:id> <ex:p> _:b0 .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#direction> "ltr" .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#language> "en-us" .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#value> "v" .
`;

describe('fromRDF', () => {
@@ -3811,6 +3822,20 @@ _:b0 <ex:p> "[null]"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON> .
});
});

it('should handle no @lang, no @dir, rdfDirection=c-l', async () => {
const input = _nq_dir_nl_nd;
const expected = _json_dir_nl_nd;

await _test({
type: 'fromRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected,
eventCodeLog: [],
testSafe: true
});
});

it('should handle no @lang, @dir, rdfDirection=i18n', async () => {
const input = _nq_dir_nl_d_i18n;
const expected = _json_dir_nl_d;
@@ -3825,6 +3850,20 @@ _:b0 <ex:p> "[null]"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON> .
});
});

it('should handle no @lang, @dir, rdfDirection=c-l', async () => {
const input = _nq_dir_nl_d_cl;
const expected = _json_dir_nl_d;

await _test({
type: 'fromRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected,
eventCodeLog: [],
testSafe: true
});
});

it('should handle @lang, no @dir, rdfDirection=i18n', async () => {
const input = _nq_dir_l_nd_ls;
const expected = _json_dir_l_nd;
@@ -3839,6 +3878,20 @@ _:b0 <ex:p> "[null]"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON> .
});
});

it('should handle @lang, no @dir, rdfDirection=c-l', async () => {
const input = _nq_dir_l_nd_ls;
const expected = _json_dir_l_nd;

await _test({
type: 'fromRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected,
eventCodeLog: [],
testSafe: true
});
});

it('should handle @lang, @dir, rdfDirection=i18n', async () => {
const input = _nq_dir_l_d_i18n;
const expected = _json_dir_l_d;
@@ -3853,6 +3906,20 @@ _:b0 <ex:p> "[null]"^^<http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON> .
});
});

it('should handle @lang, @dir, rdfDirection=c-l', async () => {
const input = _nq_dir_l_d_cl;
const expected = _json_dir_l_d;

await _test({
type: 'fromRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected,
eventCodeLog: [],
testSafe: true
});
});

it('should handle bad rdfDirection', async () => {
const input = _nq_dir_l_d_i18n;

@@ -4101,6 +4168,20 @@ _:b0 <ex:p> "v" .
});
});

it('should handle no @lang, no @dir, rdfDirection=c-l', async () => {
const input = _json_dir_nl_nd;
const nq = _nq_dir_nl_nd;

await _test({
type: 'toRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected: nq,
eventCodeLog: [],
testSafe: true
});
});

it('should handle no @lang, @dir, rdfDirection=null', async () => {
const input = _json_dir_nl_d;
const nq = _nq_dir_nl_nd;
@@ -4131,6 +4212,20 @@ _:b0 <ex:p> "v" .
});
});

it('should handle no @lang, @dir, rdfDirection=c-l', async () => {
const input = _json_dir_nl_d;
const nq = _nq_dir_nl_d_cl;

await _test({
type: 'toRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected: nq,
eventCodeLog: [],
testSafe: true
});
});

it('should handle @lang, no @dir, rdfDirection=null', async () => {
const input = _json_dir_l_nd;
const nq = _nq_dir_l_nd_ls;
@@ -4159,6 +4254,20 @@ _:b0 <ex:p> "v" .
});
});

it('should handle @lang, no @dir, rdfDirection=c-l', async () => {
const input = _json_dir_l_nd;
const nq = _nq_dir_l_nd_ls;

await _test({
type: 'toRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected: nq,
eventCodeLog: [],
testSafe: true
});
});

it('should handle @lang, @dir, rdfDirection=null', async () => {
const input = _json_dir_l_d;
const nq = _nq_dir_l_nd_ls;
@@ -4189,6 +4298,20 @@ _:b0 <ex:p> "v" .
});
});

it('should handle @lang, @dir, rdfDirection=c-l', async () => {
const input = _json_dir_l_d;
const nq = _nq_dir_l_d_cl;

await _test({
type: 'toRDF',
input,
options: {skipExpansion: true, rdfDirection: 'compound-literal'},
expected: nq,
eventCodeLog: [],
testSafe: true
});
});

it('should handle bad rdfDirection', async () => {
const input = _json_dir_l_d;

@@ -4258,6 +4381,30 @@ _:b0 <urn:ex:title> "RTL"^^<https://www.w3.org/ns/i18n#ar-eg_rtl> .
testSafe: true
});
});

it('should handle ctx @lang/@dir/rdfDirection=c-l', async () => {
const input = _ctx_dir_input;
const nq = `\
_:b0 <urn:ex:publisher> "NULL"@ar-eg .
_:b0 <urn:ex:title> _:b1 .
_:b0 <urn:ex:title> _:b2 .
_:b1 <http://www.w3.org/1999/02/22-rdf-syntax-ns#direction> "rtl" .
_:b1 <http://www.w3.org/1999/02/22-rdf-syntax-ns#language> "ar-eg" .
_:b1 <http://www.w3.org/1999/02/22-rdf-syntax-ns#value> "RTL" .
_:b2 <http://www.w3.org/1999/02/22-rdf-syntax-ns#direction> "ltr" .
_:b2 <http://www.w3.org/1999/02/22-rdf-syntax-ns#language> "en" .
_:b2 <http://www.w3.org/1999/02/22-rdf-syntax-ns#value> "LTR" .
`;

await _test({
type: 'toRDF',
input,
options: {skipExpansion: false, rdfDirection: 'compound-literal'},
expected: nq,
eventCodeLog: [],
testSafe: true
});
});
});

describe('various', () => {
11 changes: 1 addition & 10 deletions tests/test.js
Original file line number Diff line number Diff line change
@@ -264,13 +264,7 @@ const TEST_TYPES = {
// NOTE: idRegex format:
// /MMM-manifest#tNNN$/,
// FIXME
idRegex: [
// direction (compound-literal)
/fromRdf-manifest#tdi09$/,
/fromRdf-manifest#tdi10$/,
/fromRdf-manifest#tdi11$/,
/fromRdf-manifest#tdi12$/,
]
idRegex: []
},
fn: 'fromRDF',
params: [
@@ -337,9 +331,6 @@ const TEST_TYPES = {
/toRdf-manifest#te075$/,
/toRdf-manifest#te111$/,
/toRdf-manifest#te112$/,
// direction (compound-literal)
/toRdf-manifest#tdi11$/,
/toRdf-manifest#tdi12$/,
]
},
fn: 'toRDF',