From 2117c3b2264e2d8cf2eccf39a19e6aa962fb81f4 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 17 Nov 2023 21:13:57 -0500 Subject: [PATCH 1/4] Drop support for Node.js < 18. --- .github/workflows/main.yml | 20 ++++++++++---------- CHANGELOG.md | 5 +++++ package.json | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08332857..b0e6f903 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,9 +8,9 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: @@ -23,9 +23,9 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x, 16.x, 18.x, 20.x] + node-version: [18.x, 20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: @@ -41,10 +41,10 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [20.x] bundler: [webpack, browserify] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: @@ -62,9 +62,9 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: @@ -78,9 +78,9 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0926a8..f9838c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # jsonld ChangeLog +## 9.0.0 - 2023-xx-xx + +### Changed +- **BREAKING**: Drop support for Node.js < 18. + ## 8.3.2 - 2023-12-06 ### Fixed diff --git a/package.json b/package.json index d1e84e37..474df3c0 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "webpack-merge": "^5.8.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "keywords": [ "JSON", From 20fa0cf0e0e734b5f281dc8026afd3df04cc457f Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 17 Nov 2023 22:52:10 -0500 Subject: [PATCH 2/4] Update to `rdf-canonize@4`. - **BREAKING**: See the `rdf-canonize` 4.0.0 changelog for **important** changes and upgrade notes. - Update to handle different RDF/JS dataset `BlankNode` format. - Enable pass through of numerous possible `rdf-canonize` options in a `canonize()` `canonizeOptions` parameter. - Update to use `rdf-canonize` options. - The `URDNA2015` default algorithm has been changed to `RDFC-1.0` from `rdf-canon`. - Complexity control defaults `maxWorkFactor` or `maxDeepIterations` may need to be adjusted to process graphs with certain blank node constructs. - A `signal` option is available to use an `AbortSignal` to limit resource usage. - The internal digest algorithm can be changed. --- CHANGELOG.md | 10 ++ lib/fromRdf.js | 43 +++++-- lib/jsonld.js | 46 ++++--- lib/toRdf.js | 66 ++++++---- package.json | 2 +- tests/misc.js | 2 +- tests/test.js | 339 +++++++++++++++++++++++++++++++------------------ 7 files changed, 330 insertions(+), 178 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9838c2c..68289422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ ### Changed - **BREAKING**: Drop support for Node.js < 18. +- **BREAKING**: Upgrade dependencies. + - `rdf-canonize@4`: See the [rdf-canonize][] 4.0.0 changelog for + **important** changes and upgrade notes. Of note: + - The `URDNA2015` default algorithm has been changed to `RDFC-1.0` from + [rdf-canon][]. + - Complexity control defaults `maxWorkFactor` or `maxDeepIterations` may + need to be adjusted to process graphs with certain blank node constructs. + - A `signal` option is available to use an `AbortSignal` to limit resource + usage. + - The internal digest algorithm can be changed. ## 8.3.2 - 2023-12-06 diff --git a/lib/fromRdf.js b/lib/fromRdf.js index 01098353..795ec9af 100644 --- a/lib/fromRdf.js +++ b/lib/fromRdf.js @@ -89,7 +89,7 @@ api.fromRDF = async ( const nodeMap = graphMap[name]; // get subject, predicate, object - const s = quad.subject.value; + const s = _nodeId(quad.subject); const p = quad.predicate.value; const o = quad.object; @@ -98,13 +98,14 @@ api.fromRDF = async ( } const node = nodeMap[s]; - const objectIsNode = o.termType.endsWith('Node'); - if(objectIsNode && !(o.value in nodeMap)) { - nodeMap[o.value] = {'@id': o.value}; + const objectNodeId = _nodeId(o); + const objectIsNode = !!objectNodeId; + if(objectIsNode && !(objectNodeId in nodeMap)) { + nodeMap[objectNodeId] = {'@id': objectNodeId}; } if(p === RDF_TYPE && !useRdfType && objectIsNode) { - _addValue(node, '@type', o.value, {propertyIsArray: true}); + _addValue(node, '@type', objectNodeId, {propertyIsArray: true}); continue; } @@ -114,9 +115,9 @@ api.fromRDF = async ( // object may be an RDF list/partial list node but we can't know easily // until all triples are read if(objectIsNode) { - if(o.value === RDF_NIL) { + if(objectNodeId === RDF_NIL) { // track rdf:nil uniquely per graph - const object = nodeMap[o.value]; + const object = nodeMap[objectNodeId]; if(!('usages' in object)) { object.usages = []; } @@ -125,12 +126,12 @@ api.fromRDF = async ( property: p, value }); - } else if(o.value in referencedOnce) { + } else if(objectNodeId in referencedOnce) { // object referenced more than once - referencedOnce[o.value] = false; + referencedOnce[objectNodeId] = false; } else { // keep track of single reference - referencedOnce[o.value] = { + referencedOnce[objectNodeId] = { node, property: p, value @@ -303,8 +304,9 @@ api.fromRDF = async ( */ function _RDFToObject(o, useNativeTypes, rdfDirection, options) { // convert NamedNode/BlankNode object to JSON-LD - if(o.termType.endsWith('Node')) { - return {'@id': o.value}; + const nodeId = _nodeId(o); + if(nodeId) { + return {'@id': nodeId}; } // convert literal to JSON-LD @@ -397,3 +399,20 @@ function _RDFToObject(o, useNativeTypes, rdfDirection, options) { return rval; } + +/** + * Return id for a term. Handles BlankNodes and NamedNodes. Adds a '_:' prefix + * for BlanksNodes. + * + * @param term a term object. + * + * @return the Node term id or null. + */ +function _nodeId(term) { + if(term.termType === 'NamedNode') { + return term.value; + } else if(term.termType === 'BlankNode') { + return '_:' + term.value; + } + return null; +} diff --git a/lib/jsonld.js b/lib/jsonld.js index c6931aeb..a2a563bb 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -523,7 +523,7 @@ jsonld.link = async function(input, ctx, options) { /** * Performs RDF dataset normalization on the given input. The input is JSON-LD * unless the 'inputFormat' option is used. The output is an RDF dataset - * unless the 'format' option is used. + * unless a non-null 'format' option is used. * * Note: Canonicalization sets `safe` to `true` and `base` to `null` by * default in order to produce safe outputs and "fail closed" by default. This @@ -531,25 +531,31 @@ jsonld.link = async function(input, ctx, options) { * allow unsafe defaults (for cryptographic usage) in order to comply with the * JSON-LD 1.1 specification. * - * @param input the input to normalize as JSON-LD or as a format specified by - * the 'inputFormat' option. + * @param input the input to normalize as JSON-LD given as an RDF dataset or as + * a format specified by the 'inputFormat' option. * @param [options] the options to use: - * [algorithm] the normalization algorithm to use, `URDNA2015` or - * `URGNA2012` (default: `URDNA2015`). * [base] the base IRI to use (default: `null`). * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. Some well-formed * and safe-mode checks may be omitted. - * [inputFormat] the format if input is not JSON-LD: - * 'application/n-quads' for N-Quads. - * [format] the format if output is a string: - * 'application/n-quads' for N-Quads. + * [inputFormat] the input format. null for a JSON-LD object, + * 'application/n-quads' for N-Quads. (default: null) + * [format] the output format. null for an RDF dataset, + * 'application/n-quads' for an N-Quads string. (default: N-Quads) * [documentLoader(url, options)] the document loader. - * [useNative] true to use a native canonize algorithm * [rdfDirection] null or 'i18n-datatype' to support RDF * transformation of @direction (default: null). * [safe] true to use safe mode. (default: true). + * [canonizeOptions] options to pass to rdf-canonize canonize(). See + * rdf-canonize for more details. Commonly used options, and their + * defaults, are: + * algorithm="RDFC-1.0", + * messageDigestAlgorithm="sha256", + * canonicalIdMap, + * maxWorkFactor=1, + * maxDeepIterations=-1, + * and signal=null. * [contextResolver] internal use only. * * @return a Promise that resolves to the normalized output. @@ -559,15 +565,19 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { throw new TypeError('Could not canonize, too few arguments.'); } - // set default options + // set toRDF options options = _setDefaults(options, { - base: _isString(input) ? input : null, - algorithm: 'URDNA2015', skipExpansion: false, safe: true, contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); + + // set canonize options + const canonizeOptions = Object.assign({}, { + algorithm: 'RDFC-1.0' + }, options.canonizeOptions || null); + if('inputFormat' in options) { if(options.inputFormat !== 'application/n-quads' && options.inputFormat !== 'application/nquads') { @@ -579,17 +589,18 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { const parsedInput = NQuads.parse(input); // do canonicalization - return canonize.canonize(parsedInput, options); + return canonize.canonize(parsedInput, canonizeOptions); } // convert to RDF dataset then do normalization const opts = {...options}; delete opts.format; + delete opts.canonizeOptions; opts.produceGeneralizedRdf = false; const dataset = await jsonld.toRDF(input, opts); // do canonicalization - return canonize.canonize(dataset, options); + return canonize.canonize(dataset, canonizeOptions); }; /** @@ -653,8 +664,8 @@ jsonld.fromRDF = async function(dataset, options) { * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. Some well-formed * and safe-mode checks may be omitted. - * [format] the format to use to output a string: - * 'application/n-quads' for N-Quads. + * [format] the output format. null for an RDF dataset, + * 'application/n-quads' for an N-Quads string. (default: null) * [produceGeneralizedRdf] true to output generalized RDF, false * to produce only standard RDF (default: false). * [documentLoader(url, options)] the document loader. @@ -672,7 +683,6 @@ jsonld.toRDF = async function(input, options) { // set default options options = _setDefaults(options, { - base: _isString(input) ? input : '', skipExpansion: false, contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) diff --git a/lib/toRdf.js b/lib/toRdf.js index 53f20af4..e8a54844 100644 --- a/lib/toRdf.js +++ b/lib/toRdf.js @@ -63,12 +63,7 @@ api.toRDF = (input, options) => { if(graphName === '@default') { graphTerm = {termType: 'DefaultGraph', value: ''}; } else if(_isAbsoluteIri(graphName)) { - if(graphName.startsWith('_:')) { - graphTerm = {termType: 'BlankNode'}; - } else { - graphTerm = {termType: 'NamedNode'}; - } - graphTerm.value = graphName; + graphTerm = _makeTerm(graphName); } else { // skip relative IRIs (not valid RDF) if(options.eventHandler) { @@ -119,10 +114,7 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { for(const item of items) { // RDF subject - const subject = { - termType: id.startsWith('_:') ? 'BlankNode' : 'NamedNode', - value: id - }; + const subject = _makeTerm(id); // skip relative IRI subjects (not valid RDF) if(!_isAbsoluteIri(id)) { @@ -144,10 +136,7 @@ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { } // RDF predicate - const predicate = { - termType: property.startsWith('_:') ? 'BlankNode' : 'NamedNode', - value: property - }; + const predicate = _makeTerm(property); // skip relative IRI predicates (not valid RDF) if(!_isAbsoluteIri(property)) { @@ -226,13 +215,16 @@ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection, options) { const last = list.pop(); // Result is the head of the list - const result = last ? {termType: 'BlankNode', value: issuer.getId()} : nil; + const result = last ? { + termType: 'BlankNode', + value: issuer.getId().slice(2) + } : nil; let subject = result; for(const item of list) { const object = _objectToRDF( item, issuer, dataset, graphTerm, rdfDirection, options); - const next = {termType: 'BlankNode', value: issuer.getId()}; + const next = {termType: 'BlankNode', value: issuer.getId().slice(2)}; dataset.push({ subject, predicate: first, @@ -284,14 +276,16 @@ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection, options) { function _objectToRDF( item, issuer, dataset, graphTerm, rdfDirection, options ) { - const object = {}; + let object; // convert value object to RDF if(graphTypes.isValue(item)) { - object.termType = 'Literal'; - object.value = undefined; - object.datatype = { - termType: 'NamedNode' + object = { + termType: 'Literal', + value: undefined, + datatype: { + termType: 'NamedNode' + } }; let value = item['@value']; const datatype = item['@type'] || null; @@ -374,13 +368,14 @@ function _objectToRDF( } else if(graphTypes.isList(item)) { const _list = _listToRDF( item['@list'], issuer, dataset, graphTerm, rdfDirection, options); - object.termType = _list.termType; - object.value = _list.value; + object = { + termType: _list.termType, + value: _list.value + }; } else { // convert string/node object to RDF const id = types.isObject(item) ? item['@id'] : item; - object.termType = id.startsWith('_:') ? 'BlankNode' : 'NamedNode'; - object.value = id; + object = _makeTerm(id); } // skip relative IRIs, not valid RDF @@ -404,3 +399,24 @@ function _objectToRDF( return object; } + +/** + * Make a term from an id. Handles BlankNodes and NamedNodes based on a + * possible '_:' id prefix. The prefix is removed for BlankNodes. + * + * @param id a term id. + * + * @return a term object. + */ +function _makeTerm(id) { + if(id.startsWith('_:')) { + return { + termType: 'BlankNode', + value: id.slice(2) + }; + } + return { + termType: 'NamedNode', + value: id + }; +} diff --git a/package.json b/package.json index 474df3c0..51285da0 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@digitalbazaar/http-client": "^3.4.1", "canonicalize": "^1.0.1", "lru-cache": "^6.0.0", - "rdf-canonize": "^3.4.0" + "rdf-canonize": "^4.0.1" }, "devDependencies": { "@babel/core": "^7.21.8", diff --git a/tests/misc.js b/tests/misc.js index 908052cd..21b3d25e 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -4030,7 +4030,7 @@ _:b0 "v" . ] ; const nq = `\ -_:b0 <_:b1> "v" . +_:b0 _:b1 "v" . `; await _test({ diff --git a/tests/test.js b/tests/test.js index a0af0eba..e9aa9f51 100644 --- a/tests/test.js +++ b/tests/test.js @@ -349,38 +349,73 @@ const TEST_TYPES = { ], compare: compareCanonizedExpectedNQuads }, - 'rdfc:Urgna2012EvalTest': { - fn: 'normalize', + 'rdfc:RDFC10EvalTest': { + skip: { + // NOTE: idRegex format: + // /manifest-urdna2015#testNNN$/, + // FIXME + idRegex: [ + // Unsupported U escape + // /manifest-urdna2015#test060/ + ] + }, + fn: 'canonize', params: [ readTestNQuads('action'), createTestOptions({ - algorithm: 'URGNA2012', + algorithm: 'RDFC-1.0', inputFormat: 'application/n-quads', format: 'application/n-quads' }) ], compare: compareExpectedNQuads }, - 'rdfc:Urdna2015EvalTest': { + 'rdfc:RDFC10NegativeEvalTest': { skip: { // NOTE: idRegex format: - // /manifest-urdna2015#testNNN$/, - // FIXME - idRegex: [ - // Unsupported U escape - /manifest-urdna2015#test060/ - ] + // /manifest-rdfc10#testNNN$/, + idRegex: [] }, fn: 'canonize', params: [ readTestNQuads('action'), createTestOptions({ - algorithm: 'URDNA2015', + algorithm: 'RDFC-1.0', + inputFormat: 'application/n-quads', + format: 'application/n-quads' + }) + ] + }, + 'rdfc:RDFC10MapTest': { + skip: { + // NOTE: idRegex format: + // /manifest-rdfc10#testNNN$/, + idRegex: [] + }, + fn: 'canonize', + params: [ + readTestNQuads('action'), + createTestOptions({ + algorithm: 'RDFC-1.0', inputFormat: 'application/n-quads', format: 'application/n-quads' }) ], - compare: compareExpectedNQuads + preRunAdjustParams: ({params, extra}) => { + // add canonicalIdMap + const m = new Map(); + extra.canonicalIdMap = m; + params[1].canonizeOptions = params[1].canonizeOptions || {}; + params[1].canonizeOptions.canonicalIdMap = m; + return params; + }, + postRunAdjustParams: ({params}) => { + // restore output param to empty map + const m = new Map(); + params[1].canonizeOptions = params[1].canonizeOptions || {}; + params[1].canonizeOptions.canonicalIdMap = m; + }, + compare: compareExpectedCanonicalIdMap } }; @@ -429,8 +464,6 @@ if(options.earl && options.earl.filename) { } } -return new Promise(resolve => { - // async generated tests // _tests => [{suite}, ...] // suite => { @@ -440,21 +473,20 @@ return new Promise(resolve => { // } const _tests = []; -return addManifest(manifest, _tests) - .then(() => { - return _testsToMocha(_tests); - }).then(result => { - if(options.earl.report) { - describe('Writing EARL report to: ' + options.earl.filename, function() { - // print out EARL even if .only was used - const _it = result.hadOnly ? it.only : it; - _it('should print the earl report', function() { - return options.writeFile( - options.earl.filename, options.earl.report.reportJson()); - }); - }); - } - }).then(() => resolve()); +await addManifest(manifest, _tests); +const result = _testsToMocha(_tests); +if(options.earl.report) { + describe('Writing EARL report to: ' + options.earl.filename, function() { + // print out EARL even if .only was used + const _it = result.hadOnly ? it.only : it; + _it('should print the earl report', function() { + return options.writeFile( + options.earl.filename, options.earl.report.reportJson()); + }); + }); +} + +return; // build mocha tests from local test structure function _testsToMocha(tests) { @@ -485,89 +517,102 @@ function _testsToMocha(tests) { }; } -}); - /** * Adds the tests for all entries in the given manifest. * - * @param manifest {Object} the manifest. - * @param parent {Object} the parent test structure - * @return {Promise} + * @param {object} manifest - The manifest. + * @param {object} parent - The parent test structure. + * @returns {Promise} - A promise with no value. */ -function addManifest(manifest, parent) { - return new Promise((resolve, reject) => { - // create test structure - const suite = { - title: manifest.name || manifest.label, - tests: [], - suites: [], - imports: [] - }; - parent.push(suite); - - // get entries and sequence (alias for entries) - const entries = [].concat( - getJsonLdValues(manifest, 'entries'), - getJsonLdValues(manifest, 'sequence') - ); - - const includes = getJsonLdValues(manifest, 'include'); - // add includes to sequence as jsonld files - for(let i = 0; i < includes.length; ++i) { - entries.push(includes[i] + '.jsonld'); +async function addManifest(manifest, parent) { + // create test structure + const suite = { + title: manifest.name || manifest.label, + tests: [], + suites: [], + imports: [] + }; + parent.push(suite); + + // get entries and sequence (alias for entries) + const entries = [].concat( + getJsonLdValues(manifest, 'entries'), + getJsonLdValues(manifest, 'sequence') + ); + + const includes = getJsonLdValues(manifest, 'include'); + // add includes to sequence as jsonld files + for(let i = 0; i < includes.length; ++i) { + entries.push(includes[i] + '.jsonld'); + } + + // resolve all entry promises and process + for await (const entry of await Promise.all(entries)) { + if(typeof entry === 'string' && entry.endsWith('js')) { + // process later as a plain JavaScript file + suite.imports.push(entry); + continue; + } else if(typeof entry === 'function') { + // process as a function that returns a promise + const childSuite = await entry(options); + if(suite) { + suite.suites.push(childSuite); + } + continue; + } + const manifestEntry = await readManifestEntry(manifest, entry); + if(isJsonLdType(manifestEntry, '__SKIP__')) { + // special local skip logic + suite.tests.push(manifestEntry); + } else if(isJsonLdType(manifestEntry, 'mf:Manifest')) { + // entry is another manifest + await addManifest(manifestEntry, suite.suites); + } else { + // assume entry is a test + await addTest(manifest, manifestEntry, suite.tests); } + } +} - // resolve all entry promises and process - Promise.all(entries).then(entries => { - let p = Promise.resolve(); - entries.forEach(entry => { - if(typeof entry === 'string' && entry.endsWith('js')) { - // process later as a plain JavaScript file - suite.imports.push(entry); - return; - } else if(typeof entry === 'function') { - // process as a function that returns a promise - p = p.then(() => { - return entry(options); - }).then(childSuite => { - if(suite) { - suite.suites.push(childSuite); - } - }); - return; - } - p = p.then(() => { - return readManifestEntry(manifest, entry); - }).then(entry => { - if(isJsonLdType(entry, '__SKIP__')) { - // special local skip logic - suite.tests.push(entry); - } else if(isJsonLdType(entry, 'mf:Manifest')) { - // entry is another manifest - return addManifest(entry, suite.suites); - } else { - // assume entry is a test - return addTest(manifest, entry, suite.tests); - } - }); - }); - return p; - }).then(() => { - resolve(); - }).catch(err => { - console.error(err); - reject(err); - }); - }); +/** + * Common adjust params helper. + * + * @param {object} params - The param to adjust. + * @param {object} test - The test. + */ +function _commonAdjustParams(params, test) { + if(isJsonLdType(test, 'rdfc:RDFC10EvalTest') || + isJsonLdType(test, 'rdfc:RDFC10MapTest') || + isJsonLdType(test, 'rdfc:RDFC10NegativeEvalTest')) { + if(test.hashAlgorithm) { + params.canonizeOptions = params.canonizeOptions || {}; + params.canonizeOptions.messageDigestAlgorithm = test.hashAlgorithm; + } + if(test.computationalComplexity === 'low') { + // simple test cases + params.canonizeOptions = params.canonizeOptions || {}; + params.canonizeOptions.maxWorkFactor = 0; + } + if(test.computationalComplexity === 'medium') { + // tests between O(n) and O(n^2) + params.canonizeOptions = params.canonizeOptions || {}; + params.canonizeOptions.maxWorkFactor = 2; + } + if(test.computationalComplexity === 'high') { + // poison tests between O(n^2) and O(n^3) + params.canonizeOptions = params.canonizeOptions || {}; + params.canonizeOptions.maxWorkFactor = 3; + } + } } /** * Adds a test. * - * @param manifest {Object} the manifest. - * @param test {Object} the test. - * @param tests {Array} the list of tests to add to. - * @return {Promise} + * @param {object} manifest - The manifest. + * @param {object} test - The test. + * @param {Array} tests - The list of tests to add to. + * @returns {Promise} - A promise with no value. */ async function addTest(manifest, test, tests) { // expand @id and input base @@ -597,6 +642,10 @@ async function addTest(manifest, test, tests) { title: description + ` (jobs=${jobs})`, f: makeFn({ test, + adjustParams: params => { + _commonAdjustParams(params[1], test); + return params; + }, run: ({/*test, */testInfo, params}) => { // skip Promise.all if(jobs === 1 && fast1) { @@ -770,7 +819,14 @@ function makeFn({ }); }); - const params = adjustParams(testInfo.params.map(param => param(test))); + let params = testInfo.params.map(param => param(test)); + const extra = {}; + // type specific pre run adjustments + if(testInfo.preRunAdjustParams) { + params = testInfo.preRunAdjustParams({params, extra}); + } + // general adjustments + params = adjustParams(params); // resolve test data const values = await Promise.all(params); // copy used to check inputs do not change @@ -780,6 +836,10 @@ function makeFn({ // run and capture errors and results try { result = await run({test, testInfo, params: values}); + // type specific post run adjustments + if(testInfo.postRunAdjustParams) { + testInfo.postRunAdjustParams({params: values, extra}); + } // check input not changed assert.deepStrictEqual(valuesOrig, values); } catch(e) { @@ -789,21 +849,25 @@ function makeFn({ try { if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { if(!isBenchmark) { - await compareExpectedError(test, err); + await compareExpectedError({test, err}); + } + } else if(isJsonLdType(test, 'rdfc:RDFC10NegativeEvalTest')) { + if(!isBenchmark) { + await checkError({test, err}); } } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || - isJsonLdType(test, 'rdfc:Urgna2012EvalTest') || - isJsonLdType(test, 'rdfc:Urdna2015EvalTest')) { + isJsonLdType(test, 'rdfc:RDFC10EvalTest') || + isJsonLdType(test, 'rdfc:RDFC10MapTest')) { if(err) { throw err; } if(!isBenchmark) { - await testInfo.compare(test, result); + await testInfo.compare({test, result, extra}); } } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) { // no checks } else { - throw Error('Unknown test type: ' + test.type); + throw new Error(`Unknown test type: "${test.type}"`); } let benchmarkResult = null; @@ -1013,11 +1077,11 @@ function _getExpectProperty(test) { } else if('result' in test) { return 'result'; } else { - throw Error('No expected output property found'); + throw new Error('No expected output property found'); } } -async function compareExpectedJson(test, result) { +async function compareExpectedJson({test, result}) { let expect; try { expect = await readTestJson(_getExpectProperty(test))(test); @@ -1032,7 +1096,7 @@ async function compareExpectedJson(test, result) { } } -async function compareExpectedNQuads(test, result) { +async function compareExpectedNQuads({test, result}) { let expect; try { expect = await readTestNQuads(_getExpectProperty(test))(test); @@ -1047,11 +1111,15 @@ async function compareExpectedNQuads(test, result) { } } -async function compareCanonizedExpectedNQuads(test, result) { +async function compareCanonizedExpectedNQuads({test, result}) { let expect; try { expect = await readTestNQuads(_getExpectProperty(test))(test); - const opts = {algorithm: 'URDNA2015'}; + const opts = { + algorithm: 'RDFC-1.0', + // some tests need this: expand 0027 and 0062 + maxWorkFactor: 2 + }; const expectDataset = rdfCanonize.NQuads.parse(expect); const expectCmp = await rdfCanonize.canonize(expectDataset, opts); const resultDataset = rdfCanonize.NQuads.parse(result); @@ -1067,7 +1135,35 @@ async function compareCanonizedExpectedNQuads(test, result) { } } -async function compareExpectedError(test, err) { +async function compareExpectedCanonicalIdMap({test, result, extra}) { + let expect; + try { + expect = await readTestJson(_getExpectProperty(test))(test); + const expectMap = new Map(Object.entries(expect)); + assert.deepStrictEqual(extra.canonicalIdMap, expectMap); + } catch(err) { + if(options.bailOnError) { + console.log('\nTEST FAILED\n'); + console.log('EXPECTED:\n ' + JSON.stringify(expect, null, 2)); + console.log('ACTUAL:\n' + JSON.stringify(result, null, 2)); + } + throw err; + } +} + +async function checkError({/*test,*/ err}) { + try { + assert.ok(err, 'no error present'); + } catch(_err) { + if(options.bailOnError) { + console.log('\nTEST FAILED\n'); + console.log('EXPECTED ERROR'); + } + throw _err; + } +} + +async function compareExpectedError({test, err}) { let expect; let result; try { @@ -1088,10 +1184,7 @@ async function compareExpectedError(test, err) { } function isJsonLdType(node, type) { - const nodeType = [].concat( - getJsonLdValues(node, '@type'), - getJsonLdValues(node, 'type') - ); + const nodeType = getJsonLdType(node); type = Array.isArray(type) ? type : [type]; for(let i = 0; i < type.length; ++i) { if(nodeType.indexOf(type[i]) !== -1) { @@ -1101,13 +1194,17 @@ function isJsonLdType(node, type) { return false; } +function getJsonLdType(node) { + return [].concat( + getJsonLdValues(node, '@type'), + getJsonLdValues(node, 'type') + ); +} + function getJsonLdValues(node, property) { let rval = []; if(property in node) { - rval = node[property]; - if(!Array.isArray(rval)) { - rval = [rval]; - } + rval = [].concat(node[property]); } return rval; } From 2409091d7043e760dbb9c5d79e5f9041e0ca4934 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 17 Nov 2023 22:58:39 -0500 Subject: [PATCH 3/4] Remove support for `application/nquads` alias. --- CHANGELOG.md | 3 +++ lib/jsonld.js | 7 ++----- tests/misc.js | 31 ++++++++++++------------------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68289422..e4db0bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ usage. - The internal digest algorithm can be changed. +### Removed +- **BREAKING**: Remove `application/nquads` alias for `application/n-quads`. + ## 8.3.2 - 2023-12-06 ### Fixed diff --git a/lib/jsonld.js b/lib/jsonld.js index a2a563bb..0a001b72 100644 --- a/lib/jsonld.js +++ b/lib/jsonld.js @@ -579,8 +579,7 @@ jsonld.normalize = jsonld.canonize = async function(input, options) { }, options.canonizeOptions || null); if('inputFormat' in options) { - if(options.inputFormat !== 'application/n-quads' && - options.inputFormat !== 'application/nquads') { + if(options.inputFormat !== 'application/n-quads') { throw new JsonLdError( 'Unknown canonicalization input format.', 'jsonld.CanonizeError'); @@ -700,8 +699,7 @@ jsonld.toRDF = async function(input, options) { // output RDF dataset const dataset = _toRDF(expanded, options); if(options.format) { - if(options.format === 'application/n-quads' || - options.format === 'application/nquads') { + if(options.format === 'application/n-quads') { return NQuads.serialize(dataset); } throw new JsonLdError( @@ -1007,7 +1005,6 @@ jsonld.unregisterRDFParser = function(contentType) { // register the N-Quads RDF parser jsonld.registerRDFParser('application/n-quads', NQuads.parse); -jsonld.registerRDFParser('application/nquads', NQuads.parse); /* URL API */ jsonld.url = require('./url'); diff --git a/tests/misc.js b/tests/misc.js index 21b3d25e..b9ebbbc3 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -143,19 +143,18 @@ describe('other toRDF tests', () => { }); }); - it('should handle deprecated N-Quads format', done => { + it('should fail for deprecated N-Quads format', done => { const doc = { "@id": "https://example.com/", "https://example.com/test": "test" }; const p = jsonld.toRDF(doc, {format: 'application/nquads'}); assert(p instanceof Promise); - p.catch(e => { - assert.ifError(e); - }).then(output => { - assert.equal( - output, - ' "test" .\n'); + p.then(() => { + assert.fail(); + }).catch(e => { + assert(e); + assert.equal(e.name, 'jsonld.UnknownFormat'); done(); }); }); @@ -232,21 +231,15 @@ describe('other fromRDF tests', () => { }); }); - it('should handle deprecated N-Quads format', done => { + it('should fail for deprecated N-Quads format', done => { const nq = ' "test" .\n'; const p = jsonld.fromRDF(nq, {format: 'application/nquads'}); assert(p instanceof Promise); - p.catch(e => { - assert.ifError(e); - }).then(output => { - assert.deepEqual( - output, - [{ - "@id": "https://example.com/", - "https://example.com/test": [{ - "@value": "test" - }] - }]); + p.then(() => { + assert.fail(); + }).catch(e => { + assert(e); + assert.equal(e.name, 'jsonld.UnknownFormat'); done(); }); }); From 735795989a1d2e94964fe8361e199ced240b3fec Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 17 Nov 2023 22:59:17 -0500 Subject: [PATCH 4/4] Update dependencies. --- CHANGELOG.md | 2 ++ package.json | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4db0bfb..11908faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Changed - **BREAKING**: Drop support for Node.js < 18. - **BREAKING**: Upgrade dependencies. + - `@digitalbazaar/http-client@4`. + - `canonicalize@2`. - `rdf-canonize@4`: See the [rdf-canonize][] 4.0.0 changelog for **important** changes and upgrade notes. Of note: - The `URDNA2015` default algorithm has been changed to `RDFC-1.0` from diff --git a/package.json b/package.json index 51285da0..488fc4b8 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "lib/**/*.js" ], "dependencies": { - "@digitalbazaar/http-client": "^3.4.1", - "canonicalize": "^1.0.1", + "@digitalbazaar/http-client": "^4.0.0", + "canonicalize": "^2.0.0", "lru-cache": "^6.0.0", "rdf-canonize": "^4.0.1" },