diff --git a/CHANGELOG.md b/CHANGELOG.md index 441828e..741f15a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @digitalbazaar/cborld ChangeLog +## 8.0.1 - 2025-05-dd + +### Fixed +- Ensure omitted `"@id"` values in term definitions are resolved using the + term key if possible (for keys that are CURIEs or absolute URLs). + ## 8.0.0 - 2025-04-24 ### Changed diff --git a/lib/ActiveContext.js b/lib/ActiveContext.js index d07d4e3..d9b7605 100644 --- a/lib/ActiveContext.js +++ b/lib/ActiveContext.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2021-2025 Digital Bazaar, Inc. All rights reserved. */ import {CborldError} from './CborldError.js'; @@ -155,19 +155,33 @@ function _resolveCurie({activeTermMap, context, possibleCurie}) { } function _resolveCuries({activeTermMap, context, newTermMap}) { - for(const def of newTermMap.values()) { + for(const [key, def] of newTermMap.entries()) { const id = def['@id']; const type = def['@type']; if(id !== undefined) { def['@id'] = _resolveCurie({ activeTermMap, context, possibleCurie: id }); + } else { + // if `key` is a CURIE/absolute URL, then "@id" can be computed + const resolved = _resolveCurie({ + activeTermMap, context, possibleCurie: key + }); + if(resolved.includes(':')) { + def['@id'] = resolved; + } } if(type !== undefined) { def['@type'] = _resolveCurie({ activeTermMap, context, possibleCurie: type }); } + if(typeof def['@id'] !== 'string') { + throw new CborldError( + 'ERR_INVALID_TERM_DEFINITION', + `Invalid JSON-LD term definition for "${key}"; the "@id" value ` + + 'could not be determined.'); + } } } diff --git a/lib/ContextLoader.js b/lib/ContextLoader.js index f9050d2..cc889d6 100644 --- a/lib/ContextLoader.js +++ b/lib/ContextLoader.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2021-2025 Digital Bazaar, Inc. All rights reserved. */ import {FIRST_CUSTOM_TERM_ID, KEYWORDS_TABLE, reverseMap} from './tables.js'; import {CborldError} from './CborldError.js'; @@ -114,11 +114,11 @@ export class ContextLoader { // normalize definition to an object if(typeof def === 'string') { def = {'@id': def}; - } else if(!(typeof def === 'object' && typeof def['@id'] === 'string')) { + } else if(Object.prototype.toString.call(def) !== '[object Object]') { throw new CborldError( 'ERR_INVALID_TERM_DEFINITION', `Invalid JSON-LD term definition for "${key}"; it must be ` + - 'a string or an object with "@id".'); + 'a string or an object.'); } // set term definition diff --git a/tests/encode.spec.js b/tests/encode.spec.js index 8aa942b..a958792 100644 --- a/tests/encode.spec.js +++ b/tests/encode.spec.js @@ -59,43 +59,41 @@ describe('cborld encode', () => { expect(cborldBytes).equalBytes('d9cb1d8202a0'); }); - it('should fail to encode with no typeTableLoader id found', - async () => { - const jsonldDocument = {}; - let result; - let error; - try { - result = await encode({ - jsonldDocument, - format: 'cbor-ld-1.0', - registryEntryId: 2, - typeTableLoader: _makeTypeTableLoader([]) - }); - } catch(e) { - error = e; - } - expect(result).to.eql(undefined); - expect(error?.code).to.eql('ERR_NO_TYPETABLE'); - }); + it('should fail to encode with no typeTableLoader id found', async () => { + const jsonldDocument = {}; + let result; + let error; + try { + result = await encode({ + jsonldDocument, + format: 'cbor-ld-1.0', + registryEntryId: 2, + typeTableLoader: _makeTypeTableLoader([]) + }); + } catch(e) { + error = e; + } + expect(result).to.eql(undefined); + expect(error?.code).to.eql('ERR_NO_TYPETABLE'); + }); - it('should fail with typeTable', - async () => { - const jsonldDocument = {}; - let result; - let error; - try { - result = await encode({ - jsonldDocument, - format: 'cbor-ld-1.0', - registryEntryId: 1, - typeTable: new Map() - }); - } catch(e) { - error = e; - } - expect(result).to.eql(undefined); - expect(error?.name).to.eql('TypeError'); - }); + it('should fail with typeTable', async () => { + const jsonldDocument = {}; + let result; + let error; + try { + result = await encode({ + jsonldDocument, + format: 'cbor-ld-1.0', + registryEntryId: 1, + typeTable: new Map() + }); + } catch(e) { + error = e; + } + expect(result).to.eql(undefined); + expect(error?.name).to.eql('TypeError'); + }); it('should encode an empty JSON-LD Document', async () => { const jsonldDocument = {}; @@ -151,6 +149,100 @@ describe('cborld encode', () => { expect(cborldBytes).equalBytes('d9cb1d821a3b9aca00a0'); }); + it('should fail with non-object term definition', async () => { + const CONTEXT_URL = 'urn:foo'; + const CONTEXT = { + '@context': { + foo: [] + } + }; + const jsonldDocument = { + '@context': CONTEXT_URL, + foo: 'anything' + }; + + const documentLoader = url => { + if(url === CONTEXT_URL) { + return { + contextUrl: null, + document: CONTEXT, + documentUrl: url + }; + } + throw new Error(`Refused to load URL "${url}".`); + }; + + const typeTable = new Map(TYPE_TABLE); + + const contextTable = new Map(STRING_TABLE); + contextTable.set(CONTEXT_URL, 0x8000); + typeTable.set('context', contextTable); + + let result; + let error; + try { + result = await encode({ + jsonldDocument, + format: 'cbor-ld-1.0', + registryEntryId: 2, + documentLoader, + typeTableLoader: () => typeTable + }); + } catch(e) { + error = e; + } + expect(result).to.eql(undefined); + expect(error?.name).to.eql('CborldError'); + }); + + it('should fail with non-CURIE term with no "@id"', async () => { + const CONTEXT_URL = 'urn:foo'; + const CONTEXT = { + '@context': { + nonCurie: { + '@type': 'urn:anything' + } + } + }; + const jsonldDocument = { + '@context': CONTEXT_URL, + nonCurie: 'anything' + }; + + const documentLoader = url => { + if(url === CONTEXT_URL) { + return { + contextUrl: null, + document: CONTEXT, + documentUrl: url + }; + } + throw new Error(`Refused to load URL "${url}".`); + }; + + const typeTable = new Map(TYPE_TABLE); + + const contextTable = new Map(STRING_TABLE); + contextTable.set(CONTEXT_URL, 0x8000); + typeTable.set('context', contextTable); + + let result; + let error; + try { + result = await encode({ + jsonldDocument, + format: 'cbor-ld-1.0', + registryEntryId: 2, + documentLoader, + typeTableLoader: () => typeTable + }); + } catch(e) { + error = e; + } + expect(result).to.eql(undefined); + expect(error?.name).to.eql('CborldError'); + }); + it('should encode xsd dateTime when using a prefix', async () => { const CONTEXT_URL = 'urn:foo'; const CONTEXT = { @@ -195,6 +287,50 @@ describe('cborld encode', () => { expect(cborldBytes).equalBytes('d9cb1d8202a20019800018661a6070bb5f'); }); + it('should pass with CURIE term with no "@id"', async () => { + const CONTEXT_URL = 'urn:foo'; + const CONTEXT = { + '@context': { + arbitraryPrefix: 'http://www.w3.org/2001/XMLSchema#', + ex: 'https://test.example#', + 'ex:foo': { + '@type': 'arbitraryPrefix:dateTime' + } + } + }; + const date = '2021-04-09T20:38:55Z'; + const jsonldDocument = { + '@context': CONTEXT_URL, + 'ex:foo': date + }; + + const documentLoader = url => { + if(url === CONTEXT_URL) { + return { + contextUrl: null, + document: CONTEXT, + documentUrl: url + }; + } + throw new Error(`Refused to load URL "${url}".`); + }; + + const typeTable = new Map(TYPE_TABLE); + + const contextTable = new Map(STRING_TABLE); + contextTable.set(CONTEXT_URL, 0x8000); + typeTable.set('context', contextTable); + + const cborldBytes = await encode({ + jsonldDocument, + format: 'cbor-ld-1.0', + registryEntryId: 2, + documentLoader, + typeTableLoader: () => typeTable + }); + expect(cborldBytes).equalBytes('d9cb1d8202a20019800018681a6070bb5f'); + }); + it('should encode xsd dateTime with type table when possible', async () => { const CONTEXT_URL = 'urn:foo'; const CONTEXT = {